zappier-edge/tests/reports.test.ts

100 lines
3.6 KiB
TypeScript

import { billingRows, toCsv, usageTrend } from '../src/reports';
import { Customer } from '../src/auth';
import { TierConfig } from '../src/pricing';
import { UsageEntry } from '../src/usage';
const tiers: TierConfig[] = [
{ id: 'free', name: 'Free', multiplier: 1, monthlyCreditCents: 100 },
{ id: 'pro', name: 'Pro', multiplier: 0.5, monthlyCreditCents: 1000 },
];
const customers: Customer[] = [
{ id: 'cust_1', name: 'Ada', tierId: 'free', apiKey: 'key-ada' },
{ id: 'cust_2', name: 'Grace', tierId: 'pro', apiKey: 'key-grace' },
{ id: 'cust_3', name: 'Linus', tierId: 'pro', apiKey: 'key-linus', billingType: 'purchase_order' },
];
const entry = (customerId: string, cents: number, iso: string): UsageEntry => ({
customerId,
endpointId: 'storage',
cents,
metadataBytes: 0,
attachmentBytes: 0,
timestamp: new Date(iso),
});
describe('billingRows', () => {
const entries = [
entry('cust_1', 150, '2026-07-05T10:00:00Z'),
entry('cust_2', 1500, '2026-07-06T10:00:00Z'),
entry('cust_2', 300, '2026-08-01T00:30:00Z'),
];
it('aggregates per customer with tier credits', () => {
const rows = billingRows({ entries, customers, tiers });
const ada = rows.find((r) => r.customerId === 'cust_1')!;
expect(ada).toMatchObject({ calls: 1, totalCents: 150, creditCents: 100, billableCents: 50 });
const grace = rows.find((r) => r.customerId === 'cust_2')!;
expect(grace).toMatchObject({ calls: 2, totalCents: 1800, creditCents: 1000, billableCents: 800 });
});
it('includes zero-usage customers and exposes billingType', () => {
const rows = billingRows({ entries, customers, tiers });
const linus = rows.find((r) => r.customerId === 'cust_3')!;
expect(linus).toMatchObject({ calls: 0, totalCents: 0, billableCents: 0, billingType: 'purchase_order' });
});
it('filters by date range (inclusive from, exclusive to)', () => {
const rows = billingRows({
entries,
customers,
tiers,
from: new Date('2026-07-01T00:00:00Z'),
to: new Date('2026-08-01T00:00:00Z'),
});
const grace = rows.find((r) => r.customerId === 'cust_2')!;
expect(grace.calls).toBe(1); // August entry excluded
expect(grace.totalCents).toBe(1500);
});
it('filters by customerId and billingType', () => {
expect(billingRows({ entries, customers, tiers, customerId: 'cust_1' })).toHaveLength(1);
const po = billingRows({ entries, customers, tiers, billingType: 'purchase_order' });
expect(po.map((r) => r.customerId)).toEqual(['cust_3']);
});
});
describe('usageTrend', () => {
it('buckets by day', () => {
const trend = usageTrend(
[entry('cust_1', 10, '2026-07-05T09:00:00Z'), entry('cust_2', 20, '2026-07-05T20:00:00Z'), entry('cust_1', 5, '2026-07-06T10:00:00Z')],
'day',
);
expect(trend).toEqual([
{ bucket: '2026-07-05', calls: 2, cents: 30 },
{ bucket: '2026-07-06', calls: 1, cents: 5 },
]);
});
it('buckets by ISO week (Monday key) across a month boundary', () => {
const trend = usageTrend(
[entry('cust_1', 10, '2026-07-31T10:00:00Z'), entry('cust_1', 20, '2026-08-01T10:00:00Z')],
'week',
);
expect(trend).toEqual([{ bucket: '2026-07-27', calls: 2, cents: 30 }]);
});
});
describe('toCsv', () => {
it('escapes commas, quotes, and newlines per RFC 4180', () => {
const csv = toCsv(
[{ name: 'Ada, "The Great"', note: 'line1\nline2', cents: 50 }],
[
{ key: 'name', label: 'Name' },
{ key: 'note', label: 'Note' },
{ key: 'cents', label: 'Cents' },
],
);
expect(csv).toBe('Name,Note,Cents\n"Ada, ""The Great""","line1\nline2",50');
});
});