Align admin optional-services billing UI with runtimes and bill RAM proportionally.

Split optional pricing vs deploy defaults and move custom domain to its own card; parse Mi as decimal GB so fractional memory scales linearly with per-GB rates. Deploy wizard hides env and optional services for WordPress and sanitizes create payloads.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
keyhan
2026-05-16 00:39:37 +03:30
parent 3a4fffcec4
commit 67b41ae311
4 changed files with 253 additions and 157 deletions
@@ -76,6 +76,29 @@ describe('PricingCatalogService', () => {
expect(service.parseCpuToCores('2')).toBe(2);
});
it('parses memory Mi to decimal GB for proportional billing (500Mi = 0.5 GB)', () => {
expect(service.parseMemoryToGb('500Mi')).toBe(0.5);
expect(service.parseMemoryToGb('1000Mi')).toBe(1);
expect(service.parseMemoryToGb('1Gi')).toBe(1);
});
it('bills memory proportional to decimal GB (500Mi at half the per-GB monthly rate)', () => {
const rates = [
{
runtime: AppRuntime.NODEJS,
resourceType: PricingResourceType.MEMORY_PER_GB,
hourlyPrice: 0,
monthlyPrice: 10_000,
yearlyPrice: 0,
isActive: true,
},
] as PricingRate[];
const dto = { ...baseDto(), memoryLimit: '500Mi' };
const result = service.computeTotalsWithRates(dto, rates, emptyOptional());
expect(result.monthly).toBe(5000);
});
it('computes CPU line with cycle-native prices (no conversion)', () => {
const rates = [
{
+12 -5
View File
@@ -580,7 +580,7 @@ export class PricingCatalogService implements OnModuleInit {
case PricingResourceType.CPU_PER_CORE:
return `CPU (${quantity.toFixed(2)} core)`;
case PricingResourceType.MEMORY_PER_GB:
return `Memory (${quantity.toFixed(2)} GB)`;
return `Memory (${quantity.toFixed(3)} GB billable)`;
case PricingResourceType.STORAGE_PER_GB:
return `Storage (${quantity} GB)`;
default:
@@ -616,12 +616,19 @@ export class PricingCatalogService implements OnModuleInit {
return parseFloat(cpu) || 0;
}
/**
* Billable memory quantity for rates labeled "per GB".
* Uses decimal GB so fractional Mi scales linearly (e.g. 500Mi → 0.5 × per-GB price).
* Gi values are treated as GB-sized billing units (1Gi → 1 unit).
*/
parseMemoryToGb(memory: string): number {
if (!memory) return 0;
if (memory.endsWith('Gi')) return parseFloat(memory);
if (memory.endsWith('Mi')) return parseFloat(memory) / 1024;
if (memory.endsWith('Ki')) return parseFloat(memory) / (1024 * 1024);
return parseFloat(memory) / (1024 * 1024 * 1024);
const m = memory.trim();
if (m.endsWith('Gi')) return parseFloat(m) || 0;
if (m.endsWith('Mi')) return (parseFloat(m) || 0) / 1000;
if (m.endsWith('Ki')) return (parseFloat(m) || 0) / 1_000_000;
const n = parseFloat(m);
return Number.isFinite(n) ? n / (1024 * 1024 * 1024) : 0;
}
private async upsertRuntimeRate(runtime: AppRuntime, row: PricingRateRow) {