Compose-ready workspace: packages/zappier (rate card, portal, Stripe), packages/verae-zapier-middleware (timestamp + NATS), packages/verae-zapier (CLI app), vendor/zapier-platform, and research/zapier vendor corpus. Gate 0 structure checks pass. Product code and research are not yet wired.
121 lines
4.8 KiB
TypeScript
121 lines
4.8 KiB
TypeScript
import {
|
|
ConfigTierCatalog,
|
|
DEFAULT_PRICING,
|
|
DEFAULT_TIERS,
|
|
InMemoryPricingStore,
|
|
quoteCall,
|
|
} from '../src/pricing';
|
|
|
|
const noUsage = { metadataBytes: 0, attachmentBytes: 0 };
|
|
|
|
describe('quoteCall', () => {
|
|
it('prices free endpoints at 0 on every tier, ignoring the multiplier', () => {
|
|
for (const tier of DEFAULT_TIERS) {
|
|
expect(quoteCall(DEFAULT_PRICING, tier.id, 'status', noUsage).totalCents).toBe(0);
|
|
expect(quoteCall(DEFAULT_PRICING, tier.id, 'storage-list', noUsage).totalCents).toBe(0);
|
|
}
|
|
});
|
|
|
|
it('applies the tier multiplier to fixed list prices', () => {
|
|
// transform list price: 4 cents
|
|
expect(quoteCall(DEFAULT_PRICING, 'free', 'transform', noUsage).totalCents).toBe(4);
|
|
expect(quoteCall(DEFAULT_PRICING, 'pro', 'transform', noUsage).totalCents).toBe(2);
|
|
expect(quoteCall(DEFAULT_PRICING, 'business', 'transform', noUsage).totalCents).toBe(1);
|
|
});
|
|
|
|
it('prices storage as base + per-KB metadata + per-MB attachments at list rates', () => {
|
|
const usage = { metadataBytes: 2048, attachmentBytes: 2 * 1024 * 1024 };
|
|
// list: 10 + 2 * 1 + 2 * 50 = 112
|
|
expect(quoteCall(DEFAULT_PRICING, 'free', 'storage', usage).totalCents).toBe(112);
|
|
expect(quoteCall(DEFAULT_PRICING, 'pro', 'storage', usage).totalCents).toBe(56);
|
|
expect(quoteCall(DEFAULT_PRICING, 'business', 'storage', usage).totalCents).toBe(28);
|
|
});
|
|
|
|
it('rounds partial KB and MB up before applying the multiplier', () => {
|
|
const q = quoteCall(DEFAULT_PRICING, 'free', 'storage', {
|
|
metadataBytes: 1,
|
|
attachmentBytes: 1,
|
|
});
|
|
// list: 10 + 1 KB * 1 + 1 MB * 50 = 61
|
|
expect(q.listCents).toBe(61);
|
|
expect(q.totalCents).toBe(61);
|
|
});
|
|
|
|
it('exposes the list-price breakdown and the multiplied total', () => {
|
|
const q = quoteCall(DEFAULT_PRICING, 'pro', 'storage', {
|
|
metadataBytes: 1024,
|
|
attachmentBytes: 0,
|
|
});
|
|
expect(q.breakdown).toEqual({ baseCents: 10, metadataCents: 1, attachmentCents: 0 });
|
|
expect(q.listCents).toBe(11);
|
|
expect(q.totalCents).toBe(6); // Math.round(11 * 0.5)
|
|
});
|
|
|
|
it('lets a per-customer multiplier override beat the tier multiplier', () => {
|
|
expect(quoteCall(DEFAULT_PRICING, 'free', 'transform', noUsage, 0.5).totalCents).toBe(2);
|
|
expect(quoteCall(DEFAULT_PRICING, 'free', 'status', noUsage, 0.5).totalCents).toBe(0);
|
|
});
|
|
|
|
it('falls back to the tier default rule for endpoints not on the rate card', () => {
|
|
// pro default rule: fixed 8 list -> round(8 * 0.5) = 4
|
|
expect(quoteCall(DEFAULT_PRICING, 'pro', 'experimental', noUsage).totalCents).toBe(4);
|
|
});
|
|
|
|
it('throws for an endpoint with no rate-card entry and no tier default', () => {
|
|
expect(() => quoteCall(DEFAULT_PRICING, 'free', 'experimental', noUsage)).toThrow(
|
|
'No price rule',
|
|
);
|
|
});
|
|
|
|
it('throws for an unknown tier', () => {
|
|
expect(() => quoteCall(DEFAULT_PRICING, 'platinum', 'status', noUsage)).toThrow(
|
|
'Unknown tier',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('ConfigTierCatalog', () => {
|
|
it('finds tiers by id and lists them', () => {
|
|
const catalog = new ConfigTierCatalog(DEFAULT_TIERS);
|
|
expect(catalog.find('pro')?.multiplier).toBe(0.5);
|
|
expect(catalog.find('nope')).toBeUndefined();
|
|
expect(catalog.list().map((t) => t.id)).toEqual(['free', 'pro', 'business']);
|
|
});
|
|
|
|
it('supports adding a customer type as pure config', () => {
|
|
const catalog = new ConfigTierCatalog([
|
|
...DEFAULT_TIERS,
|
|
{ id: 'edu', name: 'Education', multiplier: 0.4, monthlyCreditCents: 500 },
|
|
]);
|
|
expect(catalog.find('edu')?.name).toBe('Education');
|
|
});
|
|
});
|
|
|
|
describe('InMemoryPricingStore', () => {
|
|
it('seeds from the default rate card and tiers', () => {
|
|
const store = new InMemoryPricingStore();
|
|
expect(store.getRateCard().endpoints.status).toEqual({ kind: 'free' });
|
|
expect(store.getTiers().map((t) => t.id)).toEqual(['free', 'pro', 'business']);
|
|
});
|
|
|
|
it('upserts and deletes endpoints', () => {
|
|
const store = new InMemoryPricingStore();
|
|
store.upsertEndpoint('experimental', { kind: 'fixed', fixedCents: 9 });
|
|
expect(store.getRateCard().endpoints.experimental).toEqual({
|
|
kind: 'fixed',
|
|
fixedCents: 9,
|
|
});
|
|
store.deleteEndpoint('experimental');
|
|
expect(store.getRateCard().endpoints.experimental).toBeUndefined();
|
|
});
|
|
|
|
it('upserts and deletes tiers', () => {
|
|
const store = new InMemoryPricingStore();
|
|
store.upsertTier({ id: 'edu', name: 'Education', multiplier: 0.4, monthlyCreditCents: 500 });
|
|
expect(store.getTiers().find((t) => t.id === 'edu')?.multiplier).toBe(0.4);
|
|
store.upsertTier({ id: 'edu', name: 'Education', multiplier: 0.3, monthlyCreditCents: 500 });
|
|
expect(store.getTiers().filter((t) => t.id === 'edu')).toHaveLength(1);
|
|
store.deleteTier('edu');
|
|
expect(store.getTiers().find((t) => t.id === 'edu')).toBeUndefined();
|
|
});
|
|
});
|