PRD: Vouchers & posting
| Module | Finance | PRD ID | PRD-VCH-001 |
| Status | Ready to dev | FEAT | VCH |
| Epic | — | Plane | BANA-1497 |
| Date | 2026-05-22 | Version | v0.1 |
| Packages | @nx/finance | URD | VCH |
| Surface | Backend | ||
| Owner | Phát Nguyễn | ||
TL;DR
Every money event becomes a balanced double-entry voucher instead of a loose per-wallet total. Typed accounts (cash, bank, QR, mobile-POS) and internal control accounts are seeded per merchant; a payment integration matrix routes each paid payment to the right account and auto-issues the matching voucher - so sales, purchases, and stock movements post to the books automatically, idempotently, and traceably.
1. Context & Problem
The books must record every money event as a balanced bookkeeping document, not a single per-wallet total. The legacy model held only a number per wallet: it couldn't tell a receipt from a payment, couldn't balance debits against credits, and had no control accounts for inventory value or cost of goods sold. The result was unauditable books and no reliable income/expense reporting - a dead end for the HKD/SME bookkeeping BANA targets.
Why now: the payment refactor needs a place for sale payments to land. A typed account model plus a payment-integration routing layer turns each paid payment into a correctly routed, balanced voucher - the foundation of the Finance module's double-entry model.
2. Goals & Non-Goals
Goals
- Replace the legacy wallet model with typed accounts (cash, bank, QR, mobile-POS) plus internal control accounts, seeded per merchant.
- Record every money event as a balanced voucher with debit/credit ledger lines.
- Drive postings from the payment integration so a paid payment auto-issues the right voucher against the routed account and category.
- Link each voucher to its source, and keep automatic postings idempotent across repeated events.
Non-Goals
- Budget tracking, P&L, and cash-flow forecasting (URD Non-Goals).
- Recurring / scheduled expense automation.
- Receipt-image capture / OCR.
- Multi-currency conversion within a single voucher - single currency, default VND.
3. Success Metrics
| Metric | Target / signal |
|---|---|
| Auto-posting coverage | 100% of succeeded sale payments produce a matching receipt voucher |
| Balance integrity | Every issued voucher balances (debits equal credits where both sides apply); zero unbalanced postings |
| Idempotency | A replayed payment event never creates a second voucher |
| Traceability | Every auto-issued voucher links back to its source document (sale order, PO, stock movement) |
| Account seeding | Every new merchant gets its default and internal control accounts |
4. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Owner | See money events posted as balanced vouchers against the right accounts, with categories applied |
| Accountant / Manager | Trust the books to reflect sales and purchases without manual data entry |
| System (payment integration) | Route a paid payment to the correct account/category and issue the voucher automatically |
Core scenarios: a new merchant is seeded with typed default and control accounts → a sale payment succeeds → the payment integration routes it to the right account and category → a balanced receipt voucher is auto-issued, linked to the source order → a replayed event is ignored (idempotent).
5. User Stories
- As an owner, I want each new merchant to start with its default and internal control accounts, so the books are ready before the first sale.
- As an accountant, I want every money event recorded as a balanced voucher with debit/credit lines, so the books are auditable double-entry, not a running total.
- As an owner, I want a paid sale to auto-issue a receipt voucher against the routed account, so I don't record routine revenue by hand.
- As the system, I want a payment integration matrix to map a payment to the correct account and category, so postings route deterministically.
- As an accountant, I want each voucher linked to its source document, so I can trace a posting back to its order or movement.
- As the system, I want automatic postings to be idempotent, so a repeated payment event never double-posts.
6. Functional Requirements
| # | Requirement | URD ref |
|---|---|---|
| FR-1 | Hold typed money accounts (cash, bank, QR, mobile-POS) plus internal control accounts, owned per merchant | URD-WAL-001 |
| FR-2 | Seed each merchant's default and internal control accounts on merchant creation | URD-WAL-003 |
| FR-3 | Maintain a default account per type per merchant for routing automatic postings | URD-WAL-002 |
| FR-4 | Record money events as vouchers of type receipt, payment, transfer, or adjustment | URD-VCH-001 |
| FR-5 | Every voucher is balanced - debits equal credits where both sides apply | URD-VCH-002 |
| FR-6 | Auto-issue a receipt voucher when a sale payment succeeds, classified as Sale | URD-VCH-003 |
| FR-7 | Auto-issue a payment voucher to the vendor when a purchase order is received | URD-VCH-004 |
| FR-8 | Auto-post cost of goods sold and stock adjustments to internal control accounts on stock movement | URD-VCH-005 |
| FR-9 | Automatic postings are idempotent - a repeated event never double-posts | URD-VCH-006 |
| FR-10 | Link each voucher to its source document (sale order, PO, stock movement, manual) | URD-VCH-007 |
| FR-11 | Number each issued voucher per merchant, per type, per month | URD-VCH-008 |
| FR-12 | Support a manual lifecycle (draft → issue, delete a draft), account transfers, and void-by-reversal | URD-VCH-009..011 |
Full requirement text and acceptance criteria live in the Finance URD. This PRD references them rather than restating them.
7. Non-Functional Requirements
| Area | Requirement |
|---|---|
| Data integrity | A voucher and its ledger lines are written together; every voucher balances (debits equal credits where both sides apply) |
| Idempotency | Automatic postings are keyed off the source event so a replayed event never double-posts |
| Immutability | Issued vouchers are never hard-deleted; corrections happen by balanced reversal (void), preserving the original |
| Tenancy & authz | Accounts and vouchers are confined to the user's own merchant; non-admins see only the merchants they are granted |
| Precision | Monetary math uses 4-decimal precision; single currency per voucher, default VND |
| i18n | User-facing labels/types/categories are bilingual (English and Vietnamese) |
8. UX & Flows
Posting is event-driven, not screen-driven: the merchant takes a payment in the merchant app, and the receipt voucher is issued automatically by the finance posting process. Manual account, voucher (draft → issue, void), and transfer screens also exist in the merchant app.
9. Data & Domain
| Concept | Role |
|---|---|
| Finance account | A typed money account (cash, bank, QR, mobile-POS) or internal control account; holds a running balance |
| Voucher | One balanced bookkeeping document for a money event (receipt, payment, transfer, adjustment), linked to its source |
| Ledger line | A debit or credit row inside a voucher, carrying account, optional category, amount, and balance-before/after snapshot |
| Payment integration | The routing matrix that maps a paid payment to the correct account and category |
| Category | The income/expense classification applied to a voucher |
Conceptual only - full schema, value sets, and invariants in the finance domain model.
10. Dependencies & Assumptions
Depends on
- Payment - a succeeded sale payment is the trigger that routes and posts the receipt voucher.
- Commerce / Merchant - a new merchant gets its default and internal control accounts reconciled automatically.
- Inventory - purchase-order receipts and stock movements drive payment/cost-of-goods vouchers.
- Categories (URD-CAT) - system categories classify auto-generated vouchers.
Assumptions
- The merchant's internal control accounts exist before cost-of-goods postings are attempted.
- Exactly one default account per type per merchant exists for deterministic routing.
- Payment events carry enough context (amount, method, source document) to route and link the voucher.
11. Risks & Open Questions
| Risk / question | Mitigation / status |
|---|---|
| A replayed payment event could double-post | Automatic postings are idempotent - keyed off the source event |
| Cost-of-goods postings before control accounts exist | Control accounts are seeded on merchant creation; posting requires them present |
| Reverting/voiding a voucher tied to an already-settled payment | Void is by balanced reversal preserving the original; compensation path tracked as the feature matures |
| Single-currency only | Out of scope; documented as a constraint (default VND) |
| PO-receipt, stock-movement auto-postings, transfers, and void | Implemented - PO-received posts a vendor payment voucher, stock movements post COGS/inventory-adjustment vouchers, and transfers and void-by-reversal are supported by the voucher engine and screens |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P1 (foundation) - see URD feature catalog |
| Rollout | All merchants; no feature flag |
| Migration | Account model redesign (legacy per-wallet running totals → typed finance accounts); default and internal control accounts seeded per merchant |
| Launch criteria | Paid sale auto-issues a balanced, linked receipt voucher; replayed event does not double-post; new merchant gets its seeded accounts |
| Monitoring | Auto-posting volume and error rate, unbalanced-voucher checks, idempotency-skip counts |
13. FAQ
What replaced the legacy wallet model? A typed finance account model with account types plus internal control accounts (inventory, cost of goods sold), seeded per merchant.
Does a merchant record sales revenue by hand? No - a succeeded sale payment auto-issues a receipt voucher, classified as Sale, against the routed account.
How does a payment know which account to post to? A payment integration matrix routes a paid payment to the correct account and category; the finance posting process then issues the voucher.
What stops a replayed event from double-posting? Automatic postings are idempotent - a repeated event never creates a second voucher.
Can an issued voucher be deleted? No - issued vouchers are voided by a balanced reversal that preserves the original for audit.
References
- URD: Finance - Vouchers & Posting · Accounts
- Builds on: Categories · Ledger Lines
- Module: Finance - URD
- Developer: @nx/finance · domain model