# Multi-Wallet Capability Matrix

Date: 2026-03-30
Status: approved
Reference: docs/multi-wallet/deliverables/multiwallet-requirements-spec.md

## Priority Legend
- **P0**: Must-have for first wave. Blocks launch.
- **P1**: Important but can ship in a fast-follow increment.
- **P2**: Nice-to-have. Deferred to future wave.

## 1. Wallet Product Lifecycle (10 capabilities)

| ID | Capability | Priority | Acceptance Criteria | Phase |
|---|---|---|---|---|
| WP-01 | Create wallet product | P0 | Given a user_id, product_type, label, and primary_currency, a WalletProduct is created with status :pending and a linked Account is opened. A default SubWallet (purpose: :general) is created automatically. WalletProductCreated event is emitted. | 4 |
| WP-02 | List wallet products for user | P0 | Given a user_id, returns all wallet products with their status, label, product_type, and primary_currency. Ordered by created_at desc. | 4 |
| WP-03 | Get wallet product details | P0 | Given a wallet_product_id, returns the wallet product with all sub-wallets, aggregate balance, and currency configs. | 4 |
| WP-04 | Update wallet product metadata | P0 | Given a wallet_product_id, update label and/or metadata map. Only allowed when status is :active or :suspended. WalletProductUpdated event emitted. | 4 |
| WP-05 | Freeze wallet product | P0 | Given a wallet_product_id, reason, and actor_id, transitions to :frozen. All sub-wallets cascade to :frozen. WalletProductFrozen event emitted per entity. Rejects if already frozen or closed. | 4 |
| WP-06 | Unfreeze wallet product | P0 | Given a wallet_product_id and actor_id, transitions to :active. Sub-wallets cascade-frozen by this product are unfrozen. Independently frozen sub-wallets stay frozen. WalletProductUnfrozen event emitted. | 4 |
| WP-07 | Suspend wallet product | P1 | Given a wallet_product_id, reason, and actor_id, transitions to :suspended. Sub-wallets are NOT cascaded. WalletProductSuspended event emitted. | 4 |
| WP-08 | Resume wallet product | P1 | Given a wallet_product_id and actor_id, transitions from :suspended to :active. WalletProductResumed event emitted. | 4 |
| WP-09 | Close wallet product | P0 | Given a wallet_product_id, reason, and actor_id. Requires: all sub-wallets are closed AND aggregate balance is zero. Transitions to :closed (terminal). WalletProductClosed event emitted. | 4 |
| WP-10 | Multiple same-type per user | P0 | Creating a second wallet product with the same product_type for the same user succeeds (no uniqueness constraint on user_id + product_type). | 4 |

## 2. Sub-wallet Lifecycle (10 capabilities)

| ID | Capability | Priority | Acceptance Criteria | Phase |
|---|---|---|---|---|
| SW-01 | Create sub-wallet | P0 | Given a wallet_product_id, label, and purpose, creates a SubWallet with status :active. Currency inherits from wallet product primary_currency. SubWalletCreated event emitted. Rejects if wallet product is frozen or closed. | 4 |
| SW-02 | List sub-wallets for product | P0 | Given a wallet_product_id, returns all sub-wallets with status, label, purpose, and balance. Ordered by created_at. | 4 |
| SW-03 | Get sub-wallet details | P0 | Given a sub_wallet_id, returns the sub-wallet with current balance, parent wallet product info. | 4 |
| SW-04 | Update sub-wallet metadata | P1 | Given a sub_wallet_id, update label and/or purpose. Only when status is :active. SubWalletUpdated event emitted. | 4 |
| SW-05 | Freeze sub-wallet | P0 | Given a sub_wallet_id, reason, and actor_id. Transitions to :frozen. Not allowed on default sub-wallet unless parent is frozen. SubWalletFrozen event emitted. | 4 |
| SW-06 | Unfreeze sub-wallet | P0 | Given a sub_wallet_id and actor_id. Transitions to :active. Rejects if parent wallet product is frozen. SubWalletUnfrozen event emitted. | 4 |
| SW-07 | Close sub-wallet | P0 | Given a sub_wallet_id, reason, and actor_id. Requires zero balance. Not allowed on default sub-wallet. Transitions to :closed (terminal). SubWalletClosed event emitted. | 4 |
| SW-08 | Transfer within wallet product | P0 | Given source_sub_wallet_id, target_sub_wallet_id (same wallet product), amount. Creates linked debit+credit ledger entries with shared reference_id. SubWalletTransferCompleted event emitted. Rejects if source has insufficient balance, either sub-wallet is frozen/closed. | 5 |
| SW-09 | Transfer across wallet products | P1 | Same as SW-08 but source and target sub-wallets belong to different wallet products of the same user. Currency must match or FX conversion applied. | 5 |
| SW-10 | Transfer cross-user | P2 | Sub-wallet-to-sub-wallet transfer across different users. Uses existing transfer state machine with sub-wallet resolution. | 5 |

## 3. Multi-Currency Configuration (5 capabilities)

| ID | Capability | Priority | Acceptance Criteria | Phase |
|---|---|---|---|---|
| MC-01 | Set primary currency | P0 | Primary currency is set at wallet product creation time. Cannot be changed after creation. One-primary invariant enforced. | 4 |
| MC-02 | Add display-only currency | P1 | Given a wallet_product_id and currency_code, add a CurrencyConfig with classification :display_only and an initial display_rate. Rejects duplicate currency_code per wallet product. | 4 |
| MC-03 | Update display rate | P1 | Given a config_id, update the display_rate and rate_source. Only for :display_only configs. | 4 |
| MC-04 | Disable/enable display currency | P2 | Given a config_id, toggle enabled flag. Does not affect balances, only display. | 4 |
| MC-05 | One-primary invariant | P0 | Any mutation that would result in zero or more than one :primary CurrencyConfig is rejected. Verified by constraint in store and persistence layer. | 4 |

## 4. Ledger and Transaction Integration (8 capabilities)

| ID | Capability | Priority | Acceptance Criteria | Phase |
|---|---|---|---|---|
| LT-01 | Credit to sub-wallet | P0 | PostJournalEntry accepts sub_wallet_id. Credit amount is atomically added to the sub-wallet's balance in LedgerStore. Ledger entry records sub_wallet_id in metadata or dedicated field. | 5 |
| LT-02 | Debit from sub-wallet | P0 | Debit checks balance at sub-wallet level (not just account level). Rejects if sub-wallet balance < amount. | 5 |
| LT-03 | Sub-wallet transfer posting | P0 | Two linked ledger entries (debit source, credit target) with shared reference_id and sub_wallet_id references. Atomic execution. | 5 |
| LT-04 | Freeze-state posting guard | P0 | Posting pipeline rejects any debit/credit to a frozen or closed sub-wallet. Returns error :sub_wallet_frozen or :sub_wallet_closed. | 5 |
| LT-05 | Balance query by sub-wallet | P0 | LedgerStore.get_sub_wallet_balance(sub_wallet_id, currency) returns current balance. | 5 |
| LT-06 | Aggregate balance by product | P0 | Sum of all sub-wallet balances for a wallet product. Computed query, not stored. | 5 |
| LT-07 | FX conversion records | P2 | When displaying balance in a display-only currency, an FX conversion record is written for audit. | 5 |
| LT-08 | Idempotency enforcement | P0 | All mutating ledger operations require an idempotency key. Duplicate requests return original result. | 5 |

## 5. API and Channel Integration (7 capabilities)

| ID | Capability | Priority | Acceptance Criteria | Phase |
|---|---|---|---|---|
| API-01 | Wallet product REST endpoints | P0 | POST/GET/PUT /api/v1/wallet-products, POST .../freeze, POST .../unfreeze, POST .../close. Standard error envelope. | 6 |
| API-02 | Sub-wallet REST endpoints | P0 | POST/GET/PUT /api/v1/wallet-products/:id/sub-wallets, freeze/unfreeze/close. | 6 |
| API-03 | Sub-wallet transfer endpoint | P0 | POST /api/v1/sub-wallet-transfers with idempotency key header. | 6 |
| API-04 | Currency config endpoints | P1 | POST/GET/PUT /api/v1/wallet-products/:id/currencies. | 6 |
| API-05 | Customer LiveView | P1 | Wallet switcher in customer nav. Sub-wallet list with balance. Transfer between sub-wallets form. | 6 |
| API-06 | Admin LiveView | P1 | Wallet product inquiry. Operational freeze/unfreeze/close. Sub-wallet drill-down. | 6 |
| API-07 | Backward compatibility | P0 | Existing /api/v1/wallet/:userId/* endpoints resolve to default wallet product's default sub-wallet. No client changes required. | 6 |

## 6. Eventing, Observability, and Compliance (6 capabilities)

| ID | Capability | Priority | Acceptance Criteria | Phase |
|---|---|---|---|---|
| EC-01 | Wallet product lifecycle events | P0 | Events: WalletProductCreated, WalletProductFrozen, WalletProductUnfrozen, WalletProductSuspended, WalletProductResumed, WalletProductClosed. All implement DomainEvent behaviour. | 7 |
| EC-02 | Sub-wallet lifecycle events | P0 | Events: SubWalletCreated, SubWalletFrozen, SubWalletUnfrozen, SubWalletClosed. | 7 |
| EC-03 | Sub-wallet transfer events | P0 | Events: SubWalletTransferInitiated, SubWalletTransferCompleted. Linked by reference_id. | 7 |
| EC-04 | Audit events for sensitive actions | P0 | Freeze, unfreeze, close, transfer emit AuditEvent with actor_id, resource_id, outcome, correlation_id. | 7 |
| EC-05 | Telemetry spans | P1 | :telemetry.span for create_wallet_product, sub_wallet_transfer, freeze_wallet_product. Duration + metadata. | 7 |
| EC-06 | Compliance control checks | P1 | Freeze/close/transfer pass through ControlCatalog checks. AML/KYC status validated for new wallet products. | 7 |

## Summary

| Domain | P0 | P1 | P2 | Total |
|---|---|---|---|---|
| Wallet Product | 7 | 2 | 0 | 9 |
| Sub-wallet | 7 | 2 | 1 | 10 |
| Multi-currency | 2 | 2 | 1 | 5 |
| Ledger/Txn | 6 | 0 | 2 | 8 |
| API/Channel | 4 | 3 | 0 | 7 |
| Eventing/Compliance | 4 | 2 | 0 | 6 |
| **Total** | **30** | **11** | **4** | **45** |
