Skip to main content

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

  1. Domain pkg. — does packages/<domain>/ exist?
  2. Schema — Prisma model + migrations + tenancy + santim semantics.
  3. Origination — request / quote intake.
  4. Eligibility — underwriting / accrual / income check.
  5. Approval — workflow to move PENDING → APPROVED.
  6. Disbursement — money out via gateway.
  7. Settlement — bank-side confirmation handling.
  8. Repayment — money back via payroll / direct debit / etc.
  9. Collections — overdue + escalation + write-off.
  10. Default — terminal "this is uncollectable" path.
  11. Reversal — corrective ledger entries.
  12. Reconciliation — bank-vs-ledger matching for this domain's transfers.
  13. Audit — entries land in same tx as state change.
  14. RBAC — who can do what.
  15. Notification — customer-facing comms.

Headline matrix

DomainPkgSchemaOriginEligApprvDisbSettleRepayCollDefaultRevReconAuditRBACNotif
EWALLLPLLLL (B1, admin)Nn/aPL (primitive)LL (A2)N
LendingLLLLLLLL (B1/B2, admin)NDPL (primitive)LL (A2)N
BNPLNDNNNNNNNNNNNn/aN
PayrollNDNn/an/aNNn/an/an/an/an/aNDN
SavingsNDNn/an/an/an/an/an/an/an/an/aNDN
EqubNDNn/an/an/an/an/an/an/an/an/aNDN
KYCNn/aNNNn/an/an/an/an/an/an/aNDN
Walletn/a (intentional)D (legacy)n/an/an/an/an/an/an/an/an/an/an/an/an/a
RiskNn/aNNn/an/an/an/an/an/an/an/aNDn/a
FraudNn/aNNn/an/an/an/an/an/an/an/aNDn/a
CollectionsNn/aNn/an/an/an/an/aNNn/an/aNDN
ComplianceNn/aNNn/an/an/an/an/an/an/an/aNDN
Audit (domain)shared/auditLn/an/an/an/an/an/an/an/an/an/aLDn/a
NotificationsN (service stub)n/an/an/an/an/an/an/an/an/an/an/an/an/aS
ReportingNn/aNn/an/an/an/an/an/an/an/an/aNDN
Reconciliation (svc)services/integration-gateway/internal/reconLn/an/an/an/an/an/an/an/an/aL (primitive)n/an/an/a
Integration Gateway (svc)services/integration-gatewayLn/an/an/aL (+ Phase C lookup-gate LIVE)Ln/an/an/an/an/an/an/an/a
Ledger (svc)services/ledgerLn/an/an/an/an/an/an/an/aLL (primitive)n/an/an/a
Settlement (glue)apps/api/src/money/integrationLn/an/an/an/aLn/an/an/an/an/aL(inherits)N
FI Partner MgmtNn/aNn/an/an/an/an/an/an/an/an/aNDN
Employer Mgmtapps/api/src/businessL (CRUD)Ln/an/an/an/an/an/an/an/an/aPL (A2)N
Employee Bankingapps/api/src/workforce/employeeL (CRUD)Ln/an/an/an/an/an/an/an/an/aPL (A2)N
Merchant MgmtNn/aNn/an/an/an/an/an/an/an/an/aNDN

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 emits ewa.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 emits loan.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:

  1. Pay-period calendar per employer.
  2. Pay-run state machine (SCHEDULED → COMPUTING → APPROVED → SUBMITTING → SETTLED).
  3. Deduction calculator (consume current EWA outstanding + due lending installments + due BNPL installments + statutory + voluntary).
  4. Net-pay disbursement via gateway (1 transfer per employee).
  5. Outbox event payroll.deductions_taken.v1 per (employee, deduction-category, amount).
  6. Outbox event payroll.run_completed.v1 per (tenant, period).
  7. 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

ChannelStatusNotes
SMSYELLOWLoggingSmsSender only — logs to stdout. Africa's Talking adapter PLANNED.
EmailREDbetter-auth's email-verify uses an in-process sender (dev-only). Production email provider PLANNED.
Push (web/mobile)REDNo service worker, no mobile app, no APN/FCM integration.
In-appREDFrontends are mock-only.

Idempotency

LayerStatus
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 / cleanupDANGEROUS — none. Tables grow forever.

Audit

CapabilityStatus
Same-tx invariantLIVE (ADR-008)
Append-onlyLIVE-by-convention
Cross-tenant viewerPLANNED
Poisonability via x-actor-idCLOSED (Phase A1) — header rejected with 400; actorId sourced from session.

Tenancy

CapabilityStatus
RLS on financial tables (ewa_request, loan, loan_repayment, outbox_event, idempotency_record, audit_entry, bank_statement_line)LIVE
FORCE ROW LEVEL SECURITYLIVE
Verify-guard at migration timeLIVE
Tenant context propagation via middlewareLIVE for EWA + Lending
Tenant context propagation for Business + EmployeeDANGEROUS — not wired

Money invariants

CapabilityStatus
NUMERIC(20,0) santim on new tablesLIVE
Decimal(15,2) on legacy tables (payroll, BNPL, equb, etc.)DANGEROUS — model violation
No cash account in any taxonomyLIVE (GAP-04)
Append-only ledgerLIVE

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