feat: add custom domain support with SSL, DNS verification, and billing

Users can assign a custom domain to their app with automatic SSL via
cert-manager. Includes DNS verification flow (CNAME check), Persian
instructions, admin-configurable pricing via PlatformSetting, and
integration into the deploy wizard cost calculation.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
keyhan
2026-05-14 00:36:29 +03:30
parent d87b50c6a4
commit 435cf92817
18 changed files with 1082 additions and 89 deletions
+18
View File
@@ -95,6 +95,24 @@ export class BillingController {
return this.billingService.calculateCost(dto);
}
// ─── Custom Domain Pricing ─────────────────────────────────────
@Get('settings/custom-domain-price')
@ApiOperation({ summary: 'Get custom domain monthly price' })
async getCustomDomainPrice() {
return this.billingService.getCustomDomainPrice();
}
@Patch('settings/custom-domain-price')
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Set custom domain monthly price (Admin)' })
async setCustomDomainPrice(@Body() body: { monthlyPrice: number }) {
if (body.monthlyPrice === undefined || body.monthlyPrice < 0) {
throw new BadRequestException('monthlyPrice must be a non-negative number');
}
return this.billingService.setCustomDomainPrice(body.monthlyPrice);
}
// ─── Wallet (User) ───────────────────────────────────────────────
@Get('wallet')
+34
View File
@@ -5,6 +5,7 @@ import { ServicePlan } from './entities/service-plan.entity';
import { PricingRule } from './entities/pricing-rule.entity';
import { Wallet } from './entities/wallet.entity';
import { WalletTransaction } from './entities/wallet-transaction.entity';
import { PlatformSetting } from './entities/platform-setting.entity';
import { TransactionType, BillingCycle, PricingResourceType } from '../common/enums';
import {
CreateServicePlanDto,
@@ -23,6 +24,7 @@ export class BillingService {
@InjectRepository(PricingRule) private ruleRepo: Repository<PricingRule>,
@InjectRepository(Wallet) private walletRepo: Repository<Wallet>,
@InjectRepository(WalletTransaction) private txRepo: Repository<WalletTransaction>,
@InjectRepository(PlatformSetting) private settingsRepo: Repository<PlatformSetting>,
) {}
// ─── Service Plans ────────────────────────────────────────────────
@@ -143,6 +145,7 @@ export class BillingService {
const hasRedis = dto.enableRedis || false;
const hasRabbitmq = dto.enableRabbitmq || false;
const hasElasticsearch = dto.enableElasticsearch || false;
const hasCustomDomain = dto.enableCustomDomain || false;
const breakdown: { label: string; hourly: number; monthly: number; yearly: number }[] = [];
let totalBase = 0;
@@ -184,6 +187,10 @@ export class BillingService {
cost = hasElasticsearch ? Number(rule.unitPrice) : 0;
label = 'Elasticsearch addon';
break;
case PricingResourceType.CUSTOM_DOMAIN_ADDON:
cost = hasCustomDomain ? Number(rule.unitPrice) : 0;
label = 'Custom domain + SSL';
break;
}
if (cost > 0) {
@@ -222,6 +229,33 @@ export class BillingService {
return parseFloat(memory) / (1024 * 1024 * 1024);
}
// ─── Custom Domain Pricing (PlatformSetting) ────────────────────
async getCustomDomainPrice(): Promise<{ monthlyPrice: number }> {
const setting = await this.settingsRepo.findOne({
where: { key: 'custom_domain_monthly_price_toman' },
});
return { monthlyPrice: setting ? Number(setting.value) : 0 };
}
async setCustomDomainPrice(monthlyPrice: number): Promise<{ monthlyPrice: number }> {
let setting = await this.settingsRepo.findOne({
where: { key: 'custom_domain_monthly_price_toman' },
});
if (setting) {
setting.value = String(monthlyPrice);
} else {
setting = this.settingsRepo.create({
key: 'custom_domain_monthly_price_toman',
value: String(monthlyPrice),
description: 'Monthly price for custom domain addon (Toman)',
});
}
await this.settingsRepo.save(setting);
this.logger.log(`Custom domain monthly price updated: ${monthlyPrice} Toman`);
return { monthlyPrice };
}
// ─── Wallet ───────────────────────────────────────────────────────
async getOrCreateWallet(userId: string): Promise<Wallet> {
+5
View File
@@ -136,6 +136,11 @@ export class CalculateCostDto {
@IsOptional()
@IsBoolean()
enableElasticsearch?: boolean;
@ApiPropertyOptional({ example: false, description: 'Enable custom domain with SSL' })
@IsOptional()
@IsBoolean()
enableCustomDomain?: boolean;
}
// ─── Renewal & Upgrade DTOs ─────────────────────────────────────────