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:
@@ -9,10 +9,14 @@ import {
|
||||
Query,
|
||||
UseGuards,
|
||||
Request,
|
||||
BadRequestException,
|
||||
Inject,
|
||||
forwardRef,
|
||||
} from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { BillingService } from './billing.service';
|
||||
import { AppLifecycleService } from '../lifecycle/app-lifecycle.service';
|
||||
import {
|
||||
CreateServicePlanDto,
|
||||
UpdateServicePlanDto,
|
||||
@@ -21,14 +25,18 @@ import {
|
||||
} from './dto/billing.dto';
|
||||
import { RolesGuard } from '../common/guards/roles.guard';
|
||||
import { Roles } from '../common/decorators/roles.decorator';
|
||||
import { UserRole } from '../common/enums';
|
||||
import { UserRole, BillingCycle, AppLifecycleStatus } from '../common/enums';
|
||||
|
||||
@ApiTags('Billing')
|
||||
@ApiBearerAuth()
|
||||
@Controller('billing')
|
||||
@UseGuards(AuthGuard('jwt'), RolesGuard)
|
||||
export class BillingController {
|
||||
constructor(private readonly billingService: BillingService) {}
|
||||
constructor(
|
||||
private readonly billingService: BillingService,
|
||||
@Inject(forwardRef(() => AppLifecycleService))
|
||||
private readonly lifecycleService: AppLifecycleService,
|
||||
) {}
|
||||
|
||||
// ─── Service Plans (Admin) ────────────────────────────────────────
|
||||
|
||||
@@ -98,18 +106,49 @@ export class BillingController {
|
||||
}
|
||||
|
||||
@Post('wallet/pay/:applicationId')
|
||||
@ApiOperation({ summary: 'Pay for an application from wallet' })
|
||||
@ApiOperation({ summary: 'Pay for an application plan from wallet — activates/renews the app' })
|
||||
async payForApplication(
|
||||
@Request() req: any,
|
||||
@Param('applicationId') applicationId: string,
|
||||
@Body() body: { amount: number; cycle: string },
|
||||
@Body() body: { planId: string; cycle: string },
|
||||
) {
|
||||
return this.billingService.deductWallet(
|
||||
const cycle = body.cycle as BillingCycle;
|
||||
if (!Object.values(BillingCycle).includes(cycle)) {
|
||||
throw new BadRequestException(`Invalid billing cycle: ${body.cycle}`);
|
||||
}
|
||||
|
||||
// Calculate cost
|
||||
const plan = await this.billingService.findPlan(body.planId);
|
||||
const costs = await this.billingService.calculateCost({
|
||||
runtime: plan.runtime,
|
||||
databaseType: 'none', // Will be refined per-app later
|
||||
cpuLimit: '500m',
|
||||
memoryLimit: '512Mi',
|
||||
replicas: 1,
|
||||
});
|
||||
|
||||
const amount = cycle === BillingCycle.HOURLY ? costs.hourly
|
||||
: cycle === BillingCycle.MONTHLY ? costs.monthly
|
||||
: costs.yearly;
|
||||
|
||||
// Deduct from wallet
|
||||
const tx = await this.billingService.deductWallet(
|
||||
req.user.id,
|
||||
body.amount,
|
||||
`Payment for application (${body.cycle})`,
|
||||
amount,
|
||||
`Payment for app ${applicationId} (${cycle}) — plan: ${plan.name}`,
|
||||
applicationId,
|
||||
);
|
||||
|
||||
// Activate/reactivate the application
|
||||
const app = await this.lifecycleService.activateApp(applicationId, cycle, body.planId);
|
||||
|
||||
return {
|
||||
transaction: tx,
|
||||
application: { id: app.id, name: app.name, lifecycleStatus: app.lifecycleStatus, planExpiresAt: app.planExpiresAt },
|
||||
message: app.lifecycleStatus === AppLifecycleStatus.ACTIVE
|
||||
? `Application "${app.name}" activated until ${app.planExpiresAt?.toISOString()}`
|
||||
: `Payment processed — application status: ${app.lifecycleStatus}`,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Payment Gateway ─────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user