Initial import of zappier-edge from zapier monorepo

This commit is contained in:
George Lambert 2026-09-11 14:59:21 -04:00
commit 435de6414f
121 changed files with 19888 additions and 0 deletions

106
src/invoicing.ts Normal file
View file

@ -0,0 +1,106 @@
import { Customer } from './auth';
import { TierConfig } from './pricing';
import { UsageEntry } from './usage';
export interface InvoiceLine {
endpointId: string;
calls: number;
cents: number;
}
export interface Invoice {
/** INV-<period>-<zero-padded sequence>, e.g. INV-2026-07-0007 */
id: string;
customerId: string;
/** Billing period, YYYY-MM */
period: string;
status: 'draft' | 'issued' | 'paid';
lines: InvoiceLine[];
/** Gross usage in cents (sum of lines, before credit). */
totalCents: number;
/** Monthly credit actually consumed. */
creditCents: number;
/** totalCents creditCents, floored at 0. */
billableCents: number;
billingType: 'stripe' | 'purchase_order';
poNumber?: string;
issuedAtMs?: number;
dueAtMs?: number;
paidAtMs?: number;
}
export interface InvoiceRepo {
save(invoice: Invoice): void;
get(id: string): Invoice | undefined;
list(filter: {
customerId?: string;
period?: string;
status?: Invoice['status'];
}): Invoice[];
/** 1-based sequence for the next invoice id within a period. */
nextSequence(period: string): number;
}
/** Test/dev adapter; production uses SqliteInvoiceRepo. */
export class InMemoryInvoiceRepo implements InvoiceRepo {
private invoices = new Map<string, Invoice>();
save(invoice: Invoice): void {
this.invoices.set(invoice.id, { ...invoice, lines: invoice.lines.map((l) => ({ ...l })) });
}
get(id: string): Invoice | undefined {
return this.invoices.get(id);
}
list(filter: {
customerId?: string;
period?: string;
status?: Invoice['status'];
}): Invoice[] {
return [...this.invoices.values()]
.filter((i) => (filter.customerId ? i.customerId === filter.customerId : true))
.filter((i) => (filter.period ? i.period === filter.period : true))
.filter((i) => (filter.status ? i.status === filter.status : true))
.sort((a, b) => a.id.localeCompare(b.id));
}
nextSequence(period: string): number {
return [...this.invoices.values()].filter((i) => i.period === period).length + 1;
}
}
export function buildInvoice(args: {
customer: Customer;
period: string;
sequence: number;
entries: UsageEntry[];
tier: TierConfig;
poNumber?: string;
}): Invoice {
const { customer, period, sequence, entries, tier, poNumber } = args;
const byEndpoint = new Map<string, InvoiceLine>();
for (const e of entries) {
const line = byEndpoint.get(e.endpointId) ?? { endpointId: e.endpointId, calls: 0, cents: 0 };
line.calls += 1;
line.cents += e.cents;
byEndpoint.set(e.endpointId, line);
}
const lines = [...byEndpoint.values()].sort((a, b) =>
a.endpointId.localeCompare(b.endpointId),
);
const totalCents = lines.reduce((sum, l) => sum + l.cents, 0);
const creditCents = Math.min(totalCents, tier.monthlyCreditCents);
return {
id: `INV-${period}-${String(sequence).padStart(4, '0')}`,
customerId: customer.id,
period,
status: 'draft',
lines,
totalCents,
creditCents,
billableCents: totalCents - creditCents,
billingType: customer.billingType ?? 'stripe',
...(poNumber !== undefined ? { poNumber } : {}),
};
}