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
+110
View File
@@ -0,0 +1,110 @@
import { IsString, IsEnum, IsOptional, IsBoolean, IsNumber, IsArray, ValidateNested, Min } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { BillingCycle, PricingResourceType } from '../../common/enums';
export class CreatePricingRuleDto {
@ApiProperty({ enum: PricingResourceType })
@IsEnum(PricingResourceType)
resourceType: PricingResourceType;
@ApiProperty({ example: 5000, description: 'Unit price in Toman' })
@IsNumber()
@Min(0)
unitPrice: number;
@ApiPropertyOptional({ example: 'CPU per core per hour' })
@IsOptional()
@IsString()
description?: string;
}
export class CreateServicePlanDto {
@ApiProperty({ example: 'Node.js Basic' })
@IsString()
name: string;
@ApiPropertyOptional({ example: 'Basic plan for Node.js applications' })
@IsOptional()
@IsString()
description?: string;
@ApiProperty({ enum: BillingCycle })
@IsEnum(BillingCycle)
billingCycle: BillingCycle;
@ApiProperty({ type: [CreatePricingRuleDto] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreatePricingRuleDto)
pricingRules: CreatePricingRuleDto[];
}
export class UpdateServicePlanDto {
@ApiPropertyOptional()
@IsOptional()
@IsString()
name?: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
description?: string;
@ApiPropertyOptional({ enum: BillingCycle })
@IsOptional()
@IsEnum(BillingCycle)
billingCycle?: BillingCycle;
@ApiPropertyOptional()
@IsOptional()
@IsBoolean()
isActive?: boolean;
@ApiPropertyOptional({ type: [CreatePricingRuleDto] })
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreatePricingRuleDto)
pricingRules?: CreatePricingRuleDto[];
}
export class ChargeWalletDto {
@ApiProperty({ example: 50000, description: 'Amount to charge in Toman' })
@IsNumber()
@Min(1000)
amount: number;
@ApiPropertyOptional({ example: 'Manual top-up' })
@IsOptional()
@IsString()
description?: string;
}
export class CalculateCostDto {
@ApiProperty({ example: 'nodejs' })
@IsString()
runtime: string;
@ApiProperty({ example: 'postgresql' })
@IsString()
databaseType: string;
@ApiProperty({ example: '500m', description: 'CPU limit' })
@IsString()
cpuLimit: string;
@ApiProperty({ example: '512Mi', description: 'Memory limit' })
@IsString()
memoryLimit: string;
@ApiProperty({ example: 1, description: 'Number of replicas' })
@IsNumber()
@Min(1)
replicas: number;
@ApiProperty({ example: '1Gi', description: 'Database storage size' })
@IsOptional()
@IsString()
dbStorageSize?: string;
}