119 lines
4.5 KiB
TypeScript
119 lines
4.5 KiB
TypeScript
import Database from 'better-sqlite3';
|
|
import { Invoice, InvoiceLine, InvoiceRepo } from '../invoicing';
|
|
|
|
export class SqliteInvoiceRepo implements InvoiceRepo {
|
|
constructor(private db: Database.Database) {
|
|
this.db.exec(`
|
|
CREATE TABLE IF NOT EXISTS invoices (
|
|
id TEXT PRIMARY KEY,
|
|
customer_id TEXT NOT NULL,
|
|
period TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
total_cents INTEGER NOT NULL,
|
|
credit_cents INTEGER NOT NULL,
|
|
billable_cents INTEGER NOT NULL,
|
|
billing_type TEXT NOT NULL DEFAULT 'stripe',
|
|
po_number TEXT,
|
|
issued_at_ms INTEGER,
|
|
due_at_ms INTEGER,
|
|
paid_at_ms INTEGER
|
|
);
|
|
CREATE TABLE IF NOT EXISTS invoice_lines (
|
|
invoice_id TEXT NOT NULL,
|
|
endpoint_id TEXT NOT NULL,
|
|
calls INTEGER NOT NULL,
|
|
cents INTEGER NOT NULL,
|
|
PRIMARY KEY (invoice_id, endpoint_id)
|
|
);
|
|
`);
|
|
}
|
|
|
|
save(invoice: Invoice): void {
|
|
this.db
|
|
.prepare(
|
|
`INSERT INTO invoices
|
|
(id, customer_id, period, status, total_cents, credit_cents, billable_cents,
|
|
billing_type, po_number, issued_at_ms, due_at_ms, paid_at_ms)
|
|
VALUES (@id, @customerId, @period, @status, @totalCents, @creditCents, @billableCents,
|
|
@billingType, @poNumber, @issuedAtMs, @dueAtMs, @paidAtMs)
|
|
ON CONFLICT(id) DO UPDATE SET
|
|
customer_id = @customerId, period = @period, status = @status,
|
|
total_cents = @totalCents, credit_cents = @creditCents,
|
|
billable_cents = @billableCents, billing_type = @billingType,
|
|
po_number = @poNumber, issued_at_ms = @issuedAtMs,
|
|
due_at_ms = @dueAtMs, paid_at_ms = @paidAtMs`,
|
|
)
|
|
.run({
|
|
...invoice,
|
|
poNumber: invoice.poNumber ?? null,
|
|
issuedAtMs: invoice.issuedAtMs ?? null,
|
|
dueAtMs: invoice.dueAtMs ?? null,
|
|
paidAtMs: invoice.paidAtMs ?? null,
|
|
});
|
|
// Replace the line set atomically with the invoice row.
|
|
this.db.prepare('DELETE FROM invoice_lines WHERE invoice_id = ?').run(invoice.id);
|
|
const insert = this.db.prepare(
|
|
'INSERT INTO invoice_lines (invoice_id, endpoint_id, calls, cents) VALUES (?, ?, ?, ?)',
|
|
);
|
|
for (const line of invoice.lines) {
|
|
insert.run(invoice.id, line.endpointId, line.calls, line.cents);
|
|
}
|
|
}
|
|
|
|
get(id: string): Invoice | undefined {
|
|
const row = this.db.prepare('SELECT * FROM invoices WHERE id = ?').get(id) as
|
|
| Record<string, unknown>
|
|
| undefined;
|
|
return row ? this.toInvoice(row) : undefined;
|
|
}
|
|
|
|
list(filter: { customerId?: string; period?: string; status?: Invoice['status'] }): Invoice[] {
|
|
const where: string[] = [];
|
|
const params: Record<string, string> = {};
|
|
if (filter.customerId) {
|
|
where.push('customer_id = @customerId');
|
|
params.customerId = filter.customerId;
|
|
}
|
|
if (filter.period) {
|
|
where.push('period = @period');
|
|
params.period = filter.period;
|
|
}
|
|
if (filter.status) {
|
|
where.push('status = @status');
|
|
params.status = filter.status;
|
|
}
|
|
const sql = `SELECT * FROM invoices ${where.length ? `WHERE ${where.join(' AND ')}` : ''} ORDER BY id`;
|
|
const rows = this.db.prepare(sql).all(params) as Record<string, unknown>[];
|
|
return rows.map((r) => this.toInvoice(r));
|
|
}
|
|
|
|
nextSequence(period: string): number {
|
|
const { n } = this.db
|
|
.prepare('SELECT COUNT(*) AS n FROM invoices WHERE period = ?')
|
|
.get(period) as { n: number };
|
|
return n + 1;
|
|
}
|
|
|
|
private toInvoice(row: Record<string, unknown>): Invoice {
|
|
const lines = this.db
|
|
.prepare('SELECT endpoint_id, calls, cents FROM invoice_lines WHERE invoice_id = ? ORDER BY endpoint_id')
|
|
.all(row.id as string) as { endpoint_id: string; calls: number; cents: number }[];
|
|
return {
|
|
id: row.id as string,
|
|
customerId: row.customer_id as string,
|
|
period: row.period as string,
|
|
status: row.status as Invoice['status'],
|
|
lines: lines.map(
|
|
(l): InvoiceLine => ({ endpointId: l.endpoint_id, calls: l.calls, cents: l.cents }),
|
|
),
|
|
totalCents: row.total_cents as number,
|
|
creditCents: row.credit_cents as number,
|
|
billableCents: row.billable_cents as number,
|
|
billingType: row.billing_type as Invoice['billingType'],
|
|
poNumber: (row.po_number as string | null) ?? undefined,
|
|
issuedAtMs: (row.issued_at_ms as number | null) ?? undefined,
|
|
dueAtMs: (row.due_at_ms as number | null) ?? undefined,
|
|
paidAtMs: (row.paid_at_ms as number | null) ?? undefined,
|
|
};
|
|
}
|
|
}
|