DemozPay — Domain Completeness Matrix
Snapshot: 2026-05-29 Companion to:
REAL_SYSTEM_STATE.md(narrative version of the same findings).Single-table view of every domain × every capability. Read row-by-row. Read also column-by-column — gaps in a single column across rows reveal cross-cutting weaknesses.
Tag glossary
- L — LIVE end-to-end. Tested. Safe to depend on.
- P — PARTIAL. One layer real, one layer missing.
- S — STUB. Boots; does nothing useful.
- N — PLANNED. Not implemented; design only.
- B — BLOCKED. Cannot land without something else first.
- D — DANGEROUS. Implementation exists but is incorrect / insecure / model-violating.
- n/a — does not apply to this domain.
Capability columns
- Domain pkg. — does
packages/<domain>/exist? - Schema — Prisma model + migrations + tenancy + santim semantics.
- Origination — request / quote intake.
- Eligibility — underwriting / accrual / income check.
- Approval — workflow to move PENDING → APPROVED.
- Disbursement — money out via gateway.
- Settlement — bank-side confirmation handling.
- Repayment — money back via payroll / direct debit / etc.
- Collections — overdue + escalation + write-off.
- Default — terminal "this is uncollectable" path.
- Reversal — corrective ledger entries.
- Reconciliation — bank-vs-ledger matching for this domain's transfers.
- Audit — entries land in same tx as state change.
- RBAC — who can do what.
- Notification — customer-facing comms.
Headline matrix
| Domain | Pkg | Schema | Origin | Elig | Apprv | Disb | Settle | Repay | Coll | Default | Rev | Recon | Audit | RBAC | Notif |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| EWA | L | L | L | P | L | L | L | L (B1, admin) | N | n/a | P | L (primitive) | L | L (A2) | N |
| Lending | L | L | L | L | L | L | L | L (B1/B2, admin) | N | D | P | L (primitive) | L | L (A2) | N |
| BNPL | N | D | N | N | N | N | N | N | N | N | N | N | N | n/a | N |
| Payroll | N | D | N | n/a | n/a | N | N | n/a | n/a | n/a | n/a | n/a | N | D | N |
| Savings | N | D | N | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | N | D | N |
| Equb | N | D | N | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | N | D | N |
| KYC | N | n/a | N | N | N | n/a | n/a | n/a | n/a | n/a | n/a | n/a | N | D | N |
| Wallet | n/a (intentional) | D (legacy) | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a |
| Risk | N | n/a | N | N | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | N | D | n/a |
| Fraud | N | n/a | N | N | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | N | D | n/a |
| Collections | N | n/a | N | n/a | n/a | n/a | n/a | n/a | N | N | n/a | n/a | N | D | N |
| Compliance | N | n/a | N | N | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | N | D | N |
| Audit (domain) | shared/audit | L | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | L | D | n/a |
| Notifications | N (service stub) | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | S |
| Reporting | N | n/a | N | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | N | D | N |
| Reconciliation (svc) | services/integration-gateway/internal/recon | L | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | L (primitive) | n/a | n/a | n/a |
| Integration Gateway (svc) | services/integration-gateway | L | n/a | n/a | n/a | L (+ Phase C lookup-gate LIVE) | L | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a |
| Ledger (svc) | services/ledger | L | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | L | L (primitive) | n/a | n/a | n/a |
| Settlement (glue) | apps/api/src/money/integration | L | n/a | n/a | n/a | n/a | L | n/a | n/a | n/a | n/a | n/a | L | (inherits) | N |
| FI Partner Mgmt | N | n/a | N | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | N | D | N |
| Employer Mgmt | apps/api/src/business | L (CRUD) | L | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | P | L (A2) | N |
| Employee Banking | apps/api/src/workforce/employee | L (CRUD) | L | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | P | L (A2) | N |
| Merchant Mgmt | N | n/a | N | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | N | D | N |
What jumps out — column-by-column
Disbursement column
EWA + Lending only. Everything else N. Disbursement primitive (InitiateDisbursement) is generic; missing domains just don't call it yet.
Settlement column
Same picture as disbursement — the gateway closes the loop for whatever calls it. Wiring more domains to it is cheap; building the domains is the cost.
Repayment column — closed at the admin/ledger level (Phase B1+B2, 2026-05-29)
- EWA:
L (B1, admin)—RecordEwaRepaymentUseCase+ admin endpoint. Ledger closes the receivable. Outbox emitsewa.repaid.v1. Payroll-event consumer path still PLANNED. - Lending:
L (B1/B2, admin)—RecordRepaymentUseCase+RemitInstallmentToFiUseCase+ admin endpoints. Ledger closes the receivable AND zeroes the FI payable. Outbox emitsloan.installment_repaid.v1+loan.installment_remitted_to_fi.v1. Payroll-event consumer path still PLANNED; bank-side outbound transfer for the remit still PLANNED. - BNPL:
N— no domain.
Disbursement-without-repayment "sieve" is structurally closed for the two active domains at the admin-driven cadence (pilot-viable at single-employer scale). Automation (payroll consumer + bank-side remit) remains Phase B continuation work.
Collections + Default columns
Both N across the board. Lending has a DEFAULTED enum value with no caller (D for lending default — it's an attractive nuisance). Collections is undefined as a process and as code.
Reconciliation column
LIVE primitives in three places (gateway recon, ledger ReconcileWithBank, settlement applier). No domain has a daily reconciliation cadence defined.
RBAC column — CLOSED for the four user-facing domains (Phase A2, 2026-05-29)
EWA, Lending, Employer Mgmt, Employee Banking now have LIVE RBAC via OrgRoleGuard + @RequireOrgRole(...) / @RequirePlatformAdmin() decorators. x-actor-id header rejected (Phase A1). Remaining D entries (KYC, Risk, Fraud, Collections, Compliance, Reporting, FI Partner, Merchant Mgmt) are PLANNED domains — they'll inherit the same RBAC pattern when their code lands.
Schema column for missing domains
BNPL, Payroll, Savings, Equb all have D schema rows — the legacy Prisma models exist with Decimal(15,2) money fields, no tenantId, no RLS. They look like domains; they are architectural mines. Delete them in a forward migration before someone writes new code against them.
Notification column
Uniformly absent. The notifications service is S. Every domain emits events; nothing routes them to a user. This is separately a 2-week project — pick a provider (Africa's Talking, Twilio, Vonage), wire the consumer, add user preferences.
Per-domain narrative
EWA — disburse-strong, collect-broken
Strongest of the user-facing domains. The bank-orchestrator transition (GAP-01..12) closes EWA's disburse path. The collect path does not exist — no repayment use case, no payroll event consumer. Every EWA issued today is an open obligation that the system has no documented way to close.
To close: build RecordEwaRepaymentUseCase, wire it to a (planned) payroll-deduction event consumer.
Lending — disburse-strong, collect-PARTIAL, default-broken
Same shape as EWA except RecordRepaymentUseCase exists. It is invoked by an admin endpoint. At pilot scale that is operationally tolerable; at 1000 active loans it is impossible. Default path is a DANGEROUS enum value with no caller.
To close: payroll consumer (same one EWA needs); collections workflow; default + write-off use cases.
BNPL — does not exist
The directory does not exist. The legacy Prisma models exist and are architectural mines. Promise nothing externally. Delete the legacy tables; start fresh with packages/bnpl/ modelled like EWA + lending.
Payroll — the anchor, does not exist
The single most important undone work. Without payroll, EWA + Lending + BNPL cannot collect. Building payroll unlocks every other domain's repayment loop.
A minimum-viable payroll consists of:
- Pay-period calendar per employer.
- Pay-run state machine (
SCHEDULED → COMPUTING → APPROVED → SUBMITTING → SETTLED). - Deduction calculator (consume current EWA outstanding + due lending installments + due BNPL installments + statutory + voluntary).
- Net-pay disbursement via gateway (1 transfer per employee).
- Outbox event
payroll.deductions_taken.v1per (employee, deduction-category, amount). - Outbox event
payroll.run_completed.v1per (tenant, period). - Consumers in EWA + Lending + BNPL.
Estimate: 3–4 engineering weeks for a first cut. Highest-leverage single work item remaining on the platform.
Savings + Equb — do not exist
Both Planned. Architectural significance: Equb (rotating savings) is culturally important in Ethiopia and is a differentiator. But the bank-orchestrator architecture supports it natively (it's just N peer-to-peer transfers on a cadence). Build last; do not block the rest.
KYC — does not exist; blocks pilot
packages/kyc/ does not exist. Production cannot ship in Ethiopia without identity verification — Fayda national ID lookup, document capture, liveness check, sanctions screen. This is on the regulator-cannot-ship list.
Wallet — intentionally absent (correct)
ADR-006 forbids it. Legacy Wallet* tables are deprecated + ESLint-blocked. Delete the tables in a forward migration once nothing reads them.
Risk + Fraud + Compliance + Collections + Reporting
All Planned. None have packages. Each is its own 2-4 week project. Some are pilot-blocking (KYC, sanctions screening); some are post-pilot (advanced behavioural fraud detection).
Employer Mgmt + Employee Banking — exist but DANGEROUS
apps/api/src/business/ + apps/api/src/workforce/employee/ are LIVE CRUD layers. They are not registered under TenantContextMiddleware and Business has no tenantId. Every authenticated user can list every employer's roster. 1-day fix; pilot-blocking.
Cross-cutting capabilities
Notifications
| Channel | Status | Notes |
|---|---|---|
| SMS | YELLOW | LoggingSmsSender only — logs to stdout. Africa's Talking adapter PLANNED. |
| RED | better-auth's email-verify uses an in-process sender (dev-only). Production email provider PLANNED. | |
| Push (web/mobile) | RED | No service worker, no mobile app, no APN/FCM integration. |
| In-app | RED | Frontends are mock-only. |
Idempotency
| Layer | Status |
|---|---|
HTTP entry (Idempotency-Key header) | LIVE — required on EWA + lending POSTs |
Ledger ((tenant, idempotency_key) UNIQUE) | LIVE |
Gateway ((tenant, idempotency_key) UNIQUE on disbursement) | LIVE |
Reconciliation ((tenant, partner, partner_reference) UNIQUE on bank_statement_line) | LIVE |
| TTL / cleanup | DANGEROUS — none. Tables grow forever. |
Audit
| Capability | Status |
|---|---|
| Same-tx invariant | LIVE (ADR-008) |
| Append-only | LIVE-by-convention |
| Cross-tenant viewer | PLANNED |
Poisonability via x-actor-id | CLOSED (Phase A1) — header rejected with 400; actorId sourced from session. |
Tenancy
| Capability | Status |
|---|---|
| RLS on financial tables (ewa_request, loan, loan_repayment, outbox_event, idempotency_record, audit_entry, bank_statement_line) | LIVE |
| FORCE ROW LEVEL SECURITY | LIVE |
| Verify-guard at migration time | LIVE |
| Tenant context propagation via middleware | LIVE for EWA + Lending |
| Tenant context propagation for Business + Employee | DANGEROUS — not wired |
Money invariants
| Capability | Status |
|---|---|
| NUMERIC(20,0) santim on new tables | LIVE |
Decimal(15,2) on legacy tables (payroll, BNPL, equb, etc.) | DANGEROUS — model violation |
No cash account in any taxonomy | LIVE (GAP-04) |
| Append-only ledger | LIVE |
Read the matrix this way
Pick any column. Every cell that's not LIVE is a future incident vector. Pick any row. Every cell that's not LIVE is a domain feature that doesn't exist. The risk is the union of both.
A row with mostly L and one D is the most urgent — the platform looks done in that domain but has a critical hidden flaw (EWA repayment, Lending default handling).
A row that's mostly N is a roadmap conversation, not an incident risk (BNPL, Savings, Equb).
A column that's uniformly D across rows is a cross-cutting bug (RBAC, payroll-driven repayment).
Cross-references
- Narrative version →
REAL_SYSTEM_STATE.md. - What gets fixed in 90 days →
90_DAY_EXECUTION_PLAN.md. - Production-readiness score per ops dimension →
PRODUCTION_READINESS.md.