Replace billing plans with per-runtime usage pricing catalog.
Store explicit hourly/monthly/yearly rates in pricing_rates and addon_rates, compute deploy costs without cycle conversion, and simplify admin UI and wallet payment to cycle-only. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -21,15 +21,15 @@ import { AppLifecycleService } from '../lifecycle/app-lifecycle.service';
|
||||
import { ApplicationsService } from '../applications/applications.service';
|
||||
import { KubernetesService } from '../kubernetes/kubernetes.service';
|
||||
import {
|
||||
CreateServicePlanDto,
|
||||
UpdateServicePlanDto,
|
||||
ChargeWalletDto,
|
||||
CalculateCostDto,
|
||||
CalculateDeployCostDto,
|
||||
SetOptionalServicesPricingDto,
|
||||
RenewApplicationDto,
|
||||
UpgradeResourcesDto,
|
||||
CalculateUpgradeCostDto,
|
||||
} from './dto/billing.dto';
|
||||
import { UpdatePricingCatalogDto } from './dto/pricing-catalog.dto';
|
||||
import { RolesGuard } from '../common/guards/roles.guard';
|
||||
import { Roles } from '../common/decorators/roles.decorator';
|
||||
import { UserRole, BillingCycle, AppLifecycleStatus } from '../common/enums';
|
||||
@@ -49,43 +49,20 @@ export class BillingController {
|
||||
private readonly kubernetesService: KubernetesService,
|
||||
) {}
|
||||
|
||||
// ─── Service Plans (Admin) ────────────────────────────────────────
|
||||
// ─── Pricing catalog (Admin) ──────────────────────────────────────
|
||||
|
||||
@Post('plans')
|
||||
@Get('pricing-catalog')
|
||||
@Roles(UserRole.ADMIN)
|
||||
@ApiOperation({ summary: 'Create a new service plan (Admin)' })
|
||||
async createPlan(@Body() dto: CreateServicePlanDto) {
|
||||
return this.billingService.createPlan(dto);
|
||||
@ApiOperation({ summary: 'Get usage-based pricing catalog (Admin)' })
|
||||
async getPricingCatalog() {
|
||||
return this.billingService.getPricingCatalog();
|
||||
}
|
||||
|
||||
@Get('plans')
|
||||
@ApiOperation({ summary: 'List all service plans' })
|
||||
async findAllPlans(@Request() req: any) {
|
||||
if (req.user.role === UserRole.ADMIN) {
|
||||
return this.billingService.findAllPlans();
|
||||
}
|
||||
return this.billingService.findActivePlans();
|
||||
}
|
||||
|
||||
@Get('plans/:id')
|
||||
@ApiOperation({ summary: 'Get service plan details' })
|
||||
async findPlan(@Param('id') id: string) {
|
||||
return this.billingService.findPlan(id);
|
||||
}
|
||||
|
||||
@Patch('plans/:id')
|
||||
@Patch('pricing-catalog')
|
||||
@Roles(UserRole.ADMIN)
|
||||
@ApiOperation({ summary: 'Update a service plan (Admin)' })
|
||||
async updatePlan(@Param('id') id: string, @Body() dto: UpdateServicePlanDto) {
|
||||
return this.billingService.updatePlan(id, dto);
|
||||
}
|
||||
|
||||
@Delete('plans/:id')
|
||||
@Roles(UserRole.ADMIN)
|
||||
@ApiOperation({ summary: 'Delete a service plan (Admin)' })
|
||||
async deletePlan(@Param('id') id: string) {
|
||||
await this.billingService.deletePlan(id);
|
||||
return { message: 'Plan deleted' };
|
||||
@ApiOperation({ summary: 'Update usage-based pricing catalog (Admin)' })
|
||||
async updatePricingCatalog(@Body() dto: UpdatePricingCatalogDto) {
|
||||
return this.billingService.updatePricingCatalog(dto);
|
||||
}
|
||||
|
||||
// ─── Cost Calculation ─────────────────────────────────────────────
|
||||
@@ -125,6 +102,19 @@ export class BillingController {
|
||||
return this.billingService.setCustomDomainPrice(body.monthlyPrice);
|
||||
}
|
||||
|
||||
@Get('settings/optional-services')
|
||||
@ApiOperation({ summary: 'Get global optional service addon prices (Redis, RabbitMQ, Elasticsearch)' })
|
||||
async getOptionalServicesPricing() {
|
||||
return this.billingService.getOptionalServicesPricing();
|
||||
}
|
||||
|
||||
@Patch('settings/optional-services')
|
||||
@Roles(UserRole.ADMIN)
|
||||
@ApiOperation({ summary: 'Set global optional service addon prices (Admin)' })
|
||||
async setOptionalServicesPricing(@Body() dto: SetOptionalServicesPricingDto) {
|
||||
return this.billingService.setOptionalServicesPricing(dto);
|
||||
}
|
||||
|
||||
// ─── Wallet (User) ───────────────────────────────────────────────
|
||||
|
||||
@Get('wallet')
|
||||
@@ -157,7 +147,7 @@ export class BillingController {
|
||||
async payForApplication(
|
||||
@Request() req: any,
|
||||
@Param('applicationId') applicationId: string,
|
||||
@Body() body: { planId?: string; cycle: string; amount?: number },
|
||||
@Body() body: { cycle: string },
|
||||
) {
|
||||
const cycle = body.cycle as BillingCycle;
|
||||
if (!Object.values(BillingCycle).includes(cycle)) {
|
||||
@@ -166,32 +156,6 @@ export class BillingController {
|
||||
|
||||
const app = await this.applicationsService.findOne(applicationId, req.user.id);
|
||||
|
||||
let amount = body.amount;
|
||||
let planId = body.planId || app.planId || '';
|
||||
|
||||
if (amount === undefined || amount === null) {
|
||||
if (!body.planId) {
|
||||
throw new BadRequestException('planId or amount is required');
|
||||
}
|
||||
const plan = await this.billingService.findPlan(body.planId);
|
||||
const costs = await this.billingService.calculateCostForApp({
|
||||
runtime: app.runtime,
|
||||
databaseType: app.databaseType,
|
||||
cpuLimit: app.cpuLimit,
|
||||
memoryLimit: app.memoryLimit,
|
||||
replicas: app.replicas,
|
||||
dbStorageSize: app.dbStorageSize,
|
||||
appStorageSize: app.appStorageSize,
|
||||
enableRedis: app.enableRedis,
|
||||
enableRabbitmq: app.enableRabbitmq,
|
||||
enableElasticsearch: app.enableElasticsearch,
|
||||
});
|
||||
amount = cycle === BillingCycle.HOURLY ? costs.hourly
|
||||
: cycle === BillingCycle.MONTHLY ? costs.monthly
|
||||
: costs.yearly;
|
||||
planId = body.planId;
|
||||
}
|
||||
|
||||
const payment = await this.billingService.resolveAppPayment(
|
||||
req.user.id,
|
||||
app,
|
||||
@@ -211,7 +175,6 @@ export class BillingController {
|
||||
const activated = await this.lifecycleService.activateApp(
|
||||
applicationId,
|
||||
cycle,
|
||||
planId,
|
||||
payment.planExpiresAt,
|
||||
);
|
||||
|
||||
@@ -348,7 +311,7 @@ export class BillingController {
|
||||
);
|
||||
|
||||
// Activate the application
|
||||
const renewedApp = await this.lifecycleService.activateApp(app.id, dto.cycle, app.planId || '');
|
||||
const renewedApp = await this.lifecycleService.activateApp(app.id, dto.cycle);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -381,7 +344,7 @@ export class BillingController {
|
||||
|
||||
if (body.bypassPayment) {
|
||||
// Direct activation without payment (for special cases, support, etc.)
|
||||
const renewedApp = await this.lifecycleService.activateApp(app.id, cycle, app.planId || '');
|
||||
const renewedApp = await this.lifecycleService.activateApp(app.id, cycle);
|
||||
return {
|
||||
success: true,
|
||||
bypassedPayment: true,
|
||||
@@ -409,7 +372,7 @@ export class BillingController {
|
||||
app.id,
|
||||
);
|
||||
|
||||
const renewedApp = await this.lifecycleService.activateApp(app.id, cycle, app.planId || '');
|
||||
const renewedApp = await this.lifecycleService.activateApp(app.id, cycle);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
||||
Reference in New Issue
Block a user