import type { Db } from "./driver.ts"; const SAFE = /^[A-Za-z_][A-Za-z0-9_]*$/; const ident = (value: string) => { if (!SAFE.test(value)) throw new TypeError(`Unsafe ledger identifier: ${value}`); return value; }; export interface LedgerOptions { db: () => Db; table: string; subjectColumn?: string; amountColumn?: string; kindColumn?: string; reasonColumn?: string; referenceColumn?: string; } export function defineLedger(options: LedgerOptions) { const table = ident(options.table); const subject = ident(options.subjectColumn ?? "subject_id"); const amount = ident(options.amountColumn ?? "amount"); const kind = ident(options.kindColumn ?? "kind"); const reason = ident(options.reasonColumn ?? "reason"); const reference = ident(options.referenceColumn ?? "ref"); const positive = (value: number) => { if (!Number.isSafeInteger(value) || value < 1) throw new RangeError("ledger amount must be a positive safe integer"); }; const write = async ( subjectId: string, delta: number, entryKind: string, entryReason: string, ref = "", ) => { if (!subjectId.trim()) throw new TypeError("ledger subject cannot be empty"); await options .db() .exec( `INSERT INTO ${table} (${subject},${amount},${kind},${reason},${reference}) VALUES (?,?,?,?,?)`, [subjectId, delta, entryKind, entryReason, ref], ); }; const balance = async (subjectId: string, db = options.db()) => { const row = await db.one<{ balance: number | string | null }>( `SELECT COALESCE(SUM(${amount}),0) AS balance FROM ${table} WHERE ${subject} = ?`, [subjectId], ); return Number(row?.balance ?? 0); }; return { balance, async credit(subjectId: string, value: number, entryReason: string, ref = "") { positive(value); await write(subjectId, value, "credit", entryReason, ref); }, async debit(subjectId: string, value: number, entryReason: string, ref = "") { positive(value); return options.db().tx(async (db) => { if ((await balance(subjectId, db)) < value) return false; await db.exec( `INSERT INTO ${table} (${subject},${amount},${kind},${reason},${reference}) VALUES (?,?,?,?,?)`, [subjectId, -value, "debit", entryReason, ref], ); return true; }); }, refund: (subjectId: string, value: number, entryReason: string, ref = "") => { positive(value); return write(subjectId, value, "refund", entryReason, ref); }, entries: (subjectId: string) => options.db().all(`SELECT * FROM ${table} WHERE ${subject} = ? ORDER BY id DESC`, [subjectId]), }; }