# ADR 0002: Canonical ownership of the payment path - **Status:** Accepted - **Date:** 2026-07-27 - **Review item:** 1.3, 1.4 (Phase 1, `NIGIG_PAY_CONSOLIDATED_REVIEW.md`) - **Related:** ADR 0001 ## Context Review item A5 described a "fork farm": three copies each of the SMS parser, the transaction store, the pending store and the payment flow handler, spread across `nigig-core`, `nigig-pay`, `nigig-pay-ui` and `nigig-mpesa`. The stated consequence was semantic drift — one caller writing a record another never reads. That table is now **stale**. Re-verified at this commit: | Component | Copies the review claimed | Copies today | Location | |---|---|---|---| | `parser.rs` (M-Pesa SMS) | 3 | **1** | `nigig-core/src/persistence/mpesa/` | | `store.rs` | 3 | **1** | `nigig-core/src/persistence/mpesa/` | | `pending_store.rs` | 3+ | **1** | `nigig-core/src/persistence/mpesa/` | | `pay_flow_handler.rs` | 3 | **1** | `nigig-pay-ui/src/pay_flow/` | | `classifier.rs` | — | **1** | `nigig-core/src/persistence/mpesa/` | (The second `parser.rs` in the tree belongs to `pdf-cos` and is unrelated.) The duplicates were collapsed by earlier upstream work. What remains is not de-duplication but **ownership**: saying which crate owns which concern, and making that boundary machine-checked so the fork farm cannot regrow. ## Decision The payment path has four layers, with strictly one-way dependencies: ``` nigig-pay-domain pure types, state machine, validation, evidence, reconciliation, fee policy. No I/O, no platform, no UI. ▲ nigig-pay-storage SQLite repositories, migrations, audit trail, encryption at rest. Depends on domain only. ▲ nigig-pay-platform (not yet created) trait implementations for Android: gateway, biometrics, SMS source, key provider. ▲ nigig-pay-ui Makepad widgets. Presentation only. Owns no transaction nigig-pay state and calls no platform API directly. ``` Rules: 1. **`nigig-pay-domain` is the only owner of payment semantics.** Money arithmetic, state transitions, validation, fee policy, evidence handling and reconciliation live here and nowhere else. 2. **`nigig-pay-storage` is the only owner of payment persistence.** No other crate may open a payment database or write a payment record. 3. **Neither may depend on a UI framework.** Enforced in CI by a Makepad import check and by `deny.toml`, which bans the `makepad-*` crates from the dependency graph outright. 4. **`nigig-core`'s `persistence/mpesa/` remains the tracker-side owner** of observed M-Pesa history — the parser, classifier and history store. It is the *observation* path, not the *payment* path, which is consistent with ADR 0001. It must not grow payment-initiation logic. 5. **`nigig-pay-ui` owns presentation only.** Its surviving `pay_flow_handler.rs` thread-local is scheduled for replacement by `PaymentCoordinator` under Phase 6. ## Known violations, deliberately not yet fixed These are recorded rather than silently tolerated. All live in `nigig-core/src/persistence/mpesa/`, which is currently commented out of the workspace as broken (`depends on robius-directories`), so changes there cannot be compiled or tested in a standalone checkout. - **B6 — `f64` money.** Nine `f64` sites in the observation path, including `MpesaTransaction::amount`. Must migrate to `nigig_pay_domain::Money`. *Partially addressed.* The UI accumulators now use exact `PeriodSummary` arithmetic, and the persistence boundary validates: a non-finite, negative or beyond-`2^53` amount is refused on parse, on load and on save, rather than becoming a silent `KSh 0` or a `NaN` that poisons every total it enters. What remains is the stored representation itself, which is a data migration and belongs with the SQLite move. - **B1 — classification discarded.** `store.rs` parses `category`, `sub_category`, `status` and `confidence` from disk and then overwrites them with `Default::default()`, so every user categorisation is lost on reload. - **B4 — lossy PSV escaping.** `clean`/`restore` are not bijective: `"JOHN~DOE"` round-trips to `"JOHN|DOE"`. Each is a one-file fix, blocked only on `nigig-core` becoming buildable. ## Consequences - New payment logic has exactly one correct home, and a reviewer can point at this ADR when it appears elsewhere. - The domain/UI boundary is enforced by tooling, not convention, so it cannot erode quietly. - The observation path keeps its own types for now. Unifying it with `Money` is tracked as B6 rather than pretended to be done.