Files
cloud-host/backend/src/billing/dto/discount.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

192 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
IsArray,
IsBoolean,
IsEnum,
IsInt,
IsOptional,
IsString,
Max,
Min,
ValidateNested,
} from 'class-validator';
import { Type } from 'class-transformer';
import { BillingCycle } from '../../common/enums';
import { CalculateCostDto, UpgradeResourcesDto } from './billing.dto';
export class CreateDiscountDto {
@ApiProperty({ example: 'NOWRUZ1403' })
@IsString()
code: string;
@ApiProperty({ example: 'تخفیف نوروزی' })
@IsString()
name: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
description?: string;
@ApiProperty({ example: 20, description: 'Percentage off (1100)' })
@IsInt()
@Min(1)
@Max(100)
percentOff: number;
@ApiPropertyOptional({
type: [String],
description: 'Service keys to target. Empty or ["*"] = all services.',
example: ['optional:redis', 'runtime:nodejs'],
})
@IsOptional()
@IsArray()
@IsString({ each: true })
services?: string[];
@ApiPropertyOptional({ default: true })
@IsOptional()
@IsBoolean()
isPublic?: boolean;
@ApiPropertyOptional({ type: [String] })
@IsOptional()
@IsArray()
@IsString({ each: true })
allowedUserIds?: string[];
@ApiPropertyOptional({ description: 'Total redemption cap (omit for unlimited)' })
@IsOptional()
@IsInt()
@Min(1)
maxUses?: number;
@ApiPropertyOptional({ description: 'Per-user redemption cap (omit for unlimited)' })
@IsOptional()
@IsInt()
@Min(1)
maxUsesPerUser?: number;
@ApiPropertyOptional({ description: 'ISO start date' })
@IsOptional()
@IsString()
startsAt?: string;
@ApiPropertyOptional({ description: 'ISO end date' })
@IsOptional()
@IsString()
endsAt?: string;
@ApiPropertyOptional({ default: true })
@IsOptional()
@IsBoolean()
isActive?: boolean;
}
export class UpdateDiscountDto {
@ApiPropertyOptional()
@IsOptional()
@IsString()
code?: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
name?: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
description?: string;
@ApiPropertyOptional({ example: 20 })
@IsOptional()
@IsInt()
@Min(1)
@Max(100)
percentOff?: number;
@ApiPropertyOptional({ type: [String] })
@IsOptional()
@IsArray()
@IsString({ each: true })
services?: string[];
@ApiPropertyOptional()
@IsOptional()
@IsBoolean()
isPublic?: boolean;
@ApiPropertyOptional({ type: [String] })
@IsOptional()
@IsArray()
@IsString({ each: true })
allowedUserIds?: string[];
@ApiPropertyOptional()
@IsOptional()
@IsInt()
@Min(1)
maxUses?: number | null;
@ApiPropertyOptional()
@IsOptional()
@IsInt()
@Min(1)
maxUsesPerUser?: number | null;
@ApiPropertyOptional()
@IsOptional()
@IsString()
startsAt?: string | null;
@ApiPropertyOptional()
@IsOptional()
@IsString()
endsAt?: string | null;
@ApiPropertyOptional()
@IsOptional()
@IsBoolean()
isActive?: boolean;
}
export enum DiscountFlow {
DEPLOY = 'deploy',
RENEWAL = 'renewal',
UPGRADE = 'upgrade',
}
/** Validate a coupon against a concrete charge context to preview the discount. */
export class ValidateDiscountDto {
@ApiProperty({ example: 'NOWRUZ1403' })
@IsString()
code: string;
@ApiProperty({ enum: BillingCycle })
@IsEnum(BillingCycle)
cycle: BillingCycle;
@ApiPropertyOptional({ enum: DiscountFlow, default: DiscountFlow.DEPLOY })
@IsOptional()
@IsEnum(DiscountFlow)
flow?: DiscountFlow;
@ApiPropertyOptional({ type: CalculateCostDto, description: 'Deploy config (deploy flow)' })
@IsOptional()
@ValidateNested()
@Type(() => CalculateCostDto)
config?: CalculateCostDto;
@ApiPropertyOptional({ description: 'Application id (renewal/upgrade flows)' })
@IsOptional()
@IsString()
applicationId?: string;
@ApiPropertyOptional({ type: UpgradeResourcesDto, description: 'Target resources (upgrade flow)' })
@IsOptional()
@ValidateNested()
@Type(() => UpgradeResourcesDto)
upgrade?: UpgradeResourcesDto;
}