feat: billing v2 — runtime-based plans, English UI, payment flow

- ServicePlan now has 'runtime' field (nodejs/laravel/wordpress)
- Admin billing page: Application Type dropdown, English UI, Toman prices
- calculateCost filters active plans by matching runtime
- Wallet page: English UI, payment gateway integration (Pay Now button)
- Deploy page Review step: billing cycle selector (hourly/monthly/yearly),
  payment method choice (wallet or payment gateway), Pay & Deploy button
- Payment gateway endpoints: POST /billing/gateway/initiate + /verify
  (simulated — ready for Zarinpal/IDPay integration)
- Deploy requires payment: wallet deduction or gateway charge before deploy
This commit is contained in:
keyhan
2026-04-07 02:05:58 +03:30
parent 4974f88e8c
commit c2c6a32ae8
9 changed files with 441 additions and 138 deletions
+9 -2
View File
@@ -28,6 +28,7 @@ export class BillingService {
async createPlan(dto: CreateServicePlanDto): Promise<ServicePlan> {
const plan = this.planRepo.create({
name: dto.name,
runtime: dto.runtime,
description: dto.description,
billingCycle: dto.billingCycle,
});
@@ -47,6 +48,7 @@ export class BillingService {
if (!plan) throw new NotFoundException('Plan not found');
if (dto.name !== undefined) plan.name = dto.name;
if (dto.runtime !== undefined) plan.runtime = dto.runtime;
if (dto.description !== undefined) plan.description = dto.description;
if (dto.billingCycle !== undefined) plan.billingCycle = dto.billingCycle;
if (dto.isActive !== undefined) plan.isActive = dto.isActive;
@@ -101,7 +103,12 @@ export class BillingService {
yearly: number;
breakdown: { label: string; hourly: number; monthly: number; yearly: number }[];
}> {
const plans = await this.findActivePlans();
// Only use active plans that match the requested runtime
const plans = await this.planRepo.find({
where: { isActive: true, runtime: dto.runtime as any },
relations: ['pricingRules'],
});
if (plans.length === 0) {
return { hourly: 0, monthly: 0, yearly: 0, breakdown: [] };
}
@@ -140,7 +147,7 @@ export class BillingService {
switch (rule.resourceType) {
case PricingResourceType.BASE_FEE:
cost = Number(rule.unitPrice);
label = 'هزینه پایه';
label = 'Base fee';
break;
case PricingResourceType.CPU_PER_CORE:
cost = cpuCores * replicas * Number(rule.unitPrice);