feat(lifecycle): add billing lifecycle with auto-suspend/delete

- New AppLifecycleService with cron-based scanner (60s interval)
- State machine: ACTIVE → SUSPENDED → PENDING_DELETION → DELETED
- Auto-renew hourly plans from wallet
- Admin-configurable grace periods via PlatformSettings table
- New LifecycleController (GET/PATCH /lifecycle/settings)
- PlatformSetting entity for runtime admin config
- BillingService: calculateCostForApp, deductWallet
- Application entity: planId, billingCycle, lifecycleStatus, planExpiresAt
- New enums: AppLifecycleStatus, BillingCycle
This commit is contained in:
keyhan
2026-04-22 16:44:20 +03:30
parent 0e408d1baa
commit 8ca787d46e
11 changed files with 562 additions and 11 deletions
@@ -8,7 +8,7 @@ import {
OneToMany,
JoinColumn,
} from 'typeorm';
import { AppRuntime, DatabaseType } from '../../common/enums';
import { AppRuntime, DatabaseType, BillingCycle, AppLifecycleStatus } from '../../common/enums';
import { User } from '../../users/entities/user.entity';
import { Deployment } from '../../deployments/entities/deployment.entity';
@@ -59,6 +59,9 @@ export class Application {
@Column({ nullable: true })
codePath: string; // Path to uploaded zip
@Column({ nullable: true })
dbDumpPath: string; // Path to uploaded SQL dump file
@Column({ type: 'jsonb', nullable: true })
envVars: Record<string, string>;
@@ -105,6 +108,25 @@ export class Application {
@Column({ nullable: true })
subdomain: string; // <subdomain>.apps.cloudhost.local
// ── Billing & Lifecycle ─────────────────────────────
@Column({ nullable: true })
planId: string; // FK to ServicePlan
@Column({ type: 'enum', enum: BillingCycle, nullable: true })
billingCycle: BillingCycle; // The cycle this app was purchased under
@Column({ type: 'enum', enum: AppLifecycleStatus, default: AppLifecycleStatus.ACTIVE })
lifecycleStatus: AppLifecycleStatus;
@Column({ type: 'timestamptz', nullable: true })
planExpiresAt: Date; // When the current billing period ends
@Column({ type: 'timestamptz', nullable: true })
suspendedAt: Date; // When the app was suspended (pods scaled to 0)
@Column({ type: 'timestamptz', nullable: true })
scheduledDeletionAt: Date; // When the app will be permanently deleted
@CreateDateColumn()
createdAt: Date;