feat: billing system — service plans, wallet, cost calculation

- Backend: billing module with ServicePlan, PricingRule, Wallet, WalletTransaction entities
- Admin can CRUD service plans with hourly/monthly/yearly billing cycles
- Each plan has flexible pricing rules (base_fee, cpu, memory, storage, db addon)
- Wallet system: auto-created per user, charge, deduct, refund, transaction history
- Cost calculation endpoint: cross-cycle conversion (hourly*720=monthly, monthly*12=yearly)
- Frontend: admin billing management page (/dashboard/admin/billing)
- Frontend: user wallet page with balance, quick-charge, transaction history
- Deploy page: cost breakdown shown in Review step (hourly/monthly/yearly)
- Navigation: Wallet link for users, Billing Plans link for admins
This commit is contained in:
keyhan
2026-04-07 01:45:45 +03:30
parent ac489c88d8
commit 4974f88e8c
16 changed files with 1302 additions and 3 deletions
+133
View File
@@ -0,0 +1,133 @@
import {
Controller,
Get,
Post,
Patch,
Delete,
Body,
Param,
Query,
UseGuards,
Request,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { BillingService } from './billing.service';
import {
CreateServicePlanDto,
UpdateServicePlanDto,
ChargeWalletDto,
CalculateCostDto,
} from './dto/billing.dto';
import { RolesGuard } from '../common/guards/roles.guard';
import { Roles } from '../common/decorators/roles.decorator';
import { UserRole } from '../common/enums';
@ApiTags('Billing')
@ApiBearerAuth()
@Controller('billing')
@UseGuards(AuthGuard('jwt'), RolesGuard)
export class BillingController {
constructor(private readonly billingService: BillingService) {}
// ─── Service Plans (Admin) ────────────────────────────────────────
@Post('plans')
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Create a new service plan (Admin)' })
async createPlan(@Body() dto: CreateServicePlanDto) {
return this.billingService.createPlan(dto);
}
@Get('plans')
@ApiOperation({ summary: 'List all service plans' })
async findAllPlans(@Request() req: any) {
if (req.user.role === UserRole.ADMIN) {
return this.billingService.findAllPlans();
}
return this.billingService.findActivePlans();
}
@Get('plans/:id')
@ApiOperation({ summary: 'Get service plan details' })
async findPlan(@Param('id') id: string) {
return this.billingService.findPlan(id);
}
@Patch('plans/:id')
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Update a service plan (Admin)' })
async updatePlan(@Param('id') id: string, @Body() dto: UpdateServicePlanDto) {
return this.billingService.updatePlan(id, dto);
}
@Delete('plans/:id')
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Delete a service plan (Admin)' })
async deletePlan(@Param('id') id: string) {
await this.billingService.deletePlan(id);
return { message: 'Plan deleted' };
}
// ─── Cost Calculation ─────────────────────────────────────────────
@Post('calculate')
@ApiOperation({ summary: 'Calculate cost for an application configuration' })
async calculateCost(@Body() dto: CalculateCostDto) {
return this.billingService.calculateCost(dto);
}
// ─── Wallet (User) ───────────────────────────────────────────────
@Get('wallet')
@ApiOperation({ summary: 'Get my wallet balance' })
async getBalance(@Request() req: any) {
return this.billingService.getBalance(req.user.id);
}
@Post('wallet/charge')
@ApiOperation({ summary: 'Charge my wallet (self top-up)' })
async chargeMyWallet(@Request() req: any, @Body() dto: ChargeWalletDto) {
return this.billingService.chargeWallet(req.user.id, dto.amount, dto.description || 'Self top-up');
}
@Get('wallet/transactions')
@ApiOperation({ summary: 'Get my wallet transactions' })
async getTransactions(@Request() req: any, @Query('limit') limit?: string) {
return this.billingService.getTransactions(req.user.id, limit ? parseInt(limit, 10) : 50);
}
@Post('wallet/pay/:applicationId')
@ApiOperation({ summary: 'Pay for an application from wallet' })
async payForApplication(
@Request() req: any,
@Param('applicationId') applicationId: string,
@Body() body: { amount: number; cycle: string },
) {
return this.billingService.deductWallet(
req.user.id,
body.amount,
`Payment for application (${body.cycle})`,
applicationId,
);
}
// ─── Wallet Admin ─────────────────────────────────────────────────
@Get('admin/wallets')
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'List all wallets (Admin)' })
async getAllWallets() {
return this.billingService.getAllWallets();
}
@Post('admin/wallets/:userId/charge')
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Charge a user\'s wallet (Admin)' })
async adminChargeWallet(
@Param('userId') userId: string,
@Body() dto: ChargeWalletDto,
) {
return this.billingService.adminChargeWallet(userId, dto.amount, dto.description);
}
}