add optinal apps
This commit is contained in:
@@ -12,16 +12,22 @@ import {
|
||||
BadRequestException,
|
||||
Inject,
|
||||
forwardRef,
|
||||
ForbiddenException,
|
||||
} 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 { ApplicationsService } from '../applications/applications.service';
|
||||
import { KubernetesService } from '../kubernetes/kubernetes.service';
|
||||
import {
|
||||
CreateServicePlanDto,
|
||||
UpdateServicePlanDto,
|
||||
ChargeWalletDto,
|
||||
CalculateCostDto,
|
||||
RenewApplicationDto,
|
||||
UpgradeResourcesDto,
|
||||
CalculateUpgradeCostDto,
|
||||
} from './dto/billing.dto';
|
||||
import { RolesGuard } from '../common/guards/roles.guard';
|
||||
import { Roles } from '../common/decorators/roles.decorator';
|
||||
@@ -36,6 +42,10 @@ export class BillingController {
|
||||
private readonly billingService: BillingService,
|
||||
@Inject(forwardRef(() => AppLifecycleService))
|
||||
private readonly lifecycleService: AppLifecycleService,
|
||||
@Inject(forwardRef(() => ApplicationsService))
|
||||
private readonly applicationsService: ApplicationsService,
|
||||
@Inject(forwardRef(() => KubernetesService))
|
||||
private readonly kubernetesService: KubernetesService,
|
||||
) {}
|
||||
|
||||
// ─── Service Plans (Admin) ────────────────────────────────────────
|
||||
@@ -208,4 +218,267 @@ export class BillingController {
|
||||
) {
|
||||
return this.billingService.adminChargeWallet(userId, dto.amount, dto.description);
|
||||
}
|
||||
|
||||
// ─── Application Renewal ──────────────────────────────────────────
|
||||
|
||||
@Get('applications/:applicationId/renewal-cost')
|
||||
@ApiOperation({ summary: 'Get renewal cost for an application' })
|
||||
async getRenewalCost(
|
||||
@Request() req: any,
|
||||
@Param('applicationId') applicationId: string,
|
||||
) {
|
||||
// User can only view their own app, admin/sales can view any
|
||||
const app = await this.getAppWithAccess(req.user, applicationId);
|
||||
const costs = await this.billingService.calculateRenewalCost(app);
|
||||
return {
|
||||
applicationId: app.id,
|
||||
applicationName: app.name,
|
||||
lifecycleStatus: app.lifecycleStatus,
|
||||
planExpiresAt: app.planExpiresAt,
|
||||
currentCycle: app.billingCycle,
|
||||
costs,
|
||||
};
|
||||
}
|
||||
|
||||
@Post('applications/:applicationId/renew')
|
||||
@ApiOperation({ summary: 'Renew an application (user pays from wallet)' })
|
||||
async renewApplication(
|
||||
@Request() req: any,
|
||||
@Param('applicationId') applicationId: string,
|
||||
@Body() dto: RenewApplicationDto,
|
||||
) {
|
||||
// User can only renew their own app, admin/sales can renew any
|
||||
const app = await this.getAppWithAccess(req.user, applicationId);
|
||||
|
||||
// Calculate cost for the selected cycle
|
||||
const costs = await this.billingService.calculateRenewalCost(app);
|
||||
const amount = dto.cycle === BillingCycle.HOURLY ? costs.hourly
|
||||
: dto.cycle === BillingCycle.MONTHLY ? costs.monthly
|
||||
: costs.yearly;
|
||||
|
||||
if (amount <= 0) {
|
||||
throw new BadRequestException('Invalid cost calculation — no pricing rules found');
|
||||
}
|
||||
|
||||
// Deduct from wallet (user's wallet for user, app owner's wallet for admin action)
|
||||
const walletUserId = req.user.role === UserRole.ADMIN || req.user.role === UserRole.SALES
|
||||
? app.userId
|
||||
: req.user.id;
|
||||
|
||||
const tx = await this.billingService.deductWallet(
|
||||
walletUserId,
|
||||
amount,
|
||||
`Renewal for ${app.name} (${dto.cycle})`,
|
||||
app.id,
|
||||
);
|
||||
|
||||
// Activate the application
|
||||
const renewedApp = await this.lifecycleService.activateApp(app.id, dto.cycle, app.planId || '');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
transaction: tx,
|
||||
application: {
|
||||
id: renewedApp.id,
|
||||
name: renewedApp.name,
|
||||
lifecycleStatus: renewedApp.lifecycleStatus,
|
||||
planExpiresAt: renewedApp.planExpiresAt,
|
||||
billingCycle: renewedApp.billingCycle,
|
||||
},
|
||||
message: `Application "${renewedApp.name}" renewed until ${renewedApp.planExpiresAt?.toISOString()}`,
|
||||
};
|
||||
}
|
||||
|
||||
@Post('admin/applications/:applicationId/renew')
|
||||
@Roles(UserRole.ADMIN, UserRole.SALES)
|
||||
@ApiOperation({ summary: 'Admin/Sales: Renew an application (can bypass wallet if needed)' })
|
||||
async adminRenewApplication(
|
||||
@Request() req: any,
|
||||
@Param('applicationId') applicationId: string,
|
||||
@Body() body: { cycle: string; bypassPayment?: boolean; reason?: string },
|
||||
) {
|
||||
const app = await this.applicationsService.findOne(applicationId);
|
||||
const cycle = body.cycle as BillingCycle;
|
||||
|
||||
if (!Object.values(BillingCycle).includes(cycle)) {
|
||||
throw new BadRequestException(`Invalid billing cycle: ${body.cycle}`);
|
||||
}
|
||||
|
||||
if (body.bypassPayment) {
|
||||
// Direct activation without payment (for special cases, support, etc.)
|
||||
const renewedApp = await this.lifecycleService.activateApp(app.id, cycle, app.planId || '');
|
||||
return {
|
||||
success: true,
|
||||
bypassedPayment: true,
|
||||
reason: body.reason || 'Admin action',
|
||||
application: {
|
||||
id: renewedApp.id,
|
||||
name: renewedApp.name,
|
||||
lifecycleStatus: renewedApp.lifecycleStatus,
|
||||
planExpiresAt: renewedApp.planExpiresAt,
|
||||
},
|
||||
message: `Application "${renewedApp.name}" renewed by admin (payment bypassed)`,
|
||||
};
|
||||
}
|
||||
|
||||
// Normal renewal - deduct from app owner's wallet
|
||||
const costs = await this.billingService.calculateRenewalCost(app);
|
||||
const amount = cycle === BillingCycle.HOURLY ? costs.hourly
|
||||
: cycle === BillingCycle.MONTHLY ? costs.monthly
|
||||
: costs.yearly;
|
||||
|
||||
const tx = await this.billingService.deductWallet(
|
||||
app.userId,
|
||||
amount,
|
||||
`Renewal by ${req.user.role} for ${app.name} (${cycle})`,
|
||||
app.id,
|
||||
);
|
||||
|
||||
const renewedApp = await this.lifecycleService.activateApp(app.id, cycle, app.planId || '');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
transaction: tx,
|
||||
application: {
|
||||
id: renewedApp.id,
|
||||
name: renewedApp.name,
|
||||
lifecycleStatus: renewedApp.lifecycleStatus,
|
||||
planExpiresAt: renewedApp.planExpiresAt,
|
||||
},
|
||||
message: `Application "${renewedApp.name}" renewed by ${req.user.role}`,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Resource Upgrade ─────────────────────────────────────────────
|
||||
|
||||
@Post('applications/:applicationId/upgrade/calculate')
|
||||
@ApiOperation({ summary: 'Calculate cost for resource upgrade' })
|
||||
async calculateUpgradeCost(
|
||||
@Request() req: any,
|
||||
@Param('applicationId') applicationId: string,
|
||||
@Body() dto: CalculateUpgradeCostDto,
|
||||
) {
|
||||
const app = await this.getAppWithAccess(req.user, applicationId);
|
||||
const result = await this.billingService.calculateUpgradeCost(app, dto);
|
||||
|
||||
return {
|
||||
applicationId: app.id,
|
||||
applicationName: app.name,
|
||||
...result,
|
||||
currentResources: {
|
||||
cpuRequest: app.cpuRequest,
|
||||
cpuLimit: app.cpuLimit,
|
||||
memoryRequest: app.memoryRequest,
|
||||
memoryLimit: app.memoryLimit,
|
||||
replicas: app.replicas,
|
||||
dbStorageSize: app.dbStorageSize,
|
||||
appStorageSize: app.appStorageSize,
|
||||
},
|
||||
newResources: {
|
||||
cpuRequest: dto.cpuRequest || app.cpuRequest,
|
||||
cpuLimit: dto.cpuLimit || app.cpuLimit,
|
||||
memoryRequest: dto.memoryRequest || app.memoryRequest,
|
||||
memoryLimit: dto.memoryLimit || app.memoryLimit,
|
||||
replicas: dto.replicas ?? app.replicas,
|
||||
dbStorageSize: dto.dbStorageSize || app.dbStorageSize,
|
||||
appStorageSize: dto.appStorageSize || app.appStorageSize,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Post('applications/:applicationId/upgrade')
|
||||
@ApiOperation({ summary: 'Upgrade application resources (with payment)' })
|
||||
async upgradeResources(
|
||||
@Request() req: any,
|
||||
@Param('applicationId') applicationId: string,
|
||||
@Body() dto: UpgradeResourcesDto,
|
||||
) {
|
||||
const app = await this.getAppWithAccess(req.user, applicationId);
|
||||
|
||||
// Application must be active to upgrade
|
||||
if (app.lifecycleStatus !== AppLifecycleStatus.ACTIVE) {
|
||||
throw new BadRequestException(
|
||||
`Cannot upgrade resources for ${app.lifecycleStatus} application. Please renew first.`
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate upgrade cost
|
||||
const costResult = await this.billingService.calculateUpgradeCost(app, dto);
|
||||
|
||||
// If upgrading (positive difference), require payment
|
||||
if (costResult.proratedAmount > 0) {
|
||||
const walletUserId = req.user.role === UserRole.ADMIN || req.user.role === UserRole.SALES
|
||||
? app.userId
|
||||
: req.user.id;
|
||||
|
||||
await this.billingService.deductWallet(
|
||||
walletUserId,
|
||||
costResult.proratedAmount,
|
||||
`Resource upgrade for ${app.name}: prorated ${costResult.remainingHours}h`,
|
||||
app.id,
|
||||
);
|
||||
}
|
||||
|
||||
// Apply the resource changes
|
||||
const updatedApp = await this.applicationsService.update(app.id, app.userId, {
|
||||
cpuRequest: dto.cpuRequest || app.cpuRequest,
|
||||
cpuLimit: dto.cpuLimit || app.cpuLimit,
|
||||
memoryRequest: dto.memoryRequest || app.memoryRequest,
|
||||
memoryLimit: dto.memoryLimit || app.memoryLimit,
|
||||
replicas: dto.replicas ?? app.replicas,
|
||||
dbStorageSize: dto.dbStorageSize || app.dbStorageSize,
|
||||
appStorageSize: dto.appStorageSize || app.appStorageSize,
|
||||
});
|
||||
|
||||
// Update Kubernetes resources
|
||||
try {
|
||||
await this.kubernetesService.updateResources(updatedApp, {
|
||||
cpuRequest: dto.cpuRequest,
|
||||
cpuLimit: dto.cpuLimit,
|
||||
memoryRequest: dto.memoryRequest,
|
||||
memoryLimit: dto.memoryLimit,
|
||||
replicas: dto.replicas,
|
||||
});
|
||||
|
||||
// Resize app storage PVC if changed
|
||||
if (dto.appStorageSize && dto.appStorageSize !== app.appStorageSize) {
|
||||
await this.kubernetesService.resizeAppStoragePvc(updatedApp, dto.appStorageSize);
|
||||
}
|
||||
} catch (e: any) {
|
||||
// Log error but don't fail - DB is updated, K8s will sync on next deploy
|
||||
console.warn(`K8s resource update failed for ${app.name}: ${e.message}`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
paidAmount: costResult.proratedAmount,
|
||||
application: {
|
||||
id: updatedApp.id,
|
||||
name: updatedApp.name,
|
||||
cpuRequest: updatedApp.cpuRequest,
|
||||
cpuLimit: updatedApp.cpuLimit,
|
||||
memoryRequest: updatedApp.memoryRequest,
|
||||
memoryLimit: updatedApp.memoryLimit,
|
||||
replicas: updatedApp.replicas,
|
||||
dbStorageSize: updatedApp.dbStorageSize,
|
||||
appStorageSize: updatedApp.appStorageSize,
|
||||
},
|
||||
message: costResult.proratedAmount > 0
|
||||
? `Resources upgraded. Paid ${costResult.proratedAmount} Toman for remaining ${costResult.remainingHours} hours.`
|
||||
: 'Resources updated (downgrade or no cost change).',
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Helper Methods ───────────────────────────────────────────────
|
||||
|
||||
private async getAppWithAccess(user: any, applicationId: string) {
|
||||
const isAdminOrSales = user.role === UserRole.ADMIN || user.role === UserRole.SALES;
|
||||
|
||||
if (isAdminOrSales) {
|
||||
return this.applicationsService.findOne(applicationId);
|
||||
}
|
||||
|
||||
// Regular user - must own the app
|
||||
return this.applicationsService.findOne(applicationId, user.id);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user