feat: add application productivity foundations
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
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]),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user