PRD: Finance categories
| Module | Finance (CORE-12) | PRD ID | PRD-CAT-001 |
| Status | Shipped | Owner | Finance squad |
| Date | 2026-05-25 | Version | v1.0 |
| Packages | @nx/finance · apps/client | URD | CAT |
TL;DR
Lets a merchant read its books by income vs expense and by reason (Sale, Purchase, Rent, Salary…). A fixed catalog of platform-owned system categories is applied automatically to every auto-generated voucher, while merchants can add their own categories nested under a parent through a management screen. The result: every money movement carries a consistent, reportable classification - no unlabelled vouchers, no ad-hoc tagging.
1. Context & Problem
Every money movement in Finance needs a classification so owners can read their books by income vs expense and by reason (Sale, Purchase, Rent, Salary…). Without a category catalog, auto-generated vouchers - receipts on paid sales, payments on purchase-order receipt - have no consistent label, and merchants cannot group or report their movements. This blocks the basic bookkeeping read that HKD/SME owners expect.
This increment delivers the category axis of the double-entry voucher model: a fixed set of categories the platform owns and applies automatically, plus the ability for a merchant to add its own categories nested under a parent, surfaced through a management screen in the client.
2. Goals & Non-Goals
Goals
- Provide a fixed catalog of system categories spanning income and expense, owned by the platform (not merchant-scoped).
- Type each category as income or expense.
- Apply a system category automatically to auto-generated vouchers (Sale, Purchase, etc.) via stable fixed identifiers.
- Let merchants add custom categories nested under a parent, managed via a client screen with live preview, parent picker, and icon/badge display.
Non-Goals
- Budget tracking and variance (URD Non-Goal).
- Recurring / scheduled expense automation (URD Non-Goal).
- Profit & loss and cash-flow forecasting (URD Non-Goal).
- The voucher and ledger-line posting mechanics themselves - owned by areas
VCHandTXN.
3. Success Metrics
| Metric | Target / signal |
|---|---|
| Classification coverage | 100% of auto-generated vouchers carry a system category |
| Catalog stability | Re-running the seeder never duplicates a system category (idempotent by identifier) |
| Custom adoption | Merchants who add at least one custom category trends up |
| Reporting readiness | Movements group cleanly by income/expense and by category in reports |
4. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Owner | Read the books by income vs expense and by reason; tailor categories to the business |
| Accountant / Manager | Classify movements consistently for reporting |
| Platform | Apply a stable, known category to every automatic posting |
Core scenarios: the platform seeds a fixed catalog at startup → auto-generated vouchers resolve a stable system category by fixed identifier → an owner adds custom categories nested under a parent through the management screen → all movements report by income/expense and by category.
5. User Stories
- As an owner, I want money movements classified as income or expense, so that I can read my books at a glance.
- As an owner, I want auto-generated vouchers to carry a consistent system category, so that sales and purchases group correctly without manual tagging.
- As an owner, I want to add my own categories nested under a parent, so that classification fits how my business thinks about money.
- As an owner, I want a management screen with a live preview and icon picker, so that creating and editing categories is clear.
- As the platform, I want a fixed set of system categories the seeder can re-run safely, so that automatic postings always resolve a stable category.
6. Functional Requirements
| # | Requirement | URD ref |
|---|---|---|
| FR-1 | Provide a fixed catalog of seeded system categories spanning income and expense, platform-owned (merchantId: null); the seeder is idempotent (create-or-update by identifier/type) | URD-CAT-001 |
| FR-2 | Each category is typed INCOME or EXPENSE | URD-CAT-002 |
| FR-3 | System categories are keyed by stable fixed identifiers (SALE, PURCHASE, REFUND_*, …) so auto-generated vouchers resolve a stable system category automatically | URD-CAT-003 |
| FR-4 | Merchants may add custom categories nested under a parent, with icon metadata | URD-CAT-004 |
| FR-5 | Merchant-scoped CRUD over categories (x-merchant-id), authorized via finance category permissions | URD-CAT-004 |
| FR-6 | A client management screen for create / edit / list, with name input, parent picker, live preview, badge + icon set, and bilingual labels | URD-CAT-004 |
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 | System categories carry stable fixed identifiers; auto-posting resolves by identifier, never by name |
| Idempotency | The seeder is create-or-update by identifier/type - re-running never duplicates a system category |
| Tenancy & authz | System categories are platform-owned (merchantId: null); custom categories are merchant-scoped (x-merchant-id) and permission-gated |
| Performance / scale | Category catalog is small and cacheable; resolution by fixed identifier is a constant-time lookup |
| i18n | Category names and screen labels are bilingual ({ en, vi }) |
8. UX & Flows
Key screens (in apps/client): category list, create, and edit - with name input, parent picker, live preview, and a category badge + icon set.
9. Data & Domain
| Entity | Role |
|---|---|
FinanceCategory | The category record - type (income/expense), parentId (nesting), icon metadata, merchantId (null = platform-owned) |
FixedFinanceCategories | Stable fixed identifiers (SALE, PURCHASE, REFUND_*, …) used by auto-posting to resolve a system category |
FinanceCategoryTypes | Income / expense type enum |
| Category seeder | Idempotent startup process that seeds the fixed system catalog (finance-0001-seed-categories.ts) |
Conceptual only - full schema and invariants in the finance domain model.
10. Dependencies & Assumptions
Depends on
- Vouchers & Posting (URD-VCH) - auto-generated vouchers consume categories.
- Ledger Lines (URD-TXN) - a ledger line carries the optional category.
@nx/finance- the seeder, schema, and merchant-scoped controller live here.
Assumptions
- The fixed system categories cover the income/expense reasons automatic postings emit (Sale, Purchase, refunds, etc.).
- A merchant exists before creating custom merchant-scoped categories.
11. Risks & Open Questions
| Risk / question | Mitigation / status |
|---|---|
| Auto-posting could fail to resolve a category | Resolution is by stable fixed identifier, seeded idempotently at startup |
| Re-running the seeder could duplicate categories | Seeder is create-or-update by identifier/type - never duplicates |
| Deep custom-category nesting could complicate reports | Single parent level is the documented model; revisit if multi-level demand appears |
| Renaming a system category by hand | System categories are platform-owned, not merchant-editable |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P1 (foundation) - see URD feature catalog |
| Rollout | All merchants; no feature flag |
| Migration | None for merchants; the fixed system catalog is seeded at startup (idempotent) |
| Launch criteria | 14 system categories seeded with correct income/expense typing; auto-generated vouchers resolve a system category by fixed identifier; custom category create/edit/list works merchant-scoped |
| Monitoring | Auto-posting category-resolution failures; custom-category create rate; seeder idempotency on restart |
13. FAQ
Are system categories editable by a merchant? No - the fixed system categories are platform-owned (merchantId: null) and applied automatically; merchants add their own custom categories instead.
How does an auto-generated voucher pick a category? By a stable fixed identifier (SALE, PURCHASE, REFUND_*, …), so the same event always resolves the same system category.
Can a custom category be nested? Yes - a custom category nests under a parent, with icon metadata for display.
Does running the seeder twice create duplicates? No - the seeder is idempotent (create-or-update by identifier/type).
What classifies as income vs expense? Each category is typed income or expense; the seeded catalog covers both (e.g. Sales Revenue, Refund Received as income; Purchase, Rent & Utilities, Salaries as expense).
References
- URD: Finance - Categories
- Related PRDs: Vouchers & Posting · Accounts & Ledger
- Module: Finance - overview + traceability
- Developer: @nx/finance · domain model