77 lines
2.6 KiB
TypeScript
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]),
|
|
};
|
|
}
|