49726f1dfd
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>
334 lines
9.1 KiB
TypeScript
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;
|
|
}
|
|
|