Add managed databases and services with billing-aligned upgrades.

Introduce product types for managed PostgreSQL, Redis, and RabbitMQ with a dedicated dashboard, Helm-only deploy pipeline, external access, snapshots with progress, and prorated resource or storage upgrades matching application billing rules. PVCs use an expandable StorageClass with automatic migration when legacy disks cannot resize in place.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
keyhan
2026-05-23 19:00:09 +03:30
parent 736509708b
commit 695e05f948
55 changed files with 5575 additions and 600 deletions
+64 -31
View File
@@ -13,6 +13,8 @@ import {
InvoiceStatus,
PaymentMethod,
UserRole,
ProductType,
isManagedProductType,
} from '../common/enums';
import { CalculateCostDto, UpgradeResourcesDto } from './dto/billing.dto';
import { UpdatePricingCatalogDto } from './dto/pricing-catalog.dto';
@@ -560,6 +562,7 @@ export class BillingService {
* Used by lifecycle service for auto-renew.
*/
async calculateCostForApp(app: {
productType?: ProductType;
runtime: string;
databaseType: string;
cpuLimit: string;
@@ -572,20 +575,9 @@ export class BillingService {
enableElasticsearch?: boolean;
customDomain?: string;
customDomainStatus?: string;
optionalServiceResources?: Application['optionalServiceResources'];
}): Promise<{ hourly: number; monthly: number; yearly: number }> {
const result = await this.calculateCost({
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,
enableCustomDomain: !!app.customDomain && app.customDomainStatus === 'verified',
});
const result = await this.calculateCost(this.toCalculateDto(this.appToResourceConfig(app as Application)));
return { hourly: result.hourly, monthly: result.monthly, yearly: result.yearly };
}
@@ -635,19 +627,43 @@ export class BillingService {
remainingHours: number;
billingCycle: BillingCycle | null;
}> {
// Current cost
const currentCost = await this.calculateCostForApp(app);
// New cost with upgraded resources
const newCost = await this.calculateCost({
runtime: app.runtime,
databaseType: app.databaseType,
cpuLimit: newResources.cpuLimit || app.cpuLimit,
memoryLimit: newResources.memoryLimit || app.memoryLimit,
replicas: newResources.replicas ?? app.replicas,
dbStorageSize: newResources.dbStorageSize || app.dbStorageSize,
appStorageSize: newResources.appStorageSize || app.appStorageSize,
});
const base = this.appToResourceConfig(app);
const merged = {
...base,
...(newResources.cpuLimit && { cpuLimit: newResources.cpuLimit }),
...(newResources.memoryLimit && { memoryLimit: newResources.memoryLimit }),
replicas: newResources.replicas ?? base.replicas,
...(newResources.dbStorageSize && { dbStorageSize: newResources.dbStorageSize }),
...(newResources.appStorageSize && { appStorageSize: newResources.appStorageSize }),
...(newResources.redisResources && {
redisResources: {
...base.redisResources,
...newResources.redisResources,
storageGi:
newResources.redisResources.storageGi ??
base.redisResources?.storageGi ??
1,
},
}),
...(newResources.rabbitmqResources && {
rabbitmqResources: {
...base.rabbitmqResources,
...newResources.rabbitmqResources,
storageGi:
newResources.rabbitmqResources.storageGi ??
base.rabbitmqResources?.storageGi ??
2,
},
}),
};
const newCostResult = await this.calculateCost(this.toCalculateDto(merged));
const newCost = {
hourly: newCostResult.hourly,
monthly: newCostResult.monthly,
yearly: newCostResult.yearly,
};
// Difference
const difference = {
@@ -719,18 +735,24 @@ export class BillingService {
: undefined;
const dtoExtras =
'redisResources' in app ? (app as CalculateCostDto) : undefined;
const productType =
'productType' in app
? ((app as Application).productType ?? ProductType.APPLICATION)
: ((app as CalculateCostDto).productType ?? ProductType.APPLICATION);
const managed = isManagedProductType(productType);
return {
productType,
runtime: app.runtime,
databaseType: app.databaseType,
cpuLimit: app.cpuLimit,
memoryLimit: app.memoryLimit,
replicas: app.replicas || 1,
replicas: managed ? 0 : (app.replicas ?? 1),
dbStorageSize: app.dbStorageSize,
appStorageSize: app.appStorageSize,
enableRedis: !!app.enableRedis,
enableRabbitmq: !!app.enableRabbitmq,
enableElasticsearch: !!app.enableElasticsearch,
enableCustomDomain,
enableElasticsearch: managed ? false : !!app.enableElasticsearch,
enableCustomDomain: managed ? false : enableCustomDomain,
redisResources: optionalRes?.redis ?? dtoExtras?.redisResources,
rabbitmqResources: optionalRes?.rabbitmq ?? dtoExtras?.rabbitmqResources,
};
@@ -744,6 +766,7 @@ export class BillingService {
const credit = this.creditRepo.create({
userId: app.userId,
sourceAppName: app.name,
productType: app.productType ?? ProductType.APPLICATION,
runtime: app.runtime,
databaseType: app.databaseType,
cpuLimit: app.cpuLimit,
@@ -781,6 +804,7 @@ export class BillingService {
return {
id: credit.id,
sourceAppName: credit.sourceAppName,
productType: credit.productType ?? ProductType.APPLICATION,
runtime: credit.runtime,
databaseType: credit.databaseType,
cpuLimit: credit.cpuLimit,
@@ -803,6 +827,9 @@ export class BillingService {
config: ReturnType<typeof this.appToResourceConfig>,
credit: ResourceCredit,
): boolean {
const configProduct = config.productType ?? ProductType.APPLICATION;
const creditProduct = credit.productType ?? ProductType.APPLICATION;
if (configProduct !== creditProduct) return false;
if (config.runtime !== credit.runtime) return false;
if (
credit.databaseType !== DatabaseType.NONE &&
@@ -842,12 +869,16 @@ export class BillingService {
): Promise<ResourceCredit | null> {
const credits = await this.getActiveCredits(userId);
return (
credits.find(
(c) =>
credits.find((c) => {
const creditProduct = c.productType ?? ProductType.APPLICATION;
const configProduct = config.productType ?? ProductType.APPLICATION;
if (creditProduct !== configProduct) return false;
return (
c.runtime === config.runtime &&
(c.databaseType === DatabaseType.NONE ||
c.databaseType === config.databaseType),
) ?? null
c.databaseType === config.databaseType)
);
}) ?? null
);
}
@@ -865,6 +896,7 @@ export class BillingService {
config: ReturnType<typeof this.appToResourceConfig>,
): CalculateCostDto {
return {
productType: config.productType,
runtime: config.runtime,
databaseType: config.databaseType,
cpuLimit: config.cpuLimit,
@@ -887,6 +919,7 @@ export class BillingService {
patch: Partial<CalculateCostDto> = {},
): CalculateCostDto {
return {
productType: credit.productType ?? ProductType.APPLICATION,
runtime: credit.runtime,
databaseType: credit.databaseType,
cpuLimit: credit.cpuLimit,