Files
2026-09-28 23:15:47 +05:30

59 lines
2.1 KiB
TypeScript

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<void> {
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<ResultSetHeader>(
`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<string | null> {
if (!isUuid(token)) return null;
const pool = getPool();
const [invoices] = await pool.query<RowDataPacket[]>(
`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<RowDataPacket[]>(
`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;
}