DemozPay — What is NOT Implemented
Snapshot: 2026-05-29 (post Phase D) Audience: anyone deciding what to promise externally OR what to build next. Purpose: the single canonical list of what does NOT exist as real code today, organised by pilot impact. Read this before any roadmap meeting.
Sister docs:
REAL_SYSTEM_STATE.md— narrative version of what IS built (with file:line evidence)DOMAIN_COMPLETENESS_MATRIX.md— 23 domains × 15 capabilities in one tableGO_LIVE_BLOCKERS.md— the 22 must-close-before-pilot items90_DAY_EXECUTION_PLAN.md— sequenced execution planThis file is the cross-cutting summary of all four. If you read only one, read this.
§1. The one-paragraph thesis
DemozPay's platform-orchestration backbone is LIVE end-to-end (Phases A through D shipped: auth + RBAC, EWA disburse + repay, lending disburse + repay + FI remit ledger-side, LookupAccount safety gate, ledger metrics, reconciliation runner, alerting rules + SLOs). But the product layer is hollow: the only two domains that exist are ewa and lending. Payroll — the trust anchor of the entire deduction-driven business model — does NOT exist as code. KYC, sanctions, BNPL, savings, equb, collections, fraud, risk, compliance, reporting — none of these have a domain package. The notifications service is a /healthz-only stub, so every outbox event you emit today falls into a void. Frontends are mock-only. A real pilot to one employer with five employees needs ~7-8 weeks of focused work on top of what's shipped.
§2. Tier 1 — pilot-blocking
You cannot run a real pilot with real customers without these. If your goal is "first employer, first 5 employees, first real money" — these are the gate.
| Module | What's missing | Why it blocks pilot | Effort | Legacy DANGEROUS schema? |
|---|---|---|---|---|
packages/payroll/ | PARTIAL (Phase E3, 2026-05-29) — Backend LIVE end-to-end EXCEPT cross-domain consumers. PayPeriod VO, Deduction VO, PayrollRun aggregate with full state machine (DRAFT → CALCULATED → APPROVED → DISBURSING → DISBURSED → COMPLETED + CANCELLED), PayrollEntry enforces net = gross − Σdeductions ≥ 0. 5 use cases: CreatePayrollRun (idempotent on tenant+period), CalculatePayrollRun, ApprovePayrollRun (emits payroll.deductions_taken.v1 per entry), DisbursePayrollRun (per-entry gateway submit, resumable, per-entry FAILED capture), CancelPayrollRun. HTTP: POST /api/payroll, /:id/{calculate,approve,disburse,cancel}, GET /, GET /:id. Prisma payroll_run + payroll_run_entry with santim NUMERIC(20,0) + tenantId + RLS verify (migration 20260529300000_payroll_run). apps/api adapters: PrismaPayrollRunRepository, EmployeeSalaryAdapter (reads Employee table), DeductionsAdapter (reads outstanding EWA principalSantim+feeSantim + active loan monthly amortisation), PayrollDisbursementAdapter (re-uses EWA's IntegrationGatewayClient), PayrollOutboxAdapter. 24 unit tests green. Still missing: EWA + lending consumers for payroll.deductions_taken.v1 — today the events fire but EWA RecordEwaRepaymentUseCase and lending RecordRepaymentUseCase are still admin-triggered. | The trust anchor. EWA + Lending + (future) BNPL all need a payroll-event consumer to automate collection. | ~3 days remaining (consumer wiring only) | YES — legacy Payroll, PayrollEntry Prisma models still exist with Decimal(15,2), no tenantId, no RLS. Drop after the new schema lands. |
packages/kyc/ | PARTIAL (Phase E2, 2026-05-29) — Backend LIVE end-to-end including disburse-gate. KycSubmission aggregate, state machine (PENDING → IN_REVIEW → APPROVED | REJECTED → EXPIRED), NationalId VO with last-4 masking, KycDocument VO with content-hash binding. 5 use cases. kyc_submission Prisma model + tenant RLS migration 20260529000000_add_kyc_submission. KycController exposes /api/kyc, /:id/{claim,approve,reject}, /queue, /status. EWA + lending disburse-gate LIVE — both domains' DisburseUseCases inject KycReadPort; non-APPROVED subjects fail-closed with Ewa/LoanKycGateError and an *KycGateBlocked audit row before any ledger / partner side-effect. KycReadAdapter exposed by KycApiModule bridges both domains to GetKycStatusUseCase. Still missing: reviewer queue UI (frontend, ~3 days), Fayda API auto-lookup + liveness check (post-pilot). | NBE (National Bank of Ethiopia) requirement. First regulator conversation asks: "How do you verify the people you're moving money for?" | ~3 days reviewer UI remaining; +2 weeks when Fayda API integrates | NO |
| Sanctions screening primitive | PARTIAL (Phase E2, 2026-05-29) — packages/sanctions/ LIVE end-to-end at the API layer: domain (SanctionsListEntry + ScreeningCheck VOs, normaliser), 2 use cases (IngestSanctionsListUseCase + ScreenIdentityUseCase), Prisma adapters + migration 20260529100000_add_sanctions (sanctions_list_entry GLOBAL + screening_check tenant-scoped with RLS + GIN index on normalisedAliases). HTTP routes: POST /api/sanctions/screen, GET /checks. CLI: apps/api/src/compliance/sanctions/cmd/ingest.ts for OFAC/UN/Ethiopia CSV ingest. Pre-disburse gate NOT yet wired — sanctions-at-disburse needs fullName on KycSubmission (currently only nationalId is captured); recommended implementation: run sanctions at KYC-approval time, fail approval on BLOCK. Still missing: KycSubmission.fullName schema field + ApproveKycUseCase sanctions integration, OFAC delta-refresh cron. | A disburse to a sanctioned individual = partnership-ending event with Dashen + immediate NBE attention. | ~2 days remaining (Kyc.fullName + ApproveKyc sanctions hook) | NO |
CLOSED (Phase E1, 2026-05-29) — EthioTelecomSmsSender ships in apps/api/src/_infra/sms/. Direct SMPP 3.4 binding to Ethio Telecom's wholesale SMSC. Three verification SMS messages delivered end-to-end. Pluggable factory in SmsModule — adding Safaricom or any provider is a documented 4-step recipe. | Closed | Closed | N/A | |
CLOSED (Phase E1, 2026-05-29) — SmtpEmailSender (nodemailer) ships in apps/api/src/_infra/email/. Pluggable factory in EmailModule; better-auth wired for verification + password-reset via EMAIL_SENDER. Same provider-switching recipe as SMS. | Closed | Closed | N/A | |
CLOSED — Engineering side (Phase E1, 2026-05-29). Legal counter-sign pending. docs/adr/ADR-014-orchestrator-not-custodian.md shipped. Indexed in ADR README. Cites every layer of enforcement (taxonomy, schema, code, ESLint, migration policy, reconciliation). | Engineering closed | Closed | N/A |
Tier 1 remaining: ~3-4 weeks of focused work (was 4-6; KYC backend + disburse-gate LIVE, sanctions backend LIVE pending sanctions-at-KYC integration, email closed, SMS closed, ADR-014 closed).
§3. Tier 2 — scale-blocking
Pilot can run without these. Production beyond 1-2 employers cannot.
| Module | What's missing | Why it limits scale | Effort | Legacy DANGEROUS schema? |
|---|---|---|---|---|
packages/bnpl/ | The entire BNPL domain. Zero code. | merchant-web frontend is mock-only because there's no backend. Product claim "BNPL" is fictional today. | 4-5 weeks (similar shape to lending) | YES — BNPLPartner, BNPLPurchase, BNPLPayment, Merchant. Same Decimal(15,2) / no-tenant problem. |
packages/savings/ | General savings product. Zero code. | Product claim hollow. | 3-4 weeks | YES — SavingGoal |
packages/equb/ | Ethiopian rotating-savings club. Cultural differentiator. Zero use case code. | Marketing differentiator. | 3-4 weeks | YES — Equb, EqubPayout, EqubMember |
packages/collections/ | Loan-default / overdue handling. Loan.markDefaulted() enum value exists with no caller. | First defaulting loan in pilot has no automated workflow. Operator handles manually. At pilot scale: tolerable. At 100+ loans: impossible. | 2 weeks | NO |
packages/fraud/ | Velocity limits, behavioural analytics, device fingerprinting. | Pilot can run on static velocity ceilings. Production needs real detection. | 3-4 weeks (basic) | NO |
packages/risk/ | Credit risk scoring beyond simple income-multiple. | Admin reviews loan requests by hand at pilot. Doesn't scale. | 2-3 weeks (basic) | NO |
packages/compliance/ | AML transaction monitoring, SAR/STR filings, NBE monthly returns. | Regulator requires monthly reports. Pilot tier: manual SQL exports. Production: required automation. | 3-4 weeks | NO |
packages/reporting/ | Financial + regulatory + partner-bank monthly reports. | Same as compliance. | 2 weeks (basic dashboards) | NO |
packages/fi-partner/ | FI onboarding workflow, per-partner exposure limits, capital ledger per partner. Today fiPartnerId is a free-form string on Loan with no parent table referenced. | Adding the second FI partner reveals the gap. | 2-3 weeks | YES — FinancialInstitution model exists but no domain logic reads it |
packages/merchant/ | BNPL merchant onboarding + settlement preferences. | Depends on BNPL. | 2 weeks | YES — Merchant model exists, no domain code |
| 2FA / TOTP / WebAuthn | TwoFactor Prisma table exists, better-auth plugin not wired. | Bank-grade access requires MFA. Operator admin credentials are single-factor today. | 3-5 days | N/A |
§4. Tier 3 — service-level subsystems that are STUBS or MISSING
These aren't full domain packages but they're load-bearing for operations.
| What | Where it should live | Current state | Effort to ship | Pilot impact |
|---|---|---|---|---|
| Notifications consumer (consumes outbox events → SMS/email/push) | services/notifications/ | STUB — only /healthz responds. Every *.bank_transfer_settled.v1 / *.repaid.v1 event today falls into a void. | 1-2 weeks (start with SMS only) | High — first user-visible differentiator |
| Email provider (Postmark, SES, Sendgrid) | apps/api/src/identity/auth/ | STUB — better-auth's internal sender used for verify emails, not production-shape | 3 days | Medium — email verification flow incomplete |
| Payroll-event consumer (drives EWA + Lending + future BNPL repayment) | new subscriber inside apps/api | PLANNED — depends on payroll domain | Included in payroll Tier 1 effort | Pilot-blocking via payroll |
| Statement-pull adapter (auto-downloads bank statements from partner SFTP) | services/integration-gateway/internal/reconciliation/ | PLANNED — operator manually feeds CSV files today | 1 week per partner (Dashen first) | High — daily recon depends on it for full automation |
| Bank-side outbound transfer for FI remit (lending) | new use case wiring gateway from PayrollClearing → FI's bank | PARTIAL — Phase B2 closed the LEDGER side only. The actual money requires manual bank ops at pilot. | 1 day with existing gateway pattern | Medium — recoverable gap, operator-driven for pilot |
| Bank-side outbound transfer for EWA business-pool reimbursement | similar | PARTIAL — same shape | 1 day | Low — most pilot employers have one bank account |
| Admin tooling UI (replay webhook, force-resync, view ledger account, authorise reverse) | new apps/admin-web integration | STUB — admin-web is mock-only. Operators run psql against 3 separate DBs today. | 1-2 weeks | Medium — first real incident reveals the gap |
| Real Dashen sandbox integration | env var swap | OPERATIONAL — adapter code is ready; needs GATEWAY_DASHEN_BASE_URL + signing key from Dashen | 0 code; 1-4 weeks elapsed (partner onboarding) | Pilot-blocking on the partner timeline |
| Frontend integration (real API calls) | apps/employer-web first (highest value), then others | PARTIAL — UI shells Live, ALL data is mock | 1 week per app; recommend employer-web first | High — without it, pilot is API-only via curl |
| Outbox event catalogue doc | docs/architecture/event-catalog.md | PLANNED — promised in ADR-011 follow-ups | 2 days | Low — but blocks new event-publishing PRs from being reviewable |
§5. Tier 4 — operational + infra deliverables (not "modules" but missing)
Production deployment requires these. They don't affect "does code work" but they affect "can we run it for real customers".
| What | Status | Effort | Tracked as |
|---|---|---|---|
| Kubernetes manifests + Helm charts | PLANNED | 1 week | GL-17/18 deps |
| Terraform / IaC for cloud infra | PLANNED | 1-2 weeks | — |
| TLS at the edge | PLANNED | 1 week | GL-17 |
| Database backups + PITR | PLANNED | 1.5 weeks (incl. restore drill) | GL-18 |
| Read replicas + read routing | PLANNED | 1 week | — |
| pgbouncer / connection pooler | PLANNED | 3 days | — |
| Secrets manager (Vault / AWS SM / Doppler) | PLANNED | 1 week | GL-16 |
| Alertmanager + Grafana + paging provider deployment | PLANNED (rules + SLOs LIVE as code from Phase D) | 1-2 weeks | GL-08 |
| Production frontend Dockerfiles | PLANNED — current dev-mode containers | 3 days | — |
| Production CI: SAST, secret-leak, dep-vuln scanning | PLANNED | 2 days | GL-19 |
| Status page (customer-facing) | PLANNED | 2 days | — |
| On-call rota + incident drills | PLANNED | 1 week elapsed | GL-20 |
| Idempotency-record TTL cleanup job | PLANNED | 0.5 day | GL-07 |
| OpenTelemetry collector + traces backend | PARTIAL (SDK wired, no exporter destination) | 1 week | — |
| Long-term log retention (Loki / CloudWatch) | PLANNED | 3 days | — |
§6. Tier 5 — Legacy DANGEROUS Prisma models (delete these)
These tables EXIST in the schema but have no domain code. They look like "X is built" to a new engineer. They are architectural mines — they violate ADR-005 (money is santim) and ADR-013 (tenant RLS). Delete in a forward migration once nothing reads them.
| Model | Issue | Recommended action |
|---|---|---|
Payroll + PayrollEntry | Decimal(15,2), no tenantId, no RLS | Drop when packages/payroll/ ships with a clean schema |
BNPLPurchase + BNPLPartner + BNPLPayment | Same | Drop when packages/bnpl/ ships |
Equb + EqubPayout + EqubMember | Same | Drop when packages/equb/ ships |
SavingGoal | Same | Drop when packages/savings/ ships |
BillPayment, Expense | Same; no clear domain owner | Drop now if no use case planned |
Wallet, WalletTransaction, WithdrawalRequest | DEPRECATED in code (Phase S4 / GL-12 ESLint blocks imports). Tables still in DB. | Drop in a forward migration; ADR-006 says wallet is derived from ledger. |
FinancialInstitution | No domain code reads or writes it | Refactor into packages/fi-partner/ when that ships |
Merchant | Same | Refactor into packages/merchant/ when BNPL ships |
§7. What ACTUALLY exists (so the contrast is honest)
For balance — here's what's LIVE end-to-end as of Phase D close:
| Category | LIVE |
|---|---|
| Domain packages | ewa (Partial: disburse Live, repay admin-LIVE, payroll consumer PLANNED), lending (Partial: same shape + FI remit ledger-side LIVE) |
shared/* | money, idempotency, audit, events, tenant-context (with actor — Phase A1), auth (RBAC decorators — Phase A2), database, logging (with PII redact — Phase A3), validation, ui, config — all Live |
| Services | ledger (Live + Phase D metrics interceptor + /metrics endpoint), integration-gateway (Live + Phase C LookupAccount + Phase D recon-runner + /metrics), bank-sandbox (Live for tests), notifications (STUB) |
| API monolith subsystems | better-auth (Live; phone-OTP Partial; 2FA Planned), business CRUD (Live + Phase A RBAC), employee CRUD (Live + Phase A RBAC), integration module (Live — webhook applier + settlement poller), outbox publisher (Live), Phase A AuthN/Z, Phase B repayment use cases (EWA + Lending + FI remit ledger-side), Phase C lookup gate, Phase D metrics + recon-runner |
| Frontends | 5 web apps + docs-web — all Partial (UI shells Live, mock data) |
| Observability | Pino + slog with PII redact (Phase A3); Prometheus on API + gateway + ledger (Phase C + D); alert rules + SLOs as code (Phase D); deployment of Alertmanager + Grafana PLANNED |
| Tenant isolation | RLS + FORCE RLS on 7 tables (Phase A2 extended); verify-guard migration |
| Reconciliation | Primitive Live (S4), Phase D recon-runner Live, daily cadence wiring PLANNED |
§8. Step-by-step build plan
If you decide to close the pilot gap, here is the order I recommend and why. Total: ~7-8 weeks of focused single-engineer work; ~3-4 weeks with two engineers in parallel.
Phase E (suggested name) — make pilot real
Sprint E1 — Auth + identity infrastructure (week 1) — PARTIAL: 2 of 3 items LIVE (2026-05-29)
| Item | Status | Effort | Why first |
|---|---|---|---|
LIVE — EthioTelecomSmsSender (SMPP direct to Ethio Telecom) ships in apps/api/src/_infra/sms/. Pluggable factory pattern — Safaricom et al. add in 4 mechanical steps. | done | Closed the phone-OTP auth path. Verified end-to-end with 3 production SMS. | |
| Wire real email provider (Postmark or AWS SES) | PLANNED | 2 days | Email verification ships end-to-end. |
LIVE — Engineering-side accepted; Legal counter-sign pending. docs/adr/ADR-014-orchestrator-not-custodian.md. | done | NBE conversation now has a citable artefact. |
Sprint E2 — KYC + sanctions (weeks 2-3)
| Item | Effort | Why next |
|---|---|---|
packages/kyc/ skeleton (capture nationalId + photo + document; manual reviewer workflow) | Regulator gate. Pilot can't ship without identity capture. Pilot tier is manual review; Fayda integration follows. | |
| Pilot tier. Partner-bank gate. |
Sprint E3 — Payroll domain (weeks 4-6)
| Item | Effort | Why |
|---|---|---|
packages/payroll/ MVP — pay-period calendar + deduction calculator + payroll-run state machine | The trust anchor. Unblocks automated repayment for EWA + Lending. | |
| Bulk-disbursement use case (one transfer per employee via gateway) | 4 days | The "net pay" half of payroll. |
Outbox events payroll.deductions_taken.v1 + payroll.run_completed.v1 | 2 days | The event surface that consumers subscribe to. |
EWA + Lending consumers of payroll.deductions_taken.v1 | 3 days | Replaces today's admin-triggered repayment with automation. |
Sprint E4 — Notifications + bank-side remit + frontend (weeks 7-8)
| Item | Effort | Why now |
|---|---|---|
services/notifications/ real consumer (subscribes to outbox; sends SMS via the Africa's Talking provider from E1) | 1 week | First user-facing differentiator. Stops events falling into a void. |
| Bank-side FI remit transfer (closes Phase B2's gap; uses existing gateway pattern) | 1 day | Closes the lending repayment loop properly. |
| Bank-side EWA business-pool reimbursement | 1 day | Same for EWA. Often same employer account — may be no-op. |
Integrate employer-web with real API calls (login + dashboard + employee list + EWA approval + lending approval) | 1 week | The "real customer experience" gate. Highest-value frontend per the 90-day plan. |
After sprint E4 — pilot soak
- 7-day drift-clean soak with seed data (1 employer, 5 fake employees, 1 FI partner).
- Sign-off matrix (GL-22) — engineering, security, finance/ops.
- Then first real employer onboarded with capped exposure.
Order rationale
- Auth before KYC because KYC capture is gated behind authentication.
- KYC + sanctions before payroll because regulator can stop pilot at any time; close the regulatory surface first.
- Payroll before notifications because notifications subscribe to outbox events; payroll generates the most important ones.
- Notifications + frontend last because they are user-visible polish — by then the platform actually does the right thing under the hood.
What you DEFER cleanly to post-pilot
- BNPL (no merchants in pilot — adjust marketing)
- Savings + Equb (no savings in pilot)
- Collections automation (manual at pilot scale)
- Risk scoring (admin reviews loans manually)
- Fraud (static velocity ceilings + audit-log review)
- Reporting (monthly export by hand)
- Admin tooling UI (operators run psql)
- 2FA (email + password acceptable for first pilot; ship before scale)
- Tier 4 ops items: k8s, helm, terraform, Grafana deployment, statement-pull automation — all 1-2 weeks each, can run as a parallel platform-engineer workstream
§9. The five most uncomfortable conversations to have
| Conversation | Audience | Talking points |
|---|---|---|
| "We cannot promise BNPL in 90 days" | Product / sales | Zero code. Remove from external decks until packages/bnpl/ ships. |
| "We cannot promise Savings or Equb in 90 days" | Product / marketing | Same. The Equb claim is culturally compelling but has no code today. |
| "The notifications service is a stub" | Customer success / product | Today every event we emit goes nowhere. Customers will NOT receive "money arrived" SMS until E4. |
| "Frontends are mock-only" | Sales / partner-onboarding | Demos look real but no data flows. Pilot needs at least employer-web integrated for real. |
| "Pilot needs ~7-8 weeks beyond what's shipped" | Investors / board | Phase D shipped pilot foundations. Pilot itself needs Tier 1 work. Set expectations now. |
§10. Cross-references
- Per-domain narrative + file:line evidence →
REAL_SYSTEM_STATE.md - 23-domain × 15-capability matrix →
DOMAIN_COMPLETENESS_MATRIX.md - 22 hard go-live blockers with acceptance criteria →
GO_LIVE_BLOCKERS.md - Original (pre Phase C/D) 90-day plan →
90_DAY_EXECUTION_PLAN.md - Architecture topology (every container, every port) →
SYSTEM_TOPOLOGY.md - The status legend →
docs/STATUS_LEGEND.md
§11. How to use this doc
| Reader | What to do |
|---|---|
| Engineering lead | Pick the Phase E sprint to start. Decide whether to run sprints sequentially (8 weeks) or with two engineers in parallel (4 weeks). |
| Product | Cross-reference against the public roadmap. Anything tagged Tier 1 or Tier 2 here that you've promised externally needs an adjustment conversation. |
| Sales / partnerships | Don't promise BNPL, Savings, Equb, or "automated payroll deduction" until the corresponding domain package ships. |
| Investors / board | This doc is the honest "what's left" picture. Phase A-D shipped infrastructure; Phase E ships pilot. |
| New engineer | Use this alongside REAL_SYSTEM_STATE.md. If you're picking up work, the order in §8 is the recommended priority. |
| Regulator preparation | Tier 1 items (KYC, sanctions, ADR-014) are exactly what the NBE conversation will ask about. |
Last word. The platform foundation is real. The product layer is hollow. Phases A-D built the floor; Phase E builds the rooms.