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
+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> {