PRD: Vouchers & posting
| Module | Finance (CORE-12) | PRD ID | PRD-VCH-001 |
| Status | In-progress | Owner | Finance squad |
| Date | 2026-05-22 | Version | v0.1 |
| Packages | @nx/finance · @nx/core · @nx/commerce | URD | VCH · WAL |
TL;DR
Lets a merchant record every money event as a balanced double-entry voucher instead of a loose running total per wallet. Typed money accounts (cash, bank, QR, mobile-POS) plus internal control accounts are seeded per merchant, and a payment integration matrix routes a paid payment to the right account and auto-issues the correct voucher. The result: sales, purchases, and stock movements post into the books automatically, idempotently, and with a link back to their source document.
1. Context & Problem
A merchant's books must record every money event as a balanced bookkeeping document - not a single running total mutated per wallet. The legacy finance-wallet model could only hold a number per wallet: it could not express a receipt vs. a payment, could not balance debits against credits, and offered no internal control accounts for inventory value or cost of goods sold. That makes the books unauditable and blocks any reliable income/expense reporting for the HKD/SME bookkeeping KICKO targets.
Why now: the payment refactor needs a place for sale payments to land in the books. A typed account schema plus a payment integration routing layer turns each paid payment into a correctly-routed, balanced voucher. This is the foundation of the Finance module's double-entry model.
2. Goals & Non-Goals
Goals
- Replace the legacy wallet model with a typed account schema (cash, bank, QR, mobile-POS) plus internal control accounts, seeded per merchant.
- Record money events as balanced vouchers with debit/credit ledger lines through the voucher service.
- Drive postings from the payment integration so a paid payment auto-issues the correct voucher against the routed account and category.
- Link each voucher to its source document and keep automatic postings idempotent under 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 merchant-scoped (x-merchant-id); non-admins see only granted merchants' accounts |
| Precision | Monetary math uses float(value, 4); single currency per voucher, default VND |
| i18n | User-facing labels/types/categories are bilingual ({ en, vi }) |
8. UX & Flows
Posting is event-driven, not screen-driven: the merchant takes a payment in apps/client, and the receipt voucher is issued automatically by the finance worker. Manual account/voucher and transfer screens are emerging in P2.
9. Data & Domain
| Entity | 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 |
LedgerLine | 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, enums, and invariants in the finance domain model.
10. Dependencies & Assumptions
Depends on
- Payment (
@nx/corepayment integration) - a succeeded sale payment is the trigger that routes and posts the receipt voucher. - Commerce / Merchant (
@nx/commerce) - a new merchant gets its default and internal control accounts reconciled automatically. - Inventory (
@nx/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 are still maturing | Tracked under the same VCH feature as they land; account redesign + payment-driven posting are the foundation |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P1 (foundation) - see URD feature catalog |
| Rollout | All merchants; no feature flag |
| Migration | Account schema redesign (finance-wallet → finance-account); 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 schema with account-type constants 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 worker 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 - overview + traceability
- Developer: @nx/finance · domain model