Initial import of zappier-edge from zapier monorepo
This commit is contained in:
commit
af2066c866
121 changed files with 19885 additions and 0 deletions
106
src/invoicing.ts
Normal file
106
src/invoicing.ts
Normal 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 } : {}),
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue