Files
cloud-host/backend/src/billing/dto/billing.dto.ts
T
keyhan 49726f1dfd feat(billing): add percentage discount coupons
Admins can create coupon codes that discount specific services (app
runtimes, optional services, managed products, custom-domain addon, or
all) and restrict them to specific users or make them public, with total
and per-user usage caps and an active date window.

Coupons apply in deploy, renewal, and upgrade flows: cost-breakdown lines
are tagged with a service key, the eligible portion is discounted and
capped to the payable amount, the invoice records discountAmount/
discountCode, and the redemption is recorded once when the invoice is
fully paid (covering wallet, gateway, and mixed payments).

- Discount + DiscountRedemption entities; invoice discount columns
- DiscountService (CRUD, validation, redemption) + admin/validate API
- Idempotent schema bootstrap on init so production (synchronize off)
  provisions the tables/columns without a migration runner
- Admin discounts UI, coupon entry in deploy/renewal, invoice discount line
- fa/en strings; discount.service unit spec

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 11:48:45 +03:30

334 lines
9.1 KiB
TypeScript

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, AppRuntime, InvoiceStatus, ProductType } from '../../common/enums';
import { OptionalServiceResourcesDto } from './optional-service-resources.dto';
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 Standard' })
@IsString()
name: string;
@ApiProperty({ enum: AppRuntime, example: 'nodejs' })
@IsEnum(AppRuntime)
runtime: AppRuntime;
@ApiPropertyOptional({ example: 'Standard 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({ enum: AppRuntime })
@IsOptional()
@IsEnum(AppRuntime)
runtime?: AppRuntime;
@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 {
@ApiPropertyOptional({ enum: ProductType, default: ProductType.APPLICATION })
@IsOptional()
@IsEnum(ProductType)
productType?: ProductType;
@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;
@ApiPropertyOptional({ example: 1, description: 'Number of replicas (0 for managed services)' })
@IsOptional()
@IsNumber()
@Min(0)
replicas?: number;
@ApiProperty({ example: '1Gi', description: 'Database storage size' })
@IsOptional()
@IsString()
dbStorageSize?: string;
@ApiProperty({ example: '2Gi', description: 'App storage size for uploads/wp-content' })
@IsOptional()
@IsString()
appStorageSize?: string;
@ApiPropertyOptional({ example: false, description: 'Enable Redis caching' })
@IsOptional()
@IsBoolean()
enableRedis?: boolean;
@ApiPropertyOptional({ example: false, description: 'Enable RabbitMQ message broker' })
@IsOptional()
@IsBoolean()
enableRabbitmq?: boolean;
@ApiPropertyOptional({ example: false, description: 'Enable Elasticsearch logging' })
@IsOptional()
@IsBoolean()
enableElasticsearch?: boolean;
@ApiPropertyOptional({ type: OptionalServiceResourcesDto, description: 'Per-database CPU/RAM/storage limits' })
@IsOptional()
@ValidateNested()
@Type(() => OptionalServiceResourcesDto)
databaseResources?: OptionalServiceResourcesDto;
@ApiPropertyOptional({ type: OptionalServiceResourcesDto })
@IsOptional()
@ValidateNested()
@Type(() => OptionalServiceResourcesDto)
redisResources?: OptionalServiceResourcesDto;
@ApiPropertyOptional({ type: OptionalServiceResourcesDto })
@IsOptional()
@ValidateNested()
@Type(() => OptionalServiceResourcesDto)
rabbitmqResources?: OptionalServiceResourcesDto;
@ApiPropertyOptional({ example: false, description: 'Enable custom domain with SSL' })
@IsOptional()
@IsBoolean()
enableCustomDomain?: boolean;
}
export class CalculateDeployCostDto extends CalculateCostDto {
@ApiProperty({ enum: BillingCycle, example: 'monthly' })
@IsEnum(BillingCycle)
cycle: BillingCycle;
@ApiPropertyOptional({ example: 'NOWRUZ1403', description: 'Coupon discount code' })
@IsOptional()
@IsString()
couponCode?: string;
}
export class PayApplicationDto {
@ApiProperty({ enum: BillingCycle, example: 'monthly' })
@IsEnum(BillingCycle)
cycle: BillingCycle;
@ApiPropertyOptional({ example: 'NOWRUZ1403', description: 'Coupon discount code' })
@IsOptional()
@IsString()
couponCode?: string;
}
// ─── Renewal & Upgrade DTOs ─────────────────────────────────────────
export class RenewApplicationDto {
@ApiProperty({ enum: BillingCycle, example: 'monthly' })
@IsEnum(BillingCycle)
cycle: BillingCycle;
@ApiPropertyOptional({ example: 'NOWRUZ1403', description: 'Coupon discount code' })
@IsOptional()
@IsString()
couponCode?: string;
}
export class UpgradeResourcesDto {
@ApiPropertyOptional({ example: '500m', description: 'New CPU request' })
@IsOptional()
@IsString()
cpuRequest?: string;
@ApiPropertyOptional({ example: '1000m', description: 'New CPU limit' })
@IsOptional()
@IsString()
cpuLimit?: string;
@ApiPropertyOptional({ example: '256Mi', description: 'New memory request' })
@IsOptional()
@IsString()
memoryRequest?: string;
@ApiPropertyOptional({ example: '1Gi', description: 'New memory limit' })
@IsOptional()
@IsString()
memoryLimit?: string;
@ApiPropertyOptional({ example: 2, description: 'New replica count' })
@IsOptional()
@IsNumber()
@Min(1)
replicas?: number;
@ApiPropertyOptional({ example: '5Gi', description: 'New database storage size' })
@IsOptional()
@IsString()
dbStorageSize?: string;
@ApiPropertyOptional({ example: '5Gi', description: 'New app storage size for uploads/wp-content' })
@IsOptional()
@IsString()
appStorageSize?: string;
@ApiPropertyOptional({ type: OptionalServiceResourcesDto, description: 'Database resource limits' })
@IsOptional()
@ValidateNested()
@Type(() => OptionalServiceResourcesDto)
databaseResources?: OptionalServiceResourcesDto;
@ApiPropertyOptional({ type: OptionalServiceResourcesDto, description: 'Managed Redis resource limits' })
@IsOptional()
@ValidateNested()
@Type(() => OptionalServiceResourcesDto)
redisResources?: OptionalServiceResourcesDto;
@ApiPropertyOptional({ type: OptionalServiceResourcesDto, description: 'Managed RabbitMQ resource limits' })
@IsOptional()
@ValidateNested()
@Type(() => OptionalServiceResourcesDto)
rabbitmqResources?: OptionalServiceResourcesDto;
@ApiPropertyOptional({ example: 'NOWRUZ1403', description: 'Coupon discount code' })
@IsOptional()
@IsString()
couponCode?: string;
}
export class CalculateUpgradeCostDto extends UpgradeResourcesDto {}
// ─── Invoice DTOs ───────────────────────────────────────────────────
export class InitiateInvoicePaymentDto {
@ApiProperty({ example: 'https://app.example.com/dashboard/invoices' })
@IsString()
callbackUrl: string;
}
export class VerifyInvoiceGatewayDto {
@ApiProperty({ example: 'INV-1710000000000-ABC123' })
@IsString()
trackingCode: string;
@ApiProperty({ example: 50000 })
@IsNumber()
@Min(1)
amount: number;
}
export class UpdateInvoiceStatusDto {
@ApiProperty({ enum: InvoiceStatus })
@IsEnum(InvoiceStatus)
status: InvoiceStatus;
@ApiProperty({ example: 'Manual reconciliation by finance team' })
@IsString()
reason: string;
}
// ─── Platform optional services pricing (Admin) ─────────────────────
export class OptionalServiceCyclePricesDto {
@ApiProperty({ example: 500 })
@IsNumber()
@Min(0)
hourly: number;
@ApiProperty({ example: 50000 })
@IsNumber()
@Min(0)
monthly: number;
@ApiProperty({ example: 500000 })
@IsNumber()
@Min(0)
yearly: number;
}
export class SetOptionalServicesPricingDto {
@ApiProperty({ type: OptionalServiceCyclePricesDto })
@ValidateNested()
@Type(() => OptionalServiceCyclePricesDto)
redis: OptionalServiceCyclePricesDto;
@ApiProperty({ type: OptionalServiceCyclePricesDto })
@ValidateNested()
@Type(() => OptionalServiceCyclePricesDto)
rabbitmq: OptionalServiceCyclePricesDto;
@ApiProperty({ type: OptionalServiceCyclePricesDto })
@ValidateNested()
@Type(() => OptionalServiceCyclePricesDto)
elasticsearch: OptionalServiceCyclePricesDto;
}