Skip to main content

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:

This 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.

ModuleWhat's missingWhy it blocks pilotEffortLegacy 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 integratesNO
Sanctions screening primitivePARTIAL (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
Real SMS providerCLOSED (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.ClosedClosedN/A
Real email providerCLOSED (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.ClosedClosedN/A
ADR-014 — "DemozPay is orchestrator, not custodian"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 closedClosedN/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.

ModuleWhat's missingWhy it limits scaleEffortLegacy 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 weeksYES — SavingGoal
packages/equb/Ethiopian rotating-savings club. Cultural differentiator. Zero use case code.Marketing differentiator.3-4 weeksYES — 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 weeksNO
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 weeksNO
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 weeksYES — FinancialInstitution model exists but no domain logic reads it
packages/merchant/BNPL merchant onboarding + settlement preferences.Depends on BNPL.2 weeksYES — Merchant model exists, no domain code
2FA / TOTP / WebAuthnTwoFactor Prisma table exists, better-auth plugin not wired.Bank-grade access requires MFA. Operator admin credentials are single-factor today.3-5 daysN/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.

WhatWhere it should liveCurrent stateEffort to shipPilot 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-shape3 daysMedium — email verification flow incomplete
Payroll-event consumer (drives EWA + Lending + future BNPL repayment)new subscriber inside apps/apiPLANNED — depends on payroll domainIncluded in payroll Tier 1 effortPilot-blocking via payroll
Statement-pull adapter (auto-downloads bank statements from partner SFTP)services/integration-gateway/internal/reconciliation/PLANNED — operator manually feeds CSV files today1 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 bankPARTIAL — Phase B2 closed the LEDGER side only. The actual money requires manual bank ops at pilot.1 day with existing gateway patternMedium — recoverable gap, operator-driven for pilot
Bank-side outbound transfer for EWA business-pool reimbursementsimilarPARTIAL — same shape1 dayLow — most pilot employers have one bank account
Admin tooling UI (replay webhook, force-resync, view ledger account, authorise reverse)new apps/admin-web integrationSTUB — admin-web is mock-only. Operators run psql against 3 separate DBs today.1-2 weeksMedium — first real incident reveals the gap
Real Dashen sandbox integrationenv var swapOPERATIONAL — adapter code is ready; needs GATEWAY_DASHEN_BASE_URL + signing key from Dashen0 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 othersPARTIAL — UI shells Live, ALL data is mock1 week per app; recommend employer-web firstHigh — without it, pilot is API-only via curl
Outbox event catalogue docdocs/architecture/event-catalog.mdPLANNED — promised in ADR-011 follow-ups2 daysLow — 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".

WhatStatusEffortTracked as
Kubernetes manifests + Helm chartsPLANNED1 weekGL-17/18 deps
Terraform / IaC for cloud infraPLANNED1-2 weeks
TLS at the edgePLANNED1 weekGL-17
Database backups + PITRPLANNED1.5 weeks (incl. restore drill)GL-18
Read replicas + read routingPLANNED1 week
pgbouncer / connection poolerPLANNED3 days
Secrets manager (Vault / AWS SM / Doppler)PLANNED1 weekGL-16
Alertmanager + Grafana + paging provider deploymentPLANNED (rules + SLOs LIVE as code from Phase D)1-2 weeksGL-08
Production frontend DockerfilesPLANNED — current dev-mode containers3 days
Production CI: SAST, secret-leak, dep-vuln scanningPLANNED2 daysGL-19
Status page (customer-facing)PLANNED2 days
On-call rota + incident drillsPLANNED1 week elapsedGL-20
Idempotency-record TTL cleanup jobPLANNED0.5 dayGL-07
OpenTelemetry collector + traces backendPARTIAL (SDK wired, no exporter destination)1 week
Long-term log retention (Loki / CloudWatch)PLANNED3 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.

ModelIssueRecommended action
Payroll + PayrollEntryDecimal(15,2), no tenantId, no RLSDrop when packages/payroll/ ships with a clean schema
BNPLPurchase + BNPLPartner + BNPLPaymentSameDrop when packages/bnpl/ ships
Equb + EqubPayout + EqubMemberSameDrop when packages/equb/ ships
SavingGoalSameDrop when packages/savings/ ships
BillPayment, ExpenseSame; no clear domain ownerDrop now if no use case planned
Wallet, WalletTransaction, WithdrawalRequestDEPRECATED 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.
FinancialInstitutionNo domain code reads or writes itRefactor into packages/fi-partner/ when that ships
MerchantSameRefactor 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:

CategoryLIVE
Domain packagesewa (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
Servicesledger (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 subsystemsbetter-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
Frontends5 web apps + docs-web — all Partial (UI shells Live, mock data)
ObservabilityPino + 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 isolationRLS + FORCE RLS on 7 tables (Phase A2 extended); verify-guard migration
ReconciliationPrimitive 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)

ItemStatusEffortWhy first
Wire real SMS providerLIVEEthioTelecomSmsSender (SMPP direct to Ethio Telecom) ships in apps/api/src/_infra/sms/. Pluggable factory pattern — Safaricom et al. add in 4 mechanical steps.doneClosed the phone-OTP auth path. Verified end-to-end with 3 production SMS.
Wire real email provider (Postmark or AWS SES)PLANNED2 daysEmail verification ships end-to-end.
Write ADR-014 ("orchestrator, not custodian")LIVE — Engineering-side accepted; Legal counter-sign pending. docs/adr/ADR-014-orchestrator-not-custodian.md.doneNBE conversation now has a citable artefact.

Sprint E2 — KYC + sanctions (weeks 2-3)

ItemEffortWhy next
packages/kyc/ skeleton (capture nationalId + photo + document; manual reviewer workflow)domain layer LIVE (2026-05-29); ~1 week remaining for Prisma adapter + controller + disburse-gate2 weeks → ~1 weekRegulator gate. Pilot can't ship without identity capture. Pilot tier is manual review; Fayda integration follows.
Sanctions screening primitive (CSV ingest of OFAC + UN + Ethiopia lists; pre-disburse check)domain layer LIVE (2026-05-29); ~3 days remaining for Prisma adapter + CSV CLI + disburse-gate wiring1 week → ~3 daysPilot tier. Partner-bank gate.

Sprint E3 — Payroll domain (weeks 4-6)

ItemEffortWhy
packages/payroll/ MVP — pay-period calendar + deduction calculator + payroll-run state machinebackend LIVE end-to-end (2026-05-29) including HTTP + Prisma + Disburse + apps/api adapters; ~3 days remaining for cross-domain consumer wiring2 weeks → ~3 days remainingThe trust anchor. Unblocks automated repayment for EWA + Lending.
Bulk-disbursement use case (one transfer per employee via gateway)4 daysThe "net pay" half of payroll.
Outbox events payroll.deductions_taken.v1 + payroll.run_completed.v12 daysThe event surface that consumers subscribe to.
EWA + Lending consumers of payroll.deductions_taken.v13 daysReplaces today's admin-triggered repayment with automation.

Sprint E4 — Notifications + bank-side remit + frontend (weeks 7-8)

ItemEffortWhy now
services/notifications/ real consumer (subscribes to outbox; sends SMS via the Africa's Talking provider from E1)1 weekFirst user-facing differentiator. Stops events falling into a void.
Bank-side FI remit transfer (closes Phase B2's gap; uses existing gateway pattern)1 dayCloses the lending repayment loop properly.
Bank-side EWA business-pool reimbursement1 daySame 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 weekThe "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

  1. Auth before KYC because KYC capture is gated behind authentication.
  2. KYC + sanctions before payroll because regulator can stop pilot at any time; close the regulatory surface first.
  3. Payroll before notifications because notifications subscribe to outbox events; payroll generates the most important ones.
  4. 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

ConversationAudienceTalking points
"We cannot promise BNPL in 90 days"Product / salesZero code. Remove from external decks until packages/bnpl/ ships.
"We cannot promise Savings or Equb in 90 days"Product / marketingSame. The Equb claim is culturally compelling but has no code today.
"The notifications service is a stub"Customer success / productToday every event we emit goes nowhere. Customers will NOT receive "money arrived" SMS until E4.
"Frontends are mock-only"Sales / partner-onboardingDemos 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 / boardPhase D shipped pilot foundations. Pilot itself needs Tier 1 work. Set expectations now.

§10. Cross-references

§11. How to use this doc

ReaderWhat to do
Engineering leadPick the Phase E sprint to start. Decide whether to run sprints sequentially (8 weeks) or with two engineers in parallel (4 weeks).
ProductCross-reference against the public roadmap. Anything tagged Tier 1 or Tier 2 here that you've promised externally needs an adjustment conversation.
Sales / partnershipsDon't promise BNPL, Savings, Equb, or "automated payroll deduction" until the corresponding domain package ships.
Investors / boardThis doc is the honest "what's left" picture. Phase A-D shipped infrastructure; Phase E ships pilot.
New engineerUse this alongside REAL_SYSTEM_STATE.md. If you're picking up work, the order in §8 is the recommended priority.
Regulator preparationTier 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.