import type { ResultSetHeader, RowDataPacket } from "mysql2/promise"; import { BILL_BODY_LIMIT } from "./body"; import { withTransaction, getPool } from "./db"; import { isUuid } from "./ids"; export class BillStoreError extends Error { readonly status: number; constructor(status: number, message: string) { super(message); this.status = status; } } export async function storePublishedBill(token: string, raw: string): Promise { if (!isUuid(token)) throw new BillStoreError(400, "invalid token"); if (Buffer.byteLength(raw, "utf8") > BILL_BODY_LIMIT) throw new BillStoreError(413, "body too large"); let parsed: unknown; try { parsed = JSON.parse(raw); } catch { throw new BillStoreError(400, "invalid json"); } const isSnapshot = parsed !== null && typeof parsed === "object" && !Array.isArray(parsed); await withTransaction(async (conn) => { await conn.query( `INSERT INTO bill_snapshots (token, payload_json, updated_at) VALUES (?, ?, UTC_TIMESTAMP(3)) ON DUPLICATE KEY UPDATE payload_json = VALUES(payload_json), updated_at = UTC_TIMESTAMP(3)`, [token, raw], ); if (isSnapshot) { await conn.query( `UPDATE invoices SET snapshot_json = ? WHERE public_token = ? AND deleted_at IS NULL`, [raw, token], ); } }); } /** Invoice snapshot first, then the legacy store. */ export async function readBillPayload(token: string): Promise { if (!isUuid(token)) return null; const pool = getPool(); const [invoices] = await pool.query( `SELECT snapshot_json FROM invoices WHERE public_token = ? AND deleted_at IS NULL LIMIT 1`, [token], ); const snapshot = invoices[0]?.snapshot_json; if (typeof snapshot === "string" && snapshot.length > 0) return snapshot; const [legacy] = await pool.query( `SELECT payload_json FROM bill_snapshots WHERE token = ? LIMIT 1`, [token], ); const payload = legacy[0]?.payload_json; if (typeof payload === "string" && payload.length > 0) return payload; return null; }