Replace billing plans with per-runtime usage pricing catalog.
Store explicit hourly/monthly/yearly rates in pricing_rates and addon_rates, compute deploy costs without cycle conversion, and simplify admin UI and wallet payment to cycle-only. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { PricingCatalogService } from './pricing-catalog.service';
|
||||
import { PricingRate } from './entities/pricing-rate.entity';
|
||||
import { AddonRate } from './entities/addon-rate.entity';
|
||||
import {
|
||||
AppRuntime,
|
||||
BillingCycle,
|
||||
DatabaseType,
|
||||
PricingResourceType,
|
||||
} from '../common/enums';
|
||||
import { CalculateCostDto } from './dto/billing.dto';
|
||||
|
||||
describe('PricingCatalogService', () => {
|
||||
let service: PricingCatalogService;
|
||||
|
||||
const rateRepo = {
|
||||
find: jest.fn().mockResolvedValue([]),
|
||||
findOne: jest.fn().mockResolvedValue(null),
|
||||
save: jest.fn().mockImplementation((x) => Promise.resolve(x)),
|
||||
create: jest.fn().mockImplementation((x) => x),
|
||||
};
|
||||
|
||||
const addonRepo = {
|
||||
find: jest.fn().mockResolvedValue([]),
|
||||
findOne: jest.fn().mockResolvedValue(null),
|
||||
save: jest.fn().mockImplementation((x) => Promise.resolve(x)),
|
||||
create: jest.fn().mockImplementation((x) => x),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
PricingCatalogService,
|
||||
{ provide: getRepositoryToken(PricingRate), useValue: rateRepo },
|
||||
{ provide: getRepositoryToken(AddonRate), useValue: addonRepo },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get(PricingCatalogService);
|
||||
});
|
||||
|
||||
const baseDto = (): CalculateCostDto => ({
|
||||
runtime: 'nodejs',
|
||||
databaseType: DatabaseType.NONE,
|
||||
cpuLimit: '500m',
|
||||
memoryLimit: '512Mi',
|
||||
replicas: 1,
|
||||
});
|
||||
|
||||
it('parses CPU millicores to cores', () => {
|
||||
expect(service.parseCpuToCores('500m')).toBe(0.5);
|
||||
expect(service.parseCpuToCores('2')).toBe(2);
|
||||
});
|
||||
|
||||
it('computes CPU line with cycle-native prices (no conversion)', () => {
|
||||
const rates = [
|
||||
{
|
||||
runtime: AppRuntime.NODEJS,
|
||||
resourceType: PricingResourceType.CPU_PER_CORE,
|
||||
hourlyPrice: 100,
|
||||
monthlyPrice: 5000,
|
||||
yearlyPrice: 50000,
|
||||
isActive: true,
|
||||
},
|
||||
] as PricingRate[];
|
||||
|
||||
const result = service.computeTotalsWithRates(baseDto(), rates, []);
|
||||
expect(result.hourly).toBe(50);
|
||||
expect(result.monthly).toBe(2500);
|
||||
expect(result.yearly).toBe(25000);
|
||||
expect(result.breakdown[0].label).toContain('CPU');
|
||||
});
|
||||
|
||||
it('includes redis addon only when enabled', () => {
|
||||
const addons = [
|
||||
{
|
||||
resourceType: PricingResourceType.REDIS_ADDON,
|
||||
hourlyPrice: 10,
|
||||
monthlyPrice: 100,
|
||||
yearlyPrice: 1000,
|
||||
isActive: true,
|
||||
},
|
||||
] as AddonRate[];
|
||||
|
||||
const without = service.computeTotalsWithRates(baseDto(), [], addons);
|
||||
expect(without.monthly).toBe(0);
|
||||
|
||||
const withRedis = service.computeTotalsWithRates(
|
||||
{ ...baseDto(), enableRedis: true },
|
||||
[],
|
||||
addons,
|
||||
);
|
||||
expect(withRedis.monthly).toBe(100);
|
||||
expect(withRedis.yearly).toBe(1000);
|
||||
expect(withRedis.hourly).toBe(10);
|
||||
});
|
||||
|
||||
it('yearly deploy uses yearly column not monthly * 12', () => {
|
||||
const rates = [
|
||||
{
|
||||
runtime: AppRuntime.NODEJS,
|
||||
resourceType: PricingResourceType.BASE_FEE,
|
||||
hourlyPrice: 1,
|
||||
monthlyPrice: 100,
|
||||
yearlyPrice: 999,
|
||||
isActive: true,
|
||||
},
|
||||
] as PricingRate[];
|
||||
|
||||
const result = service.computeTotalsWithRates(baseDto(), rates, []);
|
||||
expect(result.yearly).toBe(999);
|
||||
expect(result.monthly).toBe(100);
|
||||
expect(result.yearly).not.toBe(result.monthly * 12);
|
||||
});
|
||||
|
||||
it('amountForCycleFromLine picks the correct column', () => {
|
||||
const line = { label: 'Test', hourly: 1, monthly: 2, yearly: 3 };
|
||||
expect(service.amountForCycleFromLine(line, BillingCycle.HOURLY)).toBe(1);
|
||||
expect(service.amountForCycleFromLine(line, BillingCycle.MONTHLY)).toBe(2);
|
||||
expect(service.amountForCycleFromLine(line, BillingCycle.YEARLY)).toBe(3);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user