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>
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
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 (1–100)' })
|
||||
@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;
|
||||
}
|
||||
Reference in New Issue
Block a user