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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,15 @@ import { Wallet } from './entities/wallet.entity';
|
||||
import { WalletTransaction } from './entities/wallet-transaction.entity';
|
||||
import { PlatformSetting } from './entities/platform-setting.entity';
|
||||
import { LifecycleModule } from '../lifecycle/lifecycle.module';
|
||||
import { ApplicationsModule } from '../applications/applications.module';
|
||||
import { KubernetesModule } from '../kubernetes/kubernetes.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([ServicePlan, PricingRule, Wallet, WalletTransaction, PlatformSetting]),
|
||||
forwardRef(() => LifecycleModule),
|
||||
forwardRef(() => ApplicationsModule),
|
||||
forwardRef(() => KubernetesModule),
|
||||
],
|
||||
controllers: [BillingController],
|
||||
providers: [BillingService],
|
||||
|
||||
@@ -10,7 +10,9 @@ import {
|
||||
CreateServicePlanDto,
|
||||
UpdateServicePlanDto,
|
||||
CalculateCostDto,
|
||||
UpgradeResourcesDto,
|
||||
} from './dto/billing.dto';
|
||||
import { Application } from '../applications/entities/application.entity';
|
||||
|
||||
@Injectable()
|
||||
export class BillingService {
|
||||
@@ -133,9 +135,14 @@ export class BillingService {
|
||||
// Parse resource values
|
||||
const cpuCores = this.parseCpuToCores(dto.cpuLimit);
|
||||
const memoryGb = this.parseMemoryToGb(dto.memoryLimit);
|
||||
const storageGb = dto.dbStorageSize ? parseFloat(dto.dbStorageSize.replace('Gi', '')) || 0 : 0;
|
||||
const dbStorageGb = dto.dbStorageSize ? parseFloat(dto.dbStorageSize.replace('Gi', '')) || 0 : 0;
|
||||
const appStorageGb = dto.appStorageSize ? parseFloat(dto.appStorageSize.replace('Gi', '')) || 0 : 0;
|
||||
const totalStorageGb = dbStorageGb + appStorageGb;
|
||||
const hasDatabase = dto.databaseType !== 'none';
|
||||
const replicas = dto.replicas || 1;
|
||||
const hasRedis = dto.enableRedis || false;
|
||||
const hasRabbitmq = dto.enableRabbitmq || false;
|
||||
const hasElasticsearch = dto.enableElasticsearch || false;
|
||||
|
||||
const breakdown: { label: string; hourly: number; monthly: number; yearly: number }[] = [];
|
||||
let totalBase = 0;
|
||||
@@ -158,13 +165,25 @@ export class BillingService {
|
||||
label = `Memory (${(memoryGb * replicas).toFixed(2)} GB)`;
|
||||
break;
|
||||
case PricingResourceType.STORAGE_PER_GB:
|
||||
cost = storageGb * Number(rule.unitPrice);
|
||||
label = `Storage (${storageGb} GB)`;
|
||||
cost = totalStorageGb * Number(rule.unitPrice);
|
||||
label = `Storage (${totalStorageGb} GB)`;
|
||||
break;
|
||||
case PricingResourceType.DATABASE_ADDON:
|
||||
cost = hasDatabase ? Number(rule.unitPrice) : 0;
|
||||
label = 'Database addon';
|
||||
break;
|
||||
case PricingResourceType.REDIS_ADDON:
|
||||
cost = hasRedis ? Number(rule.unitPrice) : 0;
|
||||
label = 'Redis addon';
|
||||
break;
|
||||
case PricingResourceType.RABBITMQ_ADDON:
|
||||
cost = hasRabbitmq ? Number(rule.unitPrice) : 0;
|
||||
label = 'RabbitMQ addon';
|
||||
break;
|
||||
case PricingResourceType.ELASTICSEARCH_ADDON:
|
||||
cost = hasElasticsearch ? Number(rule.unitPrice) : 0;
|
||||
label = 'Elasticsearch addon';
|
||||
break;
|
||||
}
|
||||
|
||||
if (cost > 0) {
|
||||
@@ -300,6 +319,7 @@ export class BillingService {
|
||||
memoryLimit: string;
|
||||
replicas: number;
|
||||
dbStorageSize?: string;
|
||||
appStorageSize?: string;
|
||||
}): Promise<{ hourly: number; monthly: number; yearly: number }> {
|
||||
const result = await this.calculateCost({
|
||||
runtime: app.runtime,
|
||||
@@ -308,7 +328,100 @@ export class BillingService {
|
||||
memoryLimit: app.memoryLimit,
|
||||
replicas: app.replicas,
|
||||
dbStorageSize: app.dbStorageSize,
|
||||
appStorageSize: app.appStorageSize,
|
||||
});
|
||||
return { hourly: result.hourly, monthly: result.monthly, yearly: result.yearly };
|
||||
}
|
||||
|
||||
// ─── Renewal Cost Calculation ─────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Calculate renewal cost for an application.
|
||||
* Returns cost for each billing cycle based on current app config.
|
||||
*/
|
||||
async calculateRenewalCost(app: Application): Promise<{
|
||||
hourly: number;
|
||||
monthly: number;
|
||||
yearly: number;
|
||||
currentCycle?: BillingCycle;
|
||||
currentCycleCost?: number;
|
||||
}> {
|
||||
const costs = await this.calculateCostForApp(app);
|
||||
|
||||
let currentCycleCost: number | undefined;
|
||||
if (app.billingCycle) {
|
||||
currentCycleCost = app.billingCycle === BillingCycle.HOURLY ? costs.hourly
|
||||
: app.billingCycle === BillingCycle.MONTHLY ? costs.monthly
|
||||
: costs.yearly;
|
||||
}
|
||||
|
||||
return {
|
||||
...costs,
|
||||
currentCycle: app.billingCycle,
|
||||
currentCycleCost,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Resource Upgrade Cost Calculation ────────────────────────────
|
||||
|
||||
/**
|
||||
* Calculate the cost difference for a resource upgrade.
|
||||
* Returns the additional cost per billing cycle.
|
||||
*/
|
||||
async calculateUpgradeCost(
|
||||
app: Application,
|
||||
newResources: UpgradeResourcesDto,
|
||||
): Promise<{
|
||||
currentCost: { hourly: number; monthly: number; yearly: number };
|
||||
newCost: { hourly: number; monthly: number; yearly: number };
|
||||
difference: { hourly: number; monthly: number; yearly: number };
|
||||
proratedAmount: number;
|
||||
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,
|
||||
});
|
||||
|
||||
// Difference
|
||||
const difference = {
|
||||
hourly: newCost.hourly - currentCost.hourly,
|
||||
monthly: newCost.monthly - currentCost.monthly,
|
||||
yearly: newCost.yearly - currentCost.yearly,
|
||||
};
|
||||
|
||||
// Calculate prorated amount based on remaining time in billing period
|
||||
let proratedAmount = 0;
|
||||
let remainingHours = 0;
|
||||
|
||||
if (app.planExpiresAt && app.billingCycle) {
|
||||
const now = new Date();
|
||||
const expiresAt = new Date(app.planExpiresAt);
|
||||
remainingHours = Math.max(0, (expiresAt.getTime() - now.getTime()) / (1000 * 60 * 60));
|
||||
|
||||
// Only charge difference if upgrading (not downgrading)
|
||||
if (difference.hourly > 0) {
|
||||
proratedAmount = Math.ceil(difference.hourly * remainingHours);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
currentCost,
|
||||
newCost,
|
||||
difference,
|
||||
proratedAmount,
|
||||
remainingHours: Math.round(remainingHours),
|
||||
billingCycle: app.billingCycle,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,4 +116,73 @@ export class CalculateCostDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
dbStorageSize?: string;
|
||||
|
||||
@ApiProperty({ example: '2Gi', description: 'App storage size for uploads/wp-content' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
appStorageSize?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: false, description: 'Enable Redis caching' })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
enableRedis?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ example: false, description: 'Enable RabbitMQ message broker' })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
enableRabbitmq?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ example: false, description: 'Enable Elasticsearch logging' })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
enableElasticsearch?: boolean;
|
||||
}
|
||||
|
||||
// ─── Renewal & Upgrade DTOs ─────────────────────────────────────────
|
||||
|
||||
export class RenewApplicationDto {
|
||||
@ApiProperty({ enum: BillingCycle, example: 'monthly' })
|
||||
@IsEnum(BillingCycle)
|
||||
cycle: BillingCycle;
|
||||
}
|
||||
|
||||
export class UpgradeResourcesDto {
|
||||
@ApiPropertyOptional({ example: '500m', description: 'New CPU request' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
cpuRequest?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '1000m', description: 'New CPU limit' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
cpuLimit?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '256Mi', description: 'New memory request' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
memoryRequest?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '1Gi', description: 'New memory limit' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
memoryLimit?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 2, description: 'New replica count' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
replicas?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: '5Gi', description: 'New database storage size' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
dbStorageSize?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '5Gi', description: 'New app storage size for uploads/wp-content' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
appStorageSize?: string;
|
||||
}
|
||||
|
||||
export class CalculateUpgradeCostDto extends UpgradeResourcesDto {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user