Files
WRNexusJS/packages/db/src/ledger.ts
T
Clintchiz 64ab20cc95
Quality / quality (ubuntu-latest) (push) Failing after 22s
Quality / quality (windows-latest) (push) Canceled after 0s
feat: add application productivity foundations
2026-08-23 11:13:03 +05:30

77 lines
2.6 KiB
TypeScript

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]),
};
}