PRD: Wallets, vouchers & ledger
| Module | Payment & Transaction (CORE-08) | PRD ID | PRD-WAL-001 |
| Status | Shipped | Owner | Payment & Finance squad |
| Date | 2026-05-28 | Version | v1.0 |
| Packages | @nx/finance · @nx/payment · @nx/ledger | URD | WAL · VCH · CAT |
TL;DR
Gives a merchant a place to record the money behind every payment: accounts (wallets) where cash, bank, QR, and mobile-POS balances sit, balanced double-entry vouchers for every movement, and auto-posting that books completed sales, received purchase orders, and stock movements into a finance ledger exactly once each. The result: an owner can see real balances and reconcile their books, instead of only knowing a payment was marked paid.
1. Context & Problem
KICKO can move a payment to paid, but it has nowhere to record the resulting money. There is no place for cash, bank, or QR balances to sit, no double-entry bookkeeping, and no way to classify income and expense - so a merchant can collect a payment yet cannot see a balance or reconcile their books. Cost tracking and owner-level financial visibility are blocked, which is a hard requirement for the HKD/SME bookkeeping KICKO targets.
This feature builds the finance side on top of the existing payment lifecycle: accounts where money sits, balanced vouchers for every movement, seeded income/expense categories, and auto-posting that connects sale, purchase, and inventory events to the ledger. "Account" is the canonical term for a place money sits, superseding the earlier "wallet" naming.
2. Goals & Non-Goals
Goals
- Finance accounts of type CASH, BANK, QR code, and mobile POS, each with an opening and running balance, with internal control accounts maintained automatically and one default account per merchant for auto-posted sale income.
- Balanced double-entry vouchers in four types - RECEIPT, PAYMENT, TRANSFER, ADJUSTMENT - with a
DRAFT → ISSUED → VOIDEDlifecycle, per-merchant/per-type numbering, and void-by-reversal (no hard delete). - Auto-posting from business events: a completed sale payment posts a RECEIPT, a received purchase order posts a PAYMENT, and a stock issue/adjustment posts the matching voucher - each idempotent on its source event id.
- Income/expense category classification: 14 seeded system categories (protected, typed INCOME or EXPENSE) plus merchant-specific custom categories with a parent hierarchy.
Non-Goals
- Structured refund / reversal back through the original payment provider (URD §7 - Planned).
- Per-shift cash reconciliation dashboard (URD §7 - Planned).
- Multi-currency conversion and cross-currency reconciliation.
- Tax invoice issuance - owned by the Tax & Invoice module.
3. Success Metrics
| Metric | Target / signal |
|---|---|
| Ledger coverage | 100% of completed sale payments, received POs, and stock movements post a matching voucher |
| Balance integrity | Every voucher balances (DEBIT total = CREDIT total); zero unbalanced entries |
| Idempotency | Each source event posts at most one voucher, even on redelivery - zero duplicates |
| Reconciliation | Owners can read live account balances that match the sum of posted ledger lines |
4. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Owner | See real balances, classify income/expense, reconcile the books, void mistakes |
| Manager | Manage accounts, create manual vouchers, review the ledger |
| Cashier | Collect payments that auto-post income (no ledger access) |
Core scenarios: an owner creates the accounts where money sits → a completed sale payment auto-posts a balanced RECEIPT to the default account → a received PO or stock movement auto-posts the matching voucher once → the owner reviews the ledger, classifies entries by category, and voids any mistake via a balanced reversal.
5. User Stories
- As an owner, I want to create accounts of type cash, bank, QR, and mobile POS with an opening balance, so money has a place to sit and I can see a running balance.
- As an owner, I want a completed sale payment to auto-post a balanced RECEIPT voucher to my default account, so income appears in the ledger without manual entry.
- As an owner, I want a received purchase order and a stock issue/adjustment to auto-post the matching voucher exactly once, so expenses and stock movements reconcile automatically.
- As an owner, I want to create a manual voucher (draft → issue) and void it by reversal, so I can correct the books without ever deleting financial history.
- As an owner, I want income and expense classified by category, with custom categories under a parent, so I can report on where money came from and went.
- As a manager, I want to see only the accounts of merchants I am granted access to, so visibility respects my scope.
6. Functional Requirements
| # | Requirement | URD ref |
|---|---|---|
| FR-1 | Create accounts of type CASH, BANK, QR code, mobile POS; each tracks an opening and running balance | URD-WAL-001 · URD-WAL-003 |
| FR-2 | Maintain internal control accounts (e.g. inventory, COGS) automatically | URD-WAL-002 |
| FR-3 | One default account per merchant receives auto-posted sale income | URD-WAL-004 |
| FR-4 | Non-owner roles see only accounts of merchants they are granted access to | URD-WAL-005 |
| FR-5 | Every voucher is balanced (DEBIT total = CREDIT total) in four types RECEIPT / PAYMENT / TRANSFER / ADJUSTMENT | URD-VCH-001..002 |
| FR-6 | A completed sale payment auto-posts a RECEIPT to the default account; a received PO auto-posts a PAYMENT; a stock issue/adjustment auto-posts the matching voucher | URD-VCH-003..005 |
| FR-7 | Each voucher records its source event so the same event posts only once (idempotent) | URD-VCH-006 |
| FR-8 | Vouchers are numbered per merchant and per type (e.g. PT / PC / PCK / PKT) | URD-VCH-007 |
| FR-9 | An owner can create a manual voucher (draft → issue) and void via a balanced reversal (no hard delete) | URD-VCH-008..009 |
| FR-10 | A transfer between two accounts is recorded as a zero-sum voucher | URD-VCH-010 |
| FR-11 | 14 system categories are seeded, protected, and typed INCOME or EXPENSE; an owner can add custom categories with a parent hierarchy | URD-CAT-001..003 |
Full requirement text and acceptance criteria live in the Payment & Transaction URD. This PRD references them rather than restating them.
7. Non-Functional Requirements
| Area | Requirement |
|---|---|
| Data integrity | Every voucher must balance (DEBIT total = CREDIT total); account running balances always equal the sum of their posted ledger lines |
| Idempotency | Each business event posts at most one voucher, keyed on the source event id; redelivery has no second effect |
| Immutability | No hard delete of financial history - corrections happen via balanced reversal vouchers |
| Tenancy & authz | All accounts, vouchers, and categories scoped per merchant (x-merchant-id); non-owner visibility limited to granted merchants |
| Precision | Monetary math uses float(value, 4) |
| i18n | User-facing labels/types/statuses are bilingual ({ en, vi }) |
8. UX & Flows
Key screens (in apps/client): account list and create, the voucher view (editable transaction table, party/transaction sections, void banner, overview), and the ledger overview with balances and voucher history.
9. Data & Domain
| Entity | Role |
|---|---|
FinanceAccount | A place money sits - type CASH / BANK / QR_CODE / MOBILE_POS, opening + running balance; includes internal control accounts |
FinanceVoucher | A balanced bookkeeping entry of one type (RECEIPT / PAYMENT / TRANSFER / ADJUSTMENT) with a DRAFT → ISSUED → VOIDED lifecycle and source event id |
| Ledger line (transaction) | One DEBIT or CREDIT line within a voucher, affecting one account's balance, optionally classified by category |
FinanceCategory | An income/expense label - 14 protected system categories plus custom ones with a parent hierarchy |
| Voucher sequence | Per-merchant, per-type numbering source |
| Ledger snapshot / job | Snapshot entries and the ledger job queue (API + worker split, progress over WebSocket) |
Conceptual only - full schema and invariants in the finance domain model.
10. Dependencies & Assumptions
Depends on
- Payment lifecycle (URD-PAY) - a completed payment is what triggers the RECEIPT auto-posting.
- Orders (Orders module) - the sale forwards finance metadata on
payment.success. - Inventory (Inventory module) - purchase-order receipt and stock issue/adjustment events drive PAYMENT / ADJUSTMENT postings.
@nx/ledger- ledger snapshot and job machinery (API/worker split).
Assumptions
- Each merchant has a default account designated to receive auto-posted sale income.
- The 14 system categories are seeded when a merchant's finance ledger initializes.
- Business events carry a stable source event id for idempotency.
11. Risks & Open Questions
| Risk / question | Mitigation / status |
|---|---|
| Duplicate postings if an event is redelivered | Each voucher records its source event id; posting is idempotent |
| An unbalanced voucher could corrupt the ledger | Enforced invariant: DEBIT total must equal CREDIT total before issue |
| Voiding vs. preserving financial history | Void is a balanced reversal; the original voucher is never hard-deleted |
| No multi-currency support | Out of scope; single currency per voucher documented as a constraint |
| Reversing finance against an already-reversed source (e.g. PO revert) | Open: define the compensation path across modules |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P2 (finance ledger) - see URD feature catalog |
| Rollout | All merchants; no feature flag |
| Migration | None for data; finance accounts and 14 system categories seeded at merchant init |
| Launch criteria | Auto-post from sale/purchase/inventory verified idempotent; every voucher balances; running balances match summed ledger lines; manual create + void-by-reversal verified |
| Monitoring | Voucher volume per merchant, auto-post error rate, balance-vs-ledger consistency checks, idempotency-collision count |
13. FAQ
Where does the money go when a sale is paid? A completed sale payment auto-posts a balanced RECEIPT voucher to the merchant's default account - no manual entry needed.
Can a voucher be deleted if it's wrong? No. Financial history is never hard-deleted; an issued voucher is corrected by a balanced reversal that voids it while preserving the original.
What stops the same event from posting twice? Each voucher records its source event id; if the event is redelivered, no second voucher is created.
What's the difference between an account and a category? An account is where money sits (cash, bank, QR, mobile POS); a category classifies what kind of income or expense a movement is.
Can I rename or remove the seeded categories? The 14 system categories are protected and cannot be removed. You can add merchant-specific custom categories, optionally under a parent.
Does this issue tax invoices? No - tax invoice issuance is owned by the Tax & Invoice module.
References
- URD: Payment & Transaction - Accounts & Wallets · Vouchers & Ledger · Categories
- Builds on: Payment Lifecycle
- Module: Payment & Transaction - overview + traceability
- Developer: @nx/finance · @nx/payment · @nx/ledger · finance domain model