DemozPay — HTTP API Inventory
Purpose. A single audit-friendly catalogue of every HTTP endpoint the API exposes today, grouped by module, with auth requirements, source file, and
Live / Partial / Stub / Plannedstatus per the STATUS_LEGEND.Scope. Backend HTTP only (the NestJS modular monolith at
apps/api/). Frontends, gRPC, Kafka events, and CLI tools are out of scope.How to use. Use this to spot gaps before sprint-planning. Each section ends with a
Gapsblock. When you ship a new endpoint, add a row here in the same commit.
What changed in this audit pass (2026-05-29)
The six findings called out in the first inventory have all been closed in code:
| # | Finding | Status | Notes |
|---|---|---|---|
| 1 | Double-prefix bug (@Controller('api/...') + globalPrefix='api' → /api/api/...) | Fixed | 12 controllers renamed; supertest spec now calls app.setGlobalPrefix('api') so URLs match production. |
| 2 | Lending had no read endpoints | Fixed | GET /api/loans + GET /api/loans/:id shipped via ListLoansQuery + GetLoanQuery. |
| 3 | No reversal for one-time earnings post-approval | Fixed (alias) | POST :id/reverse added as an operator-vocabulary alias for /void. The state machine already permitted POSTED → VOIDED. |
| 4 | No employee self-service surface | Partially fixed | /api/me/profile, /api/me/loans, /api/me/loans/:id shipped. /me/ewa/requests + /me/payslips follow when the EWA + payroll repos gain findByEmployee. |
| 5 | 2FA + magic-link plugins not wired | Fixed | twoFactor() + magicLink() registered in better-auth.factory.ts. TwoFactor table already existed. |
| 6 | No bank-webhook replay surface | Fixed | New bank_webhook_event table; receive-side audit logging; GET/POST /api/integration/webhooks(/...) platform-admin surface. |
Plus a Tier-2 gap closed inline: KYC POST :id/request-more-info.
Conventions
| Symbol | Meaning |
|---|---|
🔓 Public | @Public() — no session required (webhooks, health, metrics). |
🔐 Session | Default — any signed-in user. |
👤 Org:X | @RequireOrgRole(...) — caller must hold role X in the active organization (member / admin / owner). |
🛡 Platform | @RequirePlatformAdmin() — cross-tenant admin (DemozPay staff). |
🪙 Idem | Money-moving — Idempotency-Key header required (ADR-007). |
Global URL conventions
- Global prefix:
apps/api/src/main.ts:46setsapp.setGlobalPrefix('api'). Every Nest controller mounts under/api/. Controllers MUST be declared without a leadingapi/in@Controller(...)— otherwise the route doubles up (Finding #1). - Auth subtree:
better-authis mounted on Express before Nest's router at/api/auth/*(apps/api/src/main.ts:41-42). It owns its full subtree.
Module index
| Module | Status | Controllers | See |
|---|---|---|---|
| Auth (better-auth) | Live | (Express handler) | §1 |
| Platform / Health / Metrics | Live | 3 | §2 |
| Business (organizations) | Live | 1 | §3 |
| Employees | Live | 1 | §4 |
| Me / self-service | Live | 1 | §5 |
| Integration (bank webhooks) | Live | 2 | §6 |
| KYC | Live | 1 | §7 |
| Sanctions | Live | 1 | §8 |
| EWA (Earned-Wage Access) | Live | 1 | §9 |
| Lending (salary-backed loans) | Live | 1 | §10 |
| Payroll (core lifecycle) | Live | 6 | §11 |
| Payroll (P2 — earnings / overtime / protection) | Live | 1 | §12 |
| Payroll (adjustments) | Live | 1 | §13 |
| Payroll (deduction mandates) | Live | 1 | §14 |
| Payroll (tenant settings + auto-lock policy) | Live | 2 | §15 |
| Payroll (PDF payslips) | Live | 1 | §16 |
| Payroll (court-order remit) | Live | 1 | §17 |
| Payroll (platform-admin overrides) | Live | 1 | §18 |
| Payroll (health) | Live | 1 | §19 |
| BNPL | Planned | — | §20 |
| Wallet (read-only over ledger) | Planned | — | §21 |
| Equb / Savings | Planned | — | §22 |
| Notifications | Stub | — | §23 |
1. Auth (better-auth)
Mount. Express owns /api/auth/* before Nest takes effect — apps/api/src/main.ts:41-42. Routes are produced by better-auth itself (apps/api/src/identity/auth/better-auth.factory.ts:33-150).
Plugins enabled (post-Finding-#5): emailAndPassword, emailVerification, organization, phoneNumber (OTP), twoFactor, magicLink.
| Method | Path | Plugin | Status | Notes |
|---|---|---|---|---|
| POST | /api/auth/sign-up/email | email+pw | Live | Org plugin auto-creates first org. |
| POST | /api/auth/sign-in/email | email+pw | Live | Sets session cookie. |
| POST | /api/auth/sign-out | core | Live | |
| GET | /api/auth/get-session | core | Live | Returns current session + user. |
| POST | /api/auth/forget-password | email+pw | Live | Fires sendResetPassword. |
| POST | /api/auth/reset-password | email+pw | Live | |
| POST | /api/auth/verify-email | emailVerification | Live | Token from sendVerificationEmail. |
| POST | /api/auth/phone-number/send-otp | phoneNumber | Live | SMS via pluggable sender. |
| POST | /api/auth/phone-number/verify | phoneNumber | Live | |
| POST | /api/auth/two-factor/enable | twoFactor | Live | Per-user TOTP. |
| POST | /api/auth/two-factor/disable | twoFactor | Live | |
| POST | /api/auth/two-factor/verify | twoFactor | Live | |
| POST | /api/auth/two-factor/generate-backup-codes | twoFactor | Live | |
| POST | /api/auth/sign-in/magic-link | magicLink | Live | Email link, 15-minute expiry. |
| POST | /api/auth/magic-link/verify | magicLink | Live | |
| POST | /api/auth/organization/create | organization | Live | New tenant. |
| GET | /api/auth/organization/list | organization | Live | Caller's orgs. |
| POST | /api/auth/organization/set-active | organization | Live | Sets active tenant. |
| GET | /api/auth/organization/get-full-organization | organization | Live | Members + invites. |
| POST | /api/auth/organization/invite-member | organization | Live | |
| POST | /api/auth/organization/accept-invitation | organization | Live | |
| POST | /api/auth/organization/leave | organization | Live | |
| POST | /api/auth/organization/update-member-role | organization | Live | |
| POST | /api/auth/organization/remove-member | organization | Live |
Gaps remaining.
- Tenant-level 2FA enforcement policy (force-enable for all admins of a tenant) —
Planned. The plugin is wired per-user only. - Passkey / WebAuthn —
Planned. Not required by current threat model.
2. Platform / Health / Metrics
| Method | Path | Auth | File:line | Notes |
|---|---|---|---|---|
| GET | /api/ | 🔓 Public | apps/api/src/app/app.controller.ts:8-12 | Hello-world; banner only. |
| GET | /api/healthz | 🔓 Public | apps/api/src/_infra/health/health.controller.ts:25 | Liveness — 200 if event loop runs. |
| GET | /api/readyz | 🔓 Public | apps/api/src/_infra/health/health.controller.ts:35 | Probes PG + Redis + Kafka + ledger gRPC. |
| GET | /api/metrics | 🔓 Public | apps/api/src/_infra/observability/metrics/metrics.controller.ts:13-19 | Prometheus scrape. |
3. Business
@Controller('business') → /api/business/*. Source: apps/api/src/business/business.controller.ts.
| Method | Path | Auth | Line | Notes |
|---|---|---|---|---|
| POST | /api/business | 🛡 Platform | 38 | Create business. |
| GET | /api/business | 🔐 Session | 61 | List businesses caller belongs to. |
| GET | /api/business/:id | 👤 Org:member+ | 74 | |
| GET | /api/business/:id/employee-count | 👤 Org:member+ | 84 | |
| PUT | /api/business/:id | 👤 Org:admin+ | 91 | |
| PUT | /api/business/:id/verify-kyc | 🛡 Platform | 101 | |
| PUT | /api/business/:id/status | 🛡 Platform | 111 | |
| DELETE | /api/business/:id | 👤 Org:owner | 124 |
Gaps.
GET /api/business/:id/branding— multi-tenant branding (logo, primary colour).Planned.- Soft-delete + restore — current DELETE is destructive.
Planned.
4. Employees
@Controller('employees') → /api/employees/*. Source: apps/api/src/workforce/employee/employee.controller.ts.
| Method | Path | Auth | Line | Notes |
|---|---|---|---|---|
| POST | /api/employees | 👤 Org:admin+ | 42 | |
| POST | /api/employees/bulk-import | 👤 Org:admin+ | 89 | CSV upload. |
| GET | /api/employees | 👤 Org:member+ | 135 | |
| GET | /api/employees/active/:businessId | 👤 Org:member+ | 141 | |
| GET | /api/employees/department/:businessId/:department | 👤 Org:member+ | 147 | |
| GET | /api/employees/:id | 👤 Org:member+ | 156 | |
| PUT | /api/employees/:id | 👤 Org:admin+ | 168 | |
| PUT | /api/employees/:id/status | 👤 Org:admin+ | 188 | |
| DELETE | /api/employees/:id | 👤 Org:admin+ | 208 |
Gaps (Tier-2 follow-up — not blocking).
POST /api/employees/:id/transfer— change businessId / department within the same tenant. Needs an audit row + outbox event.GET /api/employees/:id/employment-history— paginated audit timeline. Backing table doesn't exist yet.GET /api/employees/export— paginated CSV for payroll reconciliation. Trivial; deferred.
5. Me / employee self-service
NEW in this pass (Finding #4). @Controller('me') → /api/me/*. Source: apps/api/src/identity/me/me.controller.ts.
Read-only. Resolves session.user.id → Employee via Employee.userId, then pins downstream queries to that employee id. Returns 404 EMPLOYEE_NOT_LINKED when the caller has no employee row (org-admin / platform-admin staff).
| Method | Path | Auth | Line | Notes |
|---|---|---|---|---|
| GET | /api/me/profile | 🔐 Session | 51 | Caller's employee row. |
| GET | /api/me/loans | 🔐 Session | 66 | Caller's loans (via ListLoansQuery). |
| GET | /api/me/loans/:id | 🔐 Session | 87 | Same 404 on cross-employee leak. |
Gaps (next commits).
GET /api/me/ewa/requests— blocked onEwaRequestRepository.findByEmployee. Mechanical port addition.GET /api/me/payslips— blocked onPayrollRunEntryRepository.findByEmployee. Mechanical.GET /api/me/sanctions— should always 200 with empty (employees should not see their own screen results except via KYC verdict).GET /api/me/notifications— depends on Notifications module (§23).
6. Integration (bank webhooks)
Source: apps/api/src/money/integration/.
| Method | Path | Auth | File:line | Notes |
|---|---|---|---|---|
| POST | /api/integration/bank-callback/:partner | 🔓 Public + HMAC | bank-webhook.controller.ts:60 | Inbound partner webhook. Now persists every accepted payload to bank_webhook_event for replay (Finding #6). |
| GET | /api/integration/webhooks | 🛡 Platform | bank-webhook-replay.controller.ts:40 | NEW — list recent audit rows. |
| GET | /api/integration/webhooks/:id | 🛡 Platform | bank-webhook-replay.controller.ts:48 | NEW — single row with rawBody. |
| POST | /api/integration/webhooks/:id/replay | 🛡 Platform | bank-webhook-replay.controller.ts:60 | NEW — re-applies the stored outcome via BankSettlementApplier. Idempotent at the applier layer. |
Gaps.
POST /api/integration/bank-callback/:partner/test— partner-driven smoke ping.Planned.
7. KYC
@Controller('kyc') → /api/kyc/*. Source: packages/kyc/backend/presentation/kyc.controller.ts.
| Method | Path | Auth | Line | Notes |
|---|---|---|---|---|
| POST | /api/kyc | 🔐 Session | 73 | Submit. |
| POST | /api/kyc/:id/claim | 🛡 Platform | 99 | |
| POST | /api/kyc/:id/approve | 🛡 Platform | 107 | Sanctions screen at approve (E2). |
| POST | /api/kyc/:id/reject | 🛡 Platform | 128 | Terminal. |
| POST | /api/kyc/:id/request-more-info | 🛡 Platform | 148 | NEW — returns to PENDING with an applicant-facing note. Emits kyc.more_info_requested.v1. |
| GET | /api/kyc/queue | 🛡 Platform | 162 | Reviewer queue. |
| GET | /api/kyc/status | 🔐 Session | 173 | Caller's own KYC state. |
Gaps.
GET /api/kyc/:id/documents— signed-URL fetch for uploaded docs.Planned.- Webhook to notify partner FIs of KYC verdict.
Planned. POST /api/kyc/:id/escalate— second-line reviewer override.
8. Sanctions
@Controller('sanctions'). Source: packages/sanctions/backend/presentation/sanctions.controller.ts.
| Method | Path | Auth | Line | Notes |
|---|---|---|---|---|
| POST | /api/sanctions/screen | 🛡 Platform | 42 | Ad-hoc; also runs at KYC approve. |
| GET | /api/sanctions/checks | 🛡 Platform | 67 | Recent screens. |
Gaps.
GET /api/sanctions/checks/:id— single-check detail with match evidence.POST /api/sanctions/lists/refresh— HTTP trigger for OFAC/UN ingest (CLI exists today).
9. EWA (Earned-Wage Access)
@Controller('ewa') → /api/ewa/*. Source: packages/ewa/backend/presentation/ewa.controller.ts.
| Method | Path | Auth | Line | Notes |
|---|---|---|---|---|
| GET | /api/ewa/eligibility | 👤 Org:admin+ | 61 | |
| POST | /api/ewa/requests | 🪙 Idem · 👤 Org:admin+ | 79 | |
| POST | /api/ewa/requests/:id/disburse | 🪙 Idem · 👤 Org:admin+ | 109 | |
| POST | /api/ewa/requests/:id/record-repayment | 🪙 Idem · 👤 Org:admin+ | 135 |
Gaps (Tier-2 follow-up).
GET /api/ewa/requests— list past requests per tenant / employee. NeedsEwaRequestRepository.findByTenant(same shape as the newLoanRepository.findByTenant).GET /api/ewa/requests/:id— single read.POST /api/ewa/requests/:id/cancel— pre-disburse cancellation.- Employee-facing self-service draw — today the admin draws on the employee's behalf.
10. Lending
@Controller('loans') → /api/loans/*. Source: packages/lending/backend/presentation/lending.controller.ts.
| Method | Path | Auth | Line | Notes |
|---|---|---|---|---|
| GET | /api/loans | 👤 Org:member+ | 64 | NEW — list, supports ?limit=, ?status= (repeatable), ?borrowerId=. |
| GET | /api/loans/:id | 👤 Org:member+ | 83 | NEW — single read; 404 with stable code. |
| POST | /api/loans/quote | 👤 Org:admin+ | 95 | Affordability quote. |
| POST | /api/loans | 🪙 Idem · 👤 Org:admin+ | 112 | Originate. |
| POST | /api/loans/:id/disburse | 🪙 Idem · 👤 Org:admin+ | 145 | |
| POST | /api/loans/:id/installments/:idx/remit-to-fi | 🪙 Idem · 👤 Org:admin+ | 175 | |
| POST | /api/loans/:id/installments/:idx/record-repayment | 🪙 Idem · 👤 Org:admin+ | 209 |
Gaps.
POST /api/loans/:id/restructure— change tenor / installment plan.Planned.POST /api/loans/:id/write-off— non-recoverable.Planned.- Hardship / pause endpoint.
Planned.
11. Payroll — core lifecycle
@Controller('payroll') → /api/payroll/*. Source: packages/payroll/backend/presentation/payroll.controller.ts. Auth: 👤 Org:admin+.
| Method | Path | Line |
|---|---|---|
| POST | /api/payroll | 102 |
| POST | /api/payroll/:id/calculate | 129 |
| POST | /api/payroll/:id/approve | 143 |
| POST | /api/payroll/:id/disburse (🪙 Idem) | 160 |
| POST | /api/payroll/:id/cancel | 177 |
| POST | /api/payroll/:id/lock | 292 |
| GET | /api/payroll/:id/report | 187 |
| GET | /api/payroll/:id/report.csv | 193 |
| GET | /api/payroll/:id/report.mor.xml | 215 |
| GET | /api/payroll/:id/report.pension.xml | 234 |
| GET | /api/payroll/:id/payslips/:employeeId | 258 |
| GET | /api/payroll/:id/payslips/:employeeId.html | 268 |
| GET | /api/payroll | 304 |
| GET | /api/payroll/:id | 312 |
Gaps (Tier-2 follow-up).
GET /api/payroll/:id/audit— full audit trail (actor, timestamps, state transitions). Backing data is in the outbox + audit log; needs a read view.- Differential / incremental run (re-calc only affected employees).
Planned.
12. Payroll — P2 (earnings, overtime, protection)
@Controller('payroll') + p2 path prefixes. Source: packages/payroll/backend/presentation/payroll-p2.controller.ts. Auth: 👤 Org:admin+.
| Method | Path | Line |
|---|---|---|
| POST | /api/payroll/protection-policies | 84 |
| POST | /api/payroll/protection-policies/:id/activate | 106 |
| GET | /api/payroll/protection-policies | 118 |
| POST | /api/payroll/one-time-earnings | 144 |
| POST | /api/payroll/one-time-earnings/:id/approve | 173 |
| POST | /api/payroll/one-time-earnings/:id/void | 185 |
| POST | /api/payroll/one-time-earnings/:id/reverse | 202 |
| GET | /api/payroll/one-time-earnings | 218 |
| POST | /api/payroll/overtime-policies | 251 |
| POST | /api/payroll/overtime-policies/:id/activate | 278 |
| GET | /api/payroll/overtime-policies | 290 |
| POST | /api/payroll/overtime-entries | 317 |
| POST | /api/payroll/overtime-entries/:id/approve | 351 |
| GET | /api/payroll/overtime-entries | 363 |
13. Payroll — adjustments
@Controller('payroll/adjustments'). Auth: 👤 Org:admin+.
| Method | Path | Line |
|---|---|---|
| POST | /api/payroll/adjustments | 59 |
| POST | /api/payroll/adjustments/:id/approve | 89 |
| POST | /api/payroll/adjustments/:id/post | 101 |
| POST | /api/payroll/adjustments/:id/reverse | 113 |
| POST | /api/payroll/adjustments/:id/void | 132 |
| GET | /api/payroll/adjustments | 148 |
| GET | /api/payroll/adjustments/:id | 168 |
14. Payroll — deduction mandates
@Controller('payroll/deduction-mandates'). Auth: 👤 Org:admin+.
| Method | Path | Line |
|---|---|---|
| POST | /api/payroll/deduction-mandates | 57 |
| POST | /api/payroll/deduction-mandates/:id/activate | 91 |
| POST | /api/payroll/deduction-mandates/:id/suspend | 103 |
| POST | /api/payroll/deduction-mandates/:id/resume | 119 |
| POST | /api/payroll/deduction-mandates/:id/cancel | 131 |
| GET | /api/payroll/deduction-mandates | 147 |
| GET | /api/payroll/deduction-mandates/:id | 165 |
15. Payroll — tenant settings + auto-lock policy
| Method | Path | Auth | File:line |
|---|---|---|---|
| POST | /api/payroll/tenant-settings | 👤 Org:admin+ | tenant-settings.controller.ts:80 |
| GET | /api/payroll/tenant-settings | 👤 Org:admin+ | :102 |
| GET | /api/payroll/tenant-settings/effective | 👤 Org:admin+ | :116 |
| DELETE | /api/payroll/tenant-settings | 👤 Org:admin+ | :128 |
| POST | /api/payroll/auto-lock-policy | 👤 Org:admin+ | auto-lock-policy.controller.ts:52 |
| GET | /api/payroll/auto-lock-policy | 👤 Org:admin+ | :83 |
| GET | /api/payroll/auto-lock-policy/effective | 👤 Org:admin+ | :97 |
| DELETE | /api/payroll/auto-lock-policy | 👤 Org:admin+ | :119 |
16. Payroll — PDF payslips
@Controller('payroll'). Source: apps/api/src/payroll/payroll-pdf.controller.ts. Auth: 👤 Org:admin+.
| Method | Path | Line |
|---|---|---|
| GET | /api/payroll/:id/payslips/:employeeId.pdf | 51 |
| GET | /api/payroll/:id/payslips.zip | 100 |
17. Payroll — court-order remit
@Controller('payroll'). Source: apps/api/src/payroll/consumers/court-order-remit.controller.ts. Auth: 👤 Org:admin+ (POST also 🪙 Idem).
| Method | Path | Line |
|---|---|---|
| GET | /api/payroll/:id/report.court-orders.json | 44 |
| GET | /api/payroll/:id/report.court-orders.xml | 58 |
| POST | /api/payroll/:id/report.court-orders/submit | 86 |
Background poller: CourtOrderAutoSubmitService, state at /api/health/payroll.
18. Payroll — platform-admin overrides
@Controller('platform/payroll'). Auth: 🛡 Platform.
| Method | Path | Line |
|---|---|---|
| POST | /api/platform/payroll/auto-lock-policy/:tenantId | 48 |
| GET | /api/platform/payroll/auto-lock-policy/:tenantId | 75 |
| DELETE | /api/platform/payroll/auto-lock-policy/:tenantId | 91 |
| GET | /api/platform/payroll/auto-lock-policies | 103 |
| GET | /api/platform/payroll/tenant-settings | 118 |
| POST | /api/platform/payroll/tenant-settings/:tenantId | 137 |
| GET | /api/platform/payroll/tenant-settings/:tenantId | 160 |
| DELETE | /api/platform/payroll/tenant-settings/:tenantId | 174 |
19. Payroll — health
@Controller('health/payroll'), @Public(). Source: apps/api/src/payroll/payroll-health.controller.ts.
| Method | Path | Line |
|---|---|---|
| GET | /api/health/payroll | 17 — {ready, healthy, crons[], generatedAt}. Crons: payroll-auto-lock, court-order-auto-submit, payroll-deductions-poller. |
20. BNPL
Status: Planned. packages/bnpl/ not yet scaffolded. Domain sketch:
Aggregates. Agreement (DRAFT → ACCEPTED → ACTIVE → SETTLED / DEFAULTED). Instalment (one per scheduled date).
ADR fit. Same idempotency + outbox model as lending; ledger postings via the same LedgerPort shape. No derived outstanding column — derived from the ledger (ADR-006).
Storage. bnpl_agreement, bnpl_instalment, bnpl_merchant_partner. Money columns: NUMERIC(20,0) in santim.
Routes (planned).
| Method | Path | Notes |
|---|---|---|
| POST | /api/bnpl/quotes | Affordability + plan offer. |
| POST | /api/bnpl/agreements | 🪙 Idem — accept + sign. |
| GET | /api/bnpl/agreements | List per tenant. |
| GET | /api/bnpl/agreements/:id | |
| POST | /api/bnpl/agreements/:id/installments/:idx/collect | 🪙 Idem — payroll-pull or wallet-debit. |
| POST | /api/bnpl/agreements/:id/cancel | Pre-activation only. |
| POST | /api/bnpl/agreements/:id/restructure | Operator-only. |
| GET | /api/me/bnpl/agreements | Self-service. |
| POST | /api/integration/merchant-callback/:partner | 🔓 Public + HMAC; merchant settlement. |
21. Wallet
Status: Planned read-only over the ledger. Per ADR-006 there is no Wallet aggregate — the ledger is source of truth. The wallet surface is a thin read view (SELECT … FROM ledger_entry WHERE account_id = …).
Routes (planned).
| Method | Path | Notes |
|---|---|---|
| GET | /api/wallet/balance | Derives from the ledger for the caller's wallet account. |
| GET | /api/wallet/transactions | Paginated; ?from=&to=&direction=. |
| GET | /api/me/wallet/balance | Self-service alias. |
| GET | /api/me/wallet/transactions | |
| POST | /api/wallet/transfer | 🪙 Idem — wallet→wallet (KYC + sanctions gated). Cross-domain choreography across ledger + integration-gateway. |
No new schema — re-uses ledger_account + ledger_entry.
22. Equb / Savings
Status: Planned. No code today.
Aggregates. EqubGroup, EqubMembership, EqubCycle. SavingsGoal, SavingsContribution.
ADR fit. Group payouts are choreographed wallet→wallet transfers; the ledger does the money work.
Storage. equb_group, equb_member, equb_cycle, savings_goal, savings_contribution. All NUMERIC(20,0) santim.
Routes (planned).
| Method | Path | Notes |
|---|---|---|
| POST | /api/equb/groups | Create. |
| GET | /api/equb/groups | List. |
| POST | /api/equb/groups/:id/join | |
| POST | /api/equb/groups/:id/leave | |
| POST | /api/equb/groups/:id/contribute | 🪙 Idem |
| POST | /api/equb/groups/:id/payout/:cycle | 🪙 Idem — selected member receives the pot. |
| GET | /api/equb/groups/:id/cycles | |
| POST | /api/savings/goals | Personal savings goal. |
| POST | /api/savings/goals/:id/deposit | 🪙 Idem |
| POST | /api/savings/goals/:id/withdraw | 🪙 Idem |
| GET | /api/me/savings/goals | Self-service. |
23. Notifications
Status: Stub. The Go services/notifications only exposes /health. No HTTP surface in apps/api/ yet.
Planned consumer behaviour. Subscribes to payroll.deductions_taken.v1, kyc.approved.v1, kyc.rejected.v1, kyc.more_info_requested.v1 (NEW), loan.disbursed.v1, ewa.disbursed.v1. Dispatches SMS / email / push per per-user preferences.
Routes (planned).
| Method | Path | Notes |
|---|---|---|
| GET | /api/notifications/preferences | Caller's channel prefs. |
| PUT | /api/notifications/preferences | Toggle SMS / email / push per category. |
| POST | /api/notifications/test | 🛡 Platform — send a test message. |
| GET | /api/me/notifications | Caller's inbox (read view over Kafka topic). |
Audit findings (this pass)
All six findings from the prior pass are closed. New follow-ups surfaced during fixes:
/me/ewa/requests+/me/payslipsare blocked on repo extensions. Each is onefindByEmployeemethod on the relevant repository — same shape as the newLoanRepository.findByTenant({borrowerId}). Mechanical.- No DB migration verification for
bank_webhook_event. The migration is shipped (20260601000000_bank_webhook_event) but the CI smoke (payroll-poller-integrationworkflow) doesn't include a non-payroll table sanity check. Add averify-bank-webhook-migration.shand call it from CI. request-more-infore-usesrejectionReasonslot for the applicant-facing note. Acceptable for the pilot but a dedicatedreviewerNotecolumn will be cleaner — tracked alongside the reviewer-notes refactor.- Tenant-level 2FA enforcement — the plugin is wired per-user only. A tenant policy ("every admin must have 2FA") needs a new table and a guard.
- EWA / payroll list endpoints still missing (see Tier-2 follow-up in §9, §11).
- No supertest coverage for the new replay + me routes. Each is small; add specs alongside the existing
platform-admin.supertest.spec.tspattern.
Maintenance
- Every new controller / new route MUST add a row here in the same PR.
- When a
Plannedrow in a Gaps block ships, move it into the table above and update the count in the Module index. - Re-run the audit script periodically:
grep -rn '@Controller\|@Get\|@Post\|@Put\|@Patch\|@Delete' apps/api/src packages/*/backend/presentationshould match this file row-for-row.