Add prepaid resource credits with prorated deploy billing.

When users delete an app before plan expiry, remaining resources become credits for a new deploy. The deploy calculator shows covered vs additional charges, prices optional services correctly, and prorates extras to days left on the credit.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
keyhan
2026-05-15 17:37:44 +03:30
parent 35dd771f63
commit 5239e8aa94
17 changed files with 1021 additions and 96 deletions
@@ -29,6 +29,8 @@ import { KubernetesService } from '../kubernetes/kubernetes.service';
import { DeploymentsService } from '../deployments/deployments.service';
import { AccessService } from '../access/access.service';
import { CreateServiceAccessDto } from '../access/dto/service-access.dto';
import { SnapshotsService } from '../snapshots/snapshots.service';
import { BillingService } from '../billing/billing.service';
@ApiTags('Applications')
@ApiBearerAuth()
@@ -44,6 +46,10 @@ export class ApplicationsController {
@Inject(forwardRef(() => DeploymentsService))
private readonly deploymentsService: DeploymentsService,
private readonly accessService: AccessService,
@Inject(forwardRef(() => SnapshotsService))
private readonly snapshotsService: SnapshotsService,
@Inject(forwardRef(() => BillingService))
private readonly billingService: BillingService,
) {}
private isStaff(role: string): boolean {
@@ -384,20 +390,19 @@ export class ApplicationsController {
}
@Delete(':id')
@ApiOperation({ summary: 'Delete an application and all its resources' })
@ApiOperation({ summary: 'Permanently delete an application and all its resources' })
async delete(@Param('id') id: string, @Request() req: any) {
const isStaff = req.user.role === UserRole.ADMIN || req.user.role === UserRole.TECHNICAL;
// 1. Get the app first
const app = await this.applicationsService.findOne(id, isStaff ? undefined : req.user.id);
// 2. Revoke temporary access grants
const credit = await this.billingService.createCreditFromDeletedApp(app);
try {
await this.accessService.revokeAllForApplication(app.id);
} catch (e: any) {
this.logger.warn(`Access grant cleanup failed for ${app.name}: ${e.message}`);
}
// 3. Delete K8s resources (deployment, service, ingress, db, secrets, PVCs)
try {
await this.kubernetesService.deleteApplication(app);
this.logger.log(`Deleted K8s resources for ${app.name}`);
@@ -405,16 +410,23 @@ export class ApplicationsController {
this.logger.warn(`K8s cleanup failed for ${app.name}: ${e.message}`);
}
// 4. Delete deployment records from DB
try {
await this.deploymentsService.deleteAllForApplication(app.id);
} catch (e: any) {
this.logger.warn(`Deployment records cleanup failed for ${app.name}: ${e.message}`);
}
// 5. Delete app (also deletes uploaded files)
try {
await this.snapshotsService.deleteAllForApplication(app.id);
} catch (e: any) {
this.logger.warn(`Snapshot cleanup failed for ${app.name}: ${e.message}`);
}
await this.applicationsService.delete(id, isStaff ? app.userId : req.user.id);
return { message: `Application "${app.name}" and all resources deleted` };
return {
message: `Application "${app.name}" and all resources deleted`,
resourceCredit: credit ? this.billingService.formatCreditForApi(credit) : null,
};
}
}