ADR-0001. Double-entry voucher + ledger model
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-04-22 |
| Deciders | finance-team |
| Supersedes | — |
Context
- The earlier model recorded money movement as flat
FinanceTransactionrows with anINCOME/EXPENSE/TRANSFERtype and a single account — there was no document grouping, no counter-leg, and no audit-ready slip. - Vietnamese POS bookkeeping expects recognizable documents: Phiếu thu (receipt), Phiếu chi (payment), Phiếu chuyển khoản (transfer), Phiếu kế toán (adjustment), each with a per-merchant running number.
- COGS and inventory-asset movements need a balanced counter-leg (DEBIT one account, CREDIT another) — impossible with single-sided transactions.
- Account balances must be reconstructable and tamper-evident.
Decision
We will model finance as double-entry: a FinanceVoucher header (type RECEIPT / PAYMENT / TRANSFER / ADJUSTMENT) owns N FinanceTransaction lines, each a DEBIT (100_DEBIT, balance up) or CREDIT (200_CREDIT, balance down) against one FinanceAccount.
Line direction is inferred for RECEIPT (all DEBIT) and PAYMENT (all CREDIT), and explicit + balanced for TRANSFER (Σdebit == Σcredit) and ADJUSTMENT. Numbers are minted per (merchantId, type, yearMonth) via FinanceVoucherSequence with prefixes PT/PC/PCK/PKT. Each posted line snapshots balanceBefore/balanceAfter and a monotonic postingSequence; the account row is FOR UPDATE-locked for the whole post.
Consequences
| Pros | Cons |
|---|---|
| Audit-ready documents with stable per-merchant numbers | More tables + a posting engine to maintain |
| Balanced counter-legs enable COGS / inventory-asset accounting | Callers of TRANSFER/ADJUSTMENT must supply balanced lines |
| Per-line balance snapshot + posting sequence make the ledger tamper-evident | Voucher posting is a multi-row transaction (lock contention under load) |
| Voids become balanced reversals, never destructive edits | Single-currency enforced per voucher (multi-currency deferred) |
Alternatives Considered
| Option | Pros | Cons | Why rejected |
|---|---|---|---|
| Flat single-sided transactions (prior model) | Simplest | No documents, no counter-leg, no COGS support | Cannot represent balanced postings |
| Full general-ledger with chart-of-accounts | Accounting-complete | Overkill for SMB POS; steep operator UX | Too heavy for current scope |
| Event-sourced ledger (append events, project balances) | Strong audit | New infra; balance projection latency | postingSequence + snapshot already gives audit without the rebuild cost |
References
packages/core/src/services/finance/finance-voucher.service.ts(_performIssue,postLines,resolveLineDirection,computeHeaderAmount)packages/core/src/models/schemas/finance/finance-voucher/,finance-transaction/,finance-voucher-sequence/- Domain Model