A. AmaniBank Ledger System (V1.0 — Double-Entry Cross-Border Ledger)

The AmaniBank ledger is the canonical system of financial truth across Sponsor → Beneficiary → Merchant → Settlement flows. It powers:

  • Funding (diaspora → PSP → AmaniBank reserve)
  • FX application & corridor conversion
  • Bundle allocation
  • Service redemption
  • Payout batching & settlement
  • Chargebacks & reversals
  • Risk-flagged adjustments

Every monetary action posts a set of debit/credit entries into corridor-isolated accounts. No balance is stored directly — balances are derived from ledger entries.

Why It Matters

Why AmaniBank Needs a Ledger

This is not a simple “payment record.” Diaspora-funded service bundles require:

  • Traceability across FX, bundles, and beneficiaries
  • Regulatory defensibility for audits
  • Chargeback-resistant accounting
  • Preventing double-spend or duplicate redemptions
  • Instant dispute decisions (proof-backed, ledger-verified)

This ledger model meets healthcare, education, and fintech compliance expectations across India, Kenya, GCC, US, and EU remittance corridors.

A.1 Ledger Philosophy (Design Principles)

  • Double-entry — every action has equal debit/credit.
  • Corridor isolation — INR, KES, PHP, MXN ledgers never mix.
  • Immutable entries — no updates; only append with reversal.
  • Idempotent operations — prevents duplicate funding or double redemption.
  • Derivable balances — account balances = SUM(entries).
  • Event-sourced — every business action → ledger event → postings.
  • Reconciliation-first — designed to match PSP transactions, settlement batches, and provider receivables.

A.2 Ledger Accounts (Chart of Accounts)

The ledger contains **five classes** of accounts per corridor:

Account Purpose Type
sponsor_fiat_balance USD/EUR/AUD/etc. sponsor funds before FX conversion Liability
corridor_reserve Converted local currency (INR/KES/etc.) held for beneficiary services Liability
bundle_allocation Value reserved for specific bundles assigned to a beneficiary Liability
merchant_receivable Amount owed to a merchant after successful redemption & proof Liability
settlement_payable Funds queued for payout via local rails Liability
chargeback_liability Amount held for chargeback exposure Contra-Liability

All balances are derived from:

```text SELECT SUM(debit) - SUM(credit) FROM ledger_entries WHERE account = X;

A.5 Ledger Architecture — Visual Diagram

Sponsor PSP / FX AmaniBank Corridor Ledger (INR/KES/PHP/MXN) Bundle Allocation Merchant Settlement Payable → Local Rails

The diagram shows the movement of value across the ledger at each key step: FUND → FX_APPLY → ALLOCATE → REDEEM → SETTLE.

---

A.6 Ledger SQL Schema (Production-Ready)

Table: ledger_accounts

CREATE TABLE ledger_accounts (
  id UUID PRIMARY KEY,
  account_code TEXT NOT NULL,           -- e.g., sponsor_fiat_balance
  owner_type TEXT NOT NULL,             -- sponsor | corridor | merchant
  owner_id UUID,                        -- nullable for corridor accounts
  currency TEXT NOT NULL,               -- USD, INR, KES, PHP, MXN
  corridor TEXT,                        -- e.g., "IN-US", "KE-US"
  created_at TIMESTAMP DEFAULT NOW()
);

Table: ledger_entries

CREATE TABLE ledger_entries (
  id UUID PRIMARY KEY,
  account_id UUID REFERENCES ledger_accounts(id),
  event_type TEXT NOT NULL,             -- FUND | ALLOCATE | REDEEM | ...
  debit NUMERIC(18,2) DEFAULT 0,
  credit NUMERIC(18,2) DEFAULT 0,
  reference_id UUID,                    -- order_id | bundle_id | proof_id
  metadata JSONB,
  created_at TIMESTAMP DEFAULT NOW()
);

Table: ledger_events

CREATE TABLE ledger_events (
  id UUID PRIMARY KEY,
  event_type TEXT NOT NULL,            -- FUND, FX_APPLY, etc.
  actor_id UUID,                       -- sponsor/merchant/admin
  corridor TEXT,
  amount NUMERIC(18,2),
  currency TEXT,
  fx_rate NUMERIC(18,6),
  psp_tx_id TEXT,
  created_at TIMESTAMP DEFAULT NOW()
);

Table: reconciliation_runs

CREATE TABLE reconciliation_runs (
  id UUID PRIMARY KEY,
  run_type TEXT NOT NULL,              -- psp | merchant | internal
  items_checked INT,
  mismatches INT,
  status TEXT,                         -- success | failed | partial
  created_at TIMESTAMP DEFAULT NOW()
);
---

A.7 Ledger API Endpoints

POST /v1/ledger/event

Posts a ledger event and generates all corresponding debit/credit entries.

{
  "event_type": "ALLOCATE",
  "corridor": "US-KE",
  "amount": 4200,
  "currency": "KES",
  "reference_id": "bundle-uuid"
}

GET /v1/ledger/account/{id}/balance

Returns derived balance from entries.

GET /v1/ledger/events

Paginates ledger feed; used by Admin portal.

POST /v1/ledger/reconcile/psp

Triggers PSP → ledger reconciliation run.

---

A.8 Idempotency & Concurrency Control

All monetary actions must be protected via idempotency keys to prevent double posting.

Idempotency-Key: FUND-sponsor-UUID-psp-txid

Within database transactions:

  • Use SERIALIZABLE isolation (or REPEATABLE READ).
  • Lock ledger_events row before posting entries.
  • Reject repeats with same idempotency key.
---

A.9 Reconciliation Engine

The reconciliation engine runs on a schedule and via manual trigger.

PSP Reconciliation

  • Fetch PSP charges/refunds
  • Match with ledger_events.FUND
  • Mark mismatches for manual review

Merchant Reconciliation

  • Match PROOF_CONFIRMED → SETTLE events
  • Check payout batch totals
  • Identify unclaimed redemptions

Internal Balance Sheet Reconciliation

  • Sum liabilities = sum corridor balances
  • Spot orphaned entries
  • Detect FX inconsistencies
---

A.10 Corridor Isolation Rules

To meet regulatory clarity across India, Kenya, Philippines, and Mexico:

  • No INR ledger touches KES/PHP/MXN.
  • Each corridor has its own reserve, merchant_receivable, settlement accounts.
  • Admin UI visually separates corridors.
  • Cross-corridor reporting is aggregated, not merged.

12. Chargebacks & Disputes (Card Funding)

In diaspora-funded services, the highest operational risk comes from card chargebacks (sponsor disputes a card payment with their bank) after AmaniBank has already:

  • Allocated coverage to a beneficiary bundle.
  • Allowed services to be consumed at a merchant.
  • Potentially paid out the merchant via local rails.

AmaniBank must maintain a clear, auditable chargeback pipeline that:

  • Captures PSP disputes (webhooks + polling).
  • Links them to the original orders, ledger_events, and bundles.
  • Executes ledger reversals in a controlled way.
  • Supports decisions: accept, contest, write-off, recover from merchant.
  • Feeds into the Risk & Fraud Engine (Section 13).

12.1 Chargeback Flow — Visual Overview

Sponsor Bank Card issuer, dispute initiated Card Network Visa / Mastercard / etc. PSP (Rapyd / DLocal) Acquirer, dispute webhook AmaniBank Dispute & Ledger Engine Maps PSP dispute → chargeback → ledger reversal Sponsor & Bundle Funding order, coverage allocations Merchant / Provider Redeemed services, settlement history Admin Console Dispute queue, evidence, decisions Flow: Sponsor disputes card → issuer → network → PSP → AmaniBank webhook → internal chargeback record → ledger reversal & optional merchant recovery → admin decision & risk signals.
Key idea: The chargeback object in AmaniBank is separate from PSP disputes but linked to them, mapped back to the original orders and bundles, and always reconciled via the ledger.

12.2 Database Tables for Chargebacks & Disputes

Table: psp_disputes

Stores raw dispute information coming from the PSP (via webhook or polling).

CREATE TABLE psp_disputes (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  psp_name TEXT NOT NULL,               -- 'rapyd', 'dlocal', etc.
  psp_dispute_id TEXT NOT NULL,         -- PSP's dispute identifier
  psp_charge_id TEXT NOT NULL,          -- underlying charge/payment id
  psp_status TEXT NOT NULL,             -- open, needs_response, won, lost...
  amount_minor BIGINT NOT NULL,         -- in  minor units
  currency TEXT NOT NULL,               -- PSP charge currency
  reason_code TEXT,                     -- fraud, product_not_received, etc.
  raw_payload JSONB NOT NULL,           -- entire webhook payload for audit
  received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  last_updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE (psp_name, psp_dispute_id)
);
    

Table: chargebacks

Canonical internal chargeback record, mapped to orders and ledger events.

CREATE TABLE chargebacks (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  psp_dispute_id UUID REFERENCES psp_disputes(id),
  order_id UUID NOT NULL REFERENCES orders(id),
  sponsor_id UUID NOT NULL REFERENCES users(id),
  beneficiary_id UUID REFERENCES beneficiaries(id),
  corridor TEXT NOT NULL,               -- e.g., 'US-KE'
  amount_minor BIGINT NOT NULL,         -- always in sponsor funding currency minor units
  currency TEXT NOT NULL,               -- e.g., 'USD'
  stage TEXT NOT NULL,                  -- intake | review | evidence | decision | closed
  status TEXT NOT NULL,                 -- open | accepted | rejected | written_off | recovered
  recovery_source TEXT,                 -- sponsor | merchant | platform_reserve
  reason_code TEXT,                     -- normalized reason
  network_deadline_at TIMESTAMPTZ,      -- last day to submit evidence
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
    

Table: chargeback_events

Timeline of actions and status changes for each chargeback.

CREATE TABLE chargeback_events (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  chargeback_id UUID NOT NULL REFERENCES chargebacks(id),
  event_type TEXT NOT NULL,             -- opened | evidence_uploaded | accepted | rejected | recovered
  actor_type TEXT NOT NULL,             -- system | admin | risk_engine
  actor_id UUID,                        -- admin user id if applicable
  note TEXT,
  metadata JSONB,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
    

Table: dispute_evidence

Documents and metadata used when responding to card network disputes.

CREATE TABLE dispute_evidence (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  chargeback_id UUID NOT NULL REFERENCES chargebacks(id),
  evidence_type TEXT NOT NULL,          -- receipt, visit_note, chat_log, ip_log, etc.
  s3_key TEXT NOT NULL,                 -- S3 location of PDF/image/etc.
  mime_type TEXT NOT NULL,
  uploaded_by UUID,                     -- admin id
  uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
    
Developer note: keep psp_disputes very close to the raw PSP schema, and normalize only at the chargebacks layer. This makes it easier to debug discrepancies with the PSP.

12.3 PSP Webhook → Chargeback Creation Flow

When the PSP notifies you of a dispute:

  1. PSP sends webhook → POST /v1/webhooks/psp/chargeback.
  2. Backend validates signature and PSP.
  3. Insert / upsert record in psp_disputes.
  4. Lookup matching orders row using psp_charge_id.
  5. Create or update chargebacks row.
  6. Emit chargeback_events entry (type=opened).
  7. Emit risk signal to Risk Engine (Section 13).

Example webhook handler (pseudo TypeScript)

POST /v1/webhooks/psp/chargeback
  - Verify PSP signature
  - Parse payload → psp_dispute_id, psp_charge_id, amount, reason, status
  - UPSERT into psp_disputes
  - Find order by psp_charge_id (store PSP charge id in orders.payment_reference)
  - If no local order → log for manual triage
  - Else:
      - Create or update chargebacks row linked to order + sponsor + corridor
      - Insert chargeback_events 'opened' or 'status_changed'
      - Notify admin (notifications_outbox)
      - Push event to risk engine (e.g., 'CHARGEBACK_OPENED')
    

12.4 Ledger Reversal Strategy for Chargebacks

Chargebacks must be reflected in the ledger but never by deleting or editing the original events. Instead, create compensating entries:

  • Original FUND event: sponsor_fiat_balance (credit), corridor_reserve (debit).
  • Original ALLOCATE and REDEEM events: bundle balances, merchant receivables.

When a chargeback is accepted or lost:

  1. Create a ledger_event of type CHARGEBACK_LOSS.
  2. Post entries reversing the relevant portion of the FUND in the sponsor/corridor accounts.
  3. Optionally, post a RECOVERY_FROM_MERCHANT event if a merchant recovery is pursued.
  4. Freeze or reduce the affected bundle(s) for the beneficiary.
  5. Emit notifications to Sponsor, Merchant (if impacted), and Admin.

Example: partial chargeback on a $200 funding order

  • Original FUND: +200 to sponsor_fiat_balance, -200 from PSP settlement balance.
  • Chargeback of 100:
    • CHARGEBACK_LOSS: -100 from sponsor_fiat_balance, +100 to loss_reserve.
    • Bundle coverage reduced by 100 equivalent, or future usage blocked.

Implement this as a single transaction:

  • Insert in ledger_events.
  • Insert in ledger_entries.
  • Update chargebacks.status and chargebacks.recovery_source.

12.5 Admin Workflows & APIs

The Admin console needs a dedicated Chargebacks & Disputes section in the left nav, powered by these endpoints:

Method Path Description
GET /v1/chargebacks List chargebacks with filters: stage, status, corridor, PSP, reason.
GET /v1/chargebacks/:id Details including linked order, ledger excerpts, events, evidence.
POST /v1/chargebacks/:id/evidence Upload evidence (generates pre-signed S3 URL, then saves dispute_evidence row).
POST /v1/chargebacks/:id/decision Record decision: accept / contest / write_off / recover_from_merchant, triggers ledger events and PSP response where applicable.
POST /v1/chargebacks/:id/notes Add internal note (stored as chargeback_events with type=note).

Admin UI flow (high level)

  • Queue view – list of open chargebacks with badges: corridor, amount, stage, PSP.
  • Detail view – timeline, linked funding order, redemptions, settlement info, risk signals, buttons: “Accept loss”, “Contest with evidence”, “Recover from merchant”.
  • Actions:
    • “Accept loss” → ledger reversal, status=written_off.
    • “Recover from merchant” → ledger entries + merchant notification.
    • “Contest” → upload evidence, send to PSP, track PSP outcome.

12.6 Sponsor & Merchant Experience Around Chargebacks

AmaniBank should maintain the Diaspora Sponsor Promise (Section 14) while still protecting itself and merchants from abuse:

Sponsor app behavior

  • Show a clear timeline: Payment → Coverage → Service Completed → Dispute (if applicable).
  • Expose friendly, non-jargon explanation of chargeback status.
  • For serial abusers, the Risk engine may:
    • Require stronger KYC.
    • Limit funding amounts or frequency.
    • Restrict corridors or merchants.

Merchant app behavior

  • Show if a redeemed service is under chargeback review.
  • Expose amount potentially at risk (but avoid panicking merchant).
  • Support secure upload of additional proof (visit notes, test results, etc.).
  • Allow merchants to flag suspected beneficiary fraud (feeds Risk engine).

All of these behaviors rely on the backend chargeback tables and APIs above — no extra new tables required on the merchant or sponsor side; only additional fields and views.

13. Risk & Fraud Engine

AmaniBank must protect against fraud and abuse while keeping the diaspora experience simple and trustworthy. The Risk & Fraud Engine is a separate layer that consumes events (funding, redemption, KYC, chargebacks) and produces:

  • Real-time decisions: allow / review / block.
  • Persistent risk profiles for sponsors, beneficiaries, merchants, devices.
  • Cases for manual review.
  • Signals that feed pricing, velocity limits, and KYC depth.

13.1 Risk Engine – Visual Overview

Funding / Top-ups Sponsor card charges Redemptions & Settlements Merchant bundle usage KYC, Sanctions, Chargebacks Sponsor/merchant history Risk & Fraud Engine Rules + thresholds + simple ML scores Emits: risk_signals, risk_profiles, risk_cases Real-time Decisions allow / review / block + limits, extra KYC, manual review risk_profiles (sponsor / beneficiary / merchant / device) risk_cases (manual review queue) risk_signals (velocity, device, behavioral flags)
Key idea: The Risk engine is called by internal backend handlers (funding, redemption, KYC, chargeback updates), not by frontend apps directly. Frontend only sees friendly messages ("We need extra info", "This payment needs review") based on decisions.

13.2 Database Tables for Risk & Fraud

Table: risk_profiles

Current risk posture per entity (sponsor, beneficiary, merchant, device).

CREATE TABLE risk_profiles (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  entity_type TEXT NOT NULL,           -- sponsor | beneficiary | merchant | device
  entity_id TEXT NOT NULL,             -- user_id, merchant_id, or device_id
  risk_score INT NOT NULL,             -- 0-100
  risk_tier TEXT NOT NULL,             -- low | medium | high | blocked
  reason_summary TEXT,                 -- short human summary
  last_event_at TIMESTAMPTZ,
  last_reviewed_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE (entity_type, entity_id)
);
    

Table: risk_signals

Fine-grained risk facts emitted by rules and events.

CREATE TABLE risk_signals (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  entity_type TEXT NOT NULL,           -- sponsor | beneficiary | merchant | device
  entity_id TEXT NOT NULL,
  signal_type TEXT NOT NULL,           -- high_velocity_funding, new_device, cb_ratio_high...
  signal_value TEXT,                   -- e.g. "5 events in 1 day"
  weight INT NOT NULL DEFAULT 1,       -- relative impact on score
  source TEXT NOT NULL,                -- risk_rule_id, job name, etc.
  metadata JSONB,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
    

Table: risk_cases

Manual review queue for Ops / Compliance.

CREATE TABLE risk_cases (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  entity_type TEXT NOT NULL,
  entity_id TEXT NOT NULL,
  title TEXT NOT NULL,
  status TEXT NOT NULL DEFAULT 'open',   -- open | in_review | closed
  severity TEXT NOT NULL,                -- low | medium | high | critical
  opened_reason TEXT,
  opened_by TEXT NOT NULL,               -- 'system' or admin user id
  assigned_to UUID,                      -- admin user
  metadata JSONB,
  opened_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  closed_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
    

Table: device_insights

Device/browser-level risk context for sponsors and merchants.

CREATE TABLE device_insights (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  device_id TEXT NOT NULL,             -- hashed device fingerprint
  user_id UUID,                        -- optional link to users.id
  first_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  trust_score INT NOT NULL DEFAULT 50, -- 0-100
  last_ip INET,
  last_country TEXT,
  last_user_agent TEXT,
  flags JSONB                          -- e.g. {"vpn_suspected": true}
);
    

13.3 Risk Evaluation API (Internal)

Provide an internal API (server-to-server) that handlers can call before finalizing actions such as funding or redemption:

POST /internal/risk/evaluate
Body:
{
  "scenario": "funding",
  "sponsorId": "s_123",
  "beneficiaryId": "b_123",
  "merchantId": null,
  "corridor": "US-KE",
  "amountMinor": 500000,   // in sponsor currency
  "deviceId": "dev_hash",
  "ip": "203.0.113.5"
}

Response:
{
  "decision": "allow",     // allow | review | block
  "riskScore": 34,
  "riskTier": "low",
  "reasons": ["good_history", "no_recent_chargebacks"]
}
    

Backend usage pattern:

  1. Handler builds risk payload and calls /internal/risk/evaluate.
  2. Risk engine aggregates signals and profile to return decision.
  3. Handler:
    • allow → proceed with PSP charge or redemption.
    • review → create risk_cases row, put transaction on hold, inform user.
    • block → deny action, optionally escalate KYC or lock account.
Developer instruction: implement risk rules as simple composable functions (e.g. "velocity", "chargeback ratio", "device trust") that each emit risk_signals and contribute to a numeric score. Do not overcomplicate – a few strong rules will outperform an unfinished ML model at MVP.

14. Diaspora Sponsor Promise

This section is written so you can reuse it directly in decks, landing pages, and app copy. It explains, in simple human terms, what AmaniBank guarantees to diaspora sponsors.

The AmaniBank Diaspora Sponsor Promise

“Every dollar you send turns into real care — not cash leakage.”

  • Service, not cash: Your payment never becomes loose cash. It becomes pre-approved services – clinic visits, diagnostics, tuition, or essentials – redeemable only at vetted partners.
  • Locked to the right person: Each bundle is tied to a specific beneficiary you choose. Coverage is protected by identity checks and secure codes/QR.
  • Proof after every visit: After each redeemed service, you receive a simple summary (e.g., “Visit completed”, “Lab test done”) and a copy of the visit note or receipt when appropriate.
  • No surprise withdrawals: Beneficiaries and merchants cannot withdraw your funds as cash or repurpose them outside the agreed services.
  • Transparent pricing & fees: You see what you pay in your card currency, what your family receives locally, and what AmaniBank earns, up front.
  • Always-on safeguards: Behind the scenes, a risk engine monitors for abuse, suspicious patterns, or chargeback misuse — protecting you, your family, and honest providers.

In one line for slides:
“AmaniBank turns diaspora dollars into verified care back home — never loose cash.”

Slide Layout Suggestion

For investor or sales decks, use this simple layout:

  • Left: Photo of a doctor + family (“Care funded from abroad”).
  • Right: Three bullets:
    • Locked to care — services, not cash.
    • Proof you can see — a visit summary for every redemption.
    • Risk-managed rails — PSP, FX, fraud checks baked in.

15. Recommended Missing Features & Backend Implementation

Below are critical features that are industry-standard for a cross-border service platform but have not yet been fully specified. The assumption: you will add the corresponding screens to Sponsor, Beneficiary, Merchant, and Admin apps, and wire them to the backend as noted.

15.1 Sponsor App – Recommended Additions

  • Saved payment methods & 3DS support
    Backend: create payment_methods table and add PSP tokenization fields; keep sensitive card data at PSP only.
  • Subscriptions / recurring funding
    Backend: create subscriptions table for auto-renew bundles (monthly chronic care, annual tuition). Scheduler / worker triggers /v1/payments/fund.
  • Statements & receipts
    Backend: expose /v1/sponsor/statement that composes from orders, ledger_events, and settlements, with filters (month, corridor, beneficiary).
  • Device & session management
    Backend: record device IDs in device_insights, add endpoints to revoke sessions / flag suspicious devices, reusing the Risk engine.

15.2 Beneficiary App – Recommended Additions

  • Appointment booking with merchants
    Backend: create appointments table tied to beneficiaries and merchants. Add endpoints /v1/appointments (CRUD). Allow merchants to confirm / reschedule.
  • Coverage & visit history
    Backend: extend orders with service_date and human-readable service summary. Expose /v1/beneficiaries/:id/history.
  • Consent & sharing preferences
    Backend: add a small beneficiary_preferences table (fields: share_details_with_sponsor, language, contact_opt_in).

15.3 Merchant / Provider App – Recommended Additions

  • Team members & roles (merchant-side RBAC)
    Backend: create merchant_users table (user id, merchant id, role: owner/admin/staff) and enforce in Merchant APIs (e.g. who can adjust claims or close the day).
  • Service catalog per merchant
    Backend: create merchant_services table that maps bundles to merchant-specific SKUs and pricing. This lets you capture co-pays or extras.
  • Daily close / reconciliation
    Backend: add endpoints to “close the day” per merchant (e.g. /v1/merchants/:id/daily-close) that aggregate redeemed orders and compare to the ledger / settlements.
  • Support for resubmissions / adjustments
    Backend: extend orders with parent_order_id for corrections and add a claim_status field (submitted, adjusted, denied).

15.4 Admin / Ops Console – Recommended Additions

  • Support tickets & notes
    Backend: create support_tickets table for issues raised by sponsors, beneficiaries, merchants. Integrate with notification_outbox for updates.
  • Feature flags & corridor configuration
    Backend: expand config_registry with typed keys for corridor-specific settings (limits, PSP routing, FX margin).
  • Analytics and cohort reporting
    Backend: add read-only endpoints /v1/metrics/* that query aggregated views (or materialized views) on top of core tables (GMV by corridor, sponsor retention, merchant utilization).
Implementation tip: treat each new feature as: (1) schema migration, (2) backend endpoints, (3) Admin visibility, (4) app UX. Keep table naming consistent; prefer UUIDs and created_at/updated_at everywhere.

16. Schema Reference – Core Tables & Fields

This section lists the remaining core tables with full fields and types. Combine with earlier CREATE TABLE snippets (bundles, notifications, ledger & chargeback tables, etc.) for a complete schema.

16.1 Users, Beneficiaries, Merchants

Table: users

CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  cognito_sub TEXT NOT NULL UNIQUE,
  email TEXT NOT NULL UNIQUE,
  phone TEXT,
  full_name TEXT,
  role TEXT NOT NULL,                 -- admin | sponsor | beneficiary | merchant
  country TEXT,
  status TEXT NOT NULL DEFAULT 'active', -- active | suspended | closed
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
    

Table: beneficiaries

CREATE TABLE beneficiaries (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id),  -- if they log in
  sponsor_id UUID NOT NULL REFERENCES users(id),
  full_name TEXT NOT NULL,
  date_of_birth DATE,
  national_id TEXT,
  relationship TEXT,                  -- parent, sibling, etc.
  country TEXT NOT NULL,
  city TEXT,
  preferred_language TEXT,
  status TEXT NOT NULL DEFAULT 'active',
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
    

Table: merchants

CREATE TABLE merchants (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  legal_name TEXT,
  country TEXT NOT NULL,
  city TEXT,
  address TEXT,
  contact_email TEXT,
  contact_phone TEXT,
  category TEXT,                      -- clinic, lab, school, etc.
  status TEXT NOT NULL DEFAULT 'pending', -- pending | active | suspended
  kyc_case_id UUID,                   -- optional link to kyc_cases
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
    

Table: merchant_users (for merchant RBAC)

CREATE TABLE merchant_users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  merchant_id UUID NOT NULL REFERENCES merchants(id),
  user_id UUID NOT NULL REFERENCES users(id),
  role TEXT NOT NULL,                 -- owner | admin | staff
  invited_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  accepted_at TIMESTAMPTZ,
  status TEXT NOT NULL DEFAULT 'active'
);
    

16.2 Orders, Settlements, Bank Transactions

Table: orders

CREATE TABLE orders (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  type TEXT NOT NULL,                 -- fund | redeem
  sponsor_id UUID REFERENCES users(id),
  beneficiary_id UUID REFERENCES beneficiaries(id),
  merchant_id UUID REFERENCES merchants(id),
  bundle_id UUID REFERENCES bundles(id),
  corridor TEXT NOT NULL,             -- e.g. US-KE
  amount_minor BIGINT NOT NULL,       -- in corridor or sponsor currency, define convention
  currency TEXT NOT NULL,             -- e.g. USD, KES
  status TEXT NOT NULL,               -- pending | succeeded | failed | cancelled
  service_date DATE,
  service_summary TEXT,
  payment_reference TEXT,             -- PSP charge id for fund orders
  claim_status TEXT,                  -- for redeem side: submitted | adjusted | denied
  parent_order_id UUID,               -- for adjustments
  has_proof BOOLEAN NOT NULL DEFAULT FALSE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
    

Table: order_proofs

CREATE TABLE order_proofs (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  order_id UUID NOT NULL REFERENCES orders(id),
  s3_key TEXT NOT NULL,
  mime_type TEXT NOT NULL,
  uploaded_by UUID REFERENCES users(id),
  uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
    

Table: settlements

CREATE TABLE settlements (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  merchant_id UUID NOT NULL REFERENCES merchants(id),
  corridor TEXT NOT NULL,
  currency TEXT NOT NULL,
  amount_minor BIGINT NOT NULL,
  status TEXT NOT NULL,               -- pending | sent | completed | failed
  settlement_date DATE,
  psp_payout_id TEXT,                 -- PSP payout reference
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
    

Table: bank_transactions (bank feed)

CREATE TABLE bank_transactions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  bank_reference TEXT NOT NULL,
  corridor TEXT,
  amount_minor BIGINT NOT NULL,
  currency TEXT NOT NULL,
  posted_at TIMESTAMPTZ NOT NULL,
  description TEXT,
  settlement_id UUID REFERENCES settlements(id),
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
    

16.3 KYC, Sanctions, Disputes, Audit WORM

Table: kyc_cases

CREATE TABLE kyc_cases (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  subject_type TEXT NOT NULL,         -- user | merchant
  subject_id UUID NOT NULL,
  provider_name TEXT NOT NULL,        -- e.g. onfido, sumsub
  provider_ref TEXT NOT NULL,         -- provider case id
  status TEXT NOT NULL,               -- pending | passed | failed | escalated
  risk_level TEXT,                    -- low | medium | high
  raw_payload JSONB,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
    

Table: sanctions

CREATE TABLE sanctions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  subject_type TEXT NOT NULL,         -- user | merchant | beneficiary
  subject_id UUID NOT NULL,
  list_name TEXT NOT NULL,            -- OFAC, UN, local list, etc.
  match_score NUMERIC(5,2) NOT NULL,
  status TEXT NOT NULL DEFAULT 'open', -- open | cleared | false_positive
  note TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
    

Table: disputes (non-card complaints)

CREATE TABLE disputes (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  order_id UUID REFERENCES orders(id),
  raised_by UUID REFERENCES users(id),
  type TEXT NOT NULL,                 -- service_quality | delay | billing
  status TEXT NOT NULL DEFAULT 'open',
  description TEXT,
  resolution TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
    

Table: audit_worm

CREATE TABLE audit_worm (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  entity_type TEXT NOT NULL,          -- config | kyc | sanctions | settlement | risk_case
  entity_id TEXT NOT NULL,
  action TEXT NOT NULL,               -- override, approve, reject, update
  actor_id UUID,                      -- admin id
  detail JSONB,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
    

16.4 Config, Notifications, Ledger (Summary)

Table: config_registry

CREATE TABLE config_registry (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  key TEXT NOT NULL UNIQUE,
  value_json JSONB NOT NULL,
  description TEXT,
  updated_by UUID,
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
    

Notifications, ledger & chargeback tables were defined in earlier sections:

  • notification_templates, notification_outbox (Section 10).
  • ledger_accounts, ledger_events, ledger_entries (Section 11.0).
  • psp_disputes, chargebacks, chargeback_events, dispute_evidence (Section 12).
  • risk_profiles, risk_signals, risk_cases, device_insights (Section 13).

16.5 Payments, Subscriptions, Appointments, Support

Table: payment_methods

CREATE TABLE payment_methods (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(id),
  psp_name TEXT NOT NULL,             -- rapyd, stripe, etc.
  psp_payment_method_id TEXT NOT NULL,
  brand TEXT,                         -- Visa, Mastercard...
  last4 TEXT,
  exp_month INT,
  exp_year INT,
  is_default BOOLEAN NOT NULL DEFAULT FALSE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
    

Table: subscriptions

CREATE TABLE subscriptions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  sponsor_id UUID NOT NULL REFERENCES users(id),
  beneficiary_id UUID REFERENCES beneficiaries(id),
  bundle_id UUID NOT NULL REFERENCES bundles(id),
  corridor TEXT NOT NULL,
  status TEXT NOT NULL DEFAULT 'active', -- active | paused | cancelled
  billing_interval TEXT NOT NULL,        -- monthly | yearly
  next_billing_at TIMESTAMPTZ NOT NULL,
  last_billing_at TIMESTAMPTZ,
  payment_method_id UUID REFERENCES payment_methods(id),
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
    

Table: appointments

CREATE TABLE appointments (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  beneficiary_id UUID NOT NULL REFERENCES beneficiaries(id),
  merchant_id UUID NOT NULL REFERENCES merchants(id),
  bundle_id UUID REFERENCES bundles(id),
  scheduled_at TIMESTAMPTZ NOT NULL,
  status TEXT NOT NULL DEFAULT 'requested', -- requested | confirmed | completed | cancelled
  notes TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
    

Table: support_tickets

CREATE TABLE support_tickets (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  created_by UUID NOT NULL REFERENCES users(id),
  subject TEXT NOT NULL,
  description TEXT,
  status TEXT NOT NULL DEFAULT 'open', -- open | in_progress | resolved | closed
  priority TEXT NOT NULL DEFAULT 'medium',
  assigned_to UUID,
  related_entity_type TEXT,
  related_entity_id TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
    
Final developer note: Treat this section as your schema contract. Any new features should either reuse these tables or add new ones using the same conventions: UUID primary keys, created_at/updated_at, corridor awareness, and clear status fields.

05. Core APIs

Below is a minimal but complete set of endpoints that will allow you to wire all the existing screens in the Admin React portal, plus Sponsor / Beneficiary / Merchant flows. Keep URLs versioned (/v1/*) so you can evolve without breaking clients.

05.1

Bundles & Catalog

Bundles are the “products” sponsors buy and beneficiaries consume. They are shared across corridors but can be corridor-specific.

Method Path Description
GET /v1/bundles List all bundles (with filters: corridor, category, status).
GET /v1/bundles/:id Get full details for a specific bundle.
POST /v1/bundles Create new bundle (Admin only).
PATCH /v1/bundles/:id Update name / price / frequency / status.
PATCH /v1/bundles/:id/toggle Quick toggle active/disabled.
05.2

Orders & Redemptions

Orders represent funding (sponsor → platform) and redemption (beneficiary → merchant). They are the backbone for statements, settlements, and ledger.

Method Path Description
GET /v1/orders List orders (filters: type = fund/redeem, status, corridor, beneficiaryId, merchantId).
GET /v1/orders/:id Get details including linked proof, ledger entry refs.
POST /v1/orders Create order (used by Sponsor/Beneficiary/Merchant flows). Usually created indirectly via funding / redemption APIs.
PATCH /v1/orders/:id Update metadata such as service_date, service_summary, or claim_status.

Important: all money-impacting operations (fund, redeem, settlement, chargeback) should:

  • Create / update orders rows, and
  • Emit ledger_entries in the double-entry ledger (covered in Section 11 – Ledger & Chargebacks).
05.3

KYC / KYB & Sanctions

KYC/KYB events flow from your vendor (Onfido / Veriff / Sumsub / etc.) into kyc_cases, which are surfaced in the Admin console.

Method Path Description
GET /v1/kyc/cases List pending / approved / rejected KYC/KYB cases.
GET /v1/kyc/cases/:id Detail view including vendor payload and audit trail.
POST /v1/kyc/cases/:id/decision Approve/reject case (adds WORM audit record + updates subject status).
GET /v1/sanctions List sanctions hits (open / cleared / false positives).
POST /v1/sanctions/:id/clear Clear sanctions hit (with reason & actor id in audit_worm).
05.4

Settlements & Bank Feed

Settlements group many redemption orders into a payout to a merchant. Bank feed rows map real-world bank movements to those settlements.

Method Path Description
GET /v1/settlements List payout batches with filters (status, corridor, merchant).
GET /v1/settlements/:id Details including linked orders and PSP payout reference.
POST /v1/settlements Create a settlement batch from eligible redeemed orders (Admin / worker only).
GET /v1/bank-feed Imported bank transactions linked to settlements. Used for reconciliation.
05.5

Config, Integrations & Health

Configuration and integration health are what make the Admin console feel like a “control plane” instead of a static dashboard.

Method Path Description
GET /v1/config List configuration registry values (corridor limits, PSP routing).
PATCH /v1/config/:key Update single config entry (append WORM audit record).
GET /v1/integrations List PSP, KYC, FX, WhatsApp integration status and last check.
POST /v1/integrations/:id/rotate Rotate API key (Admin only; update Secrets Manager + config).
GET /v1/health/apis API latency metrics (p50, p95, error rate, by service).

06. Proof Upload Pipeline (S3)

When a merchant completes a service, they must upload proof (visit note, receipt, lab results). Admins and Sponsors should see whether proof is present for each redemption order.

Merchant Console “Upload proof” button Backend: pre-signed URL POST /v1/proof/upload-url S3: proof bucket amanibank-*-proof 1. Merchant calls /v1/proof/upload-url 2. Backend creates pre-signed URL + S3 key 3. Merchant uploads directly to S3 4. Backend saves proof metadata in DB: order_id, s3_key, mime_type, uploaded_at

6.1 API Contract

POST /v1/proof/upload-url
Authorization: Bearer <merchant-token>
Body:
{
  "orderId": "o2",
  "mimeType": "application/pdf"
}
Response:
{
  "uploadUrl": "https://amanibank-dev-proof.s3.amazonaws.com/...",
  "key": "proof/o2/1699999999-visit-note.pdf"
}
  

The merchant app then does a direct PUT to S3 using the pre-signed uploadUrl. After successful upload, the backend marks has_proof = true on the corresponding order and creates an order_proofs row.

Developer instruction: never expose your S3 credentials to the frontend. Always use pre-signed URLs and enforce size / mime-type limits when generating the URL.

07. Wiring the React Admin Console

Your Admin React code currently uses mock data (arrays like bundles, orders, settlements, etc.). The goal: replace those with real fetch calls to the backend, while preserving the UI and RBAC behavior.

Step A

Replace mock store with API hooks

  • Create a small api.ts (or api.js) module that wraps fetch with:
    • Base URL: https://api-dev.amanibank.com
    • Attaches Authorization: Bearer <token> from Cognito to each request.
    • JSON parsing & centralized error handling.
  • Replace each store slice with a React Query hook (or simple useEffect) that calls the real endpoints and populates local state.
  • Ensure loading states and error toasts surface backend issues instead of silently failing.
/* Example: bundles hook (pseudo-code) */
async function apiFetch(path, options = {}) {
  const token = localStorage.getItem("amanibank_id_token");
  const res = await fetch(`${API_BASE}${path}`, {
    ...options,
    headers: {
      "Content-Type": "application/json",
      Authorization: token ? `Bearer ${token}` : "",
      ...(options.headers || {})
    }
  });
  if (!res.ok) throw new Error(`API error ${res.status}`);
  return res.json();
}

export const fetchBundles = () => apiFetch("/v1/bundles");
Step B

Login Screen → Cognito

Current mock login:

  • Accepts admin@amanibank.com and just sets state.

Replace with:

  1. Call Cognito Hosted UI or InitiateAuth with email/password.
  2. Store ID token in memory or localStorage (short-lived) and refresh token securely (if used).
  3. On app mount, call /v1/auth/me to fetch user profile and role; store in a global context.
  4. Enforce role-based access on the frontend via the existing can("ACTION") helper.
Step C

Admin-only vs. Shared APIs

Some APIs are Admin-only (e.g. config, integrations, ledger inspection), while others are shared with Sponsor / Merchant apps (e.g. orders, settlements).

  • Use backend RBAC to enforce access on every request based on Cognito role and/or internal users.role.
  • In the Admin UI, hide dangerous controls (e.g. “Rotate PSP key”) for non-SuperAdmin users.
  • Emit audit_worm entries for config changes, KYC decisions, sanctions overrides, and settlement approvals.
Developer instruction: replace mock data gradually: start with Bundles & Orders, then Settlements & KYC, then Config/Health. Use feature flags (e.g. USE_API=true) during transition if needed.

08. Deployment Plan – One-Week Execution Roadmap

This is the high-level schedule assuming a junior developer working full-time with this blueprint.

Phase Breakdown

  • Day 1: Set up RDS schema, S3 buckets, Cognito user pool + roles, and a basic Node/TypeScript project (Express / Nest / Fastify).
  • Day 2: Implement core bundles and orders endpoints; wire Admin “Ops”, “Godsview”, and “Catalog” screens to API.
  • Day 3: Implement KYC, Sanctions, Disputes, and WORM audit endpoints; wire KYC / Sanctions screens.
  • Day 4: Implement settlements, bank feed, integrations, config, and health endpoints; connect Admin screens.
  • Day 5: Implement proof upload pipeline for merchants + admin view; harden RBAC, logging, error reporting.
  • Day 6–7: Buffer for QA, bug-fixes, and deployment to prod with CloudFront / API Gateway in front.

09. Final Checklists – What “Done” Means

Backend “Done”

  • All tables created in RDS; migrations committed to repo (repeatable in dev & prod).
  • All listed endpoints (/v1/bundles, /v1/orders, /v1/kyc/cases, /v1/settlements, /v1/config, /v1/health/apis, etc.) implemented and tested with Postman/Insomnia.
  • Proof uploads successfully land in S3 with correct keys and order_proofs rows.
  • RBAC enforced on backend (admins only for sensitive routes).
  • Errors logged to CloudWatch with correlation IDs where possible (e.g. x-request-id).
  • Ledger & chargeback tables wired (Section 11–12) so every money movement has a corresponding ledger entry.

Admin Console “Done”

  • No more hard-coded mock arrays in React state.
  • All screens load data via API and show real RDS content (even if dummy test data).
  • Admin login uses Cognito; logout clears tokens and state.
  • Team & Access screen reads/writes real user records and invite tokens (if implemented).
  • FX banner and corridor badges pull from a real config or /v1/fx/quote-style endpoint.
  • Risk- and ledger-related warnings render using live data (e.g., unsettled balances, pending chargebacks).
Hand-off note to developer: As you implement, keep this page open and tick off each section. If you need to add new endpoints or tables, keep them consistent with the patterns in this blueprint so the system stays coherent and auditable.

10. Notifications & Messaging

AmaniBank uses email and WhatsApp for critical events across Sponsors, Beneficiaries, Merchants, and Admin/Ops. Everything should flow through a notifications outbox so channels can be added or changed without touching core business logic.

10.1

Notification Catalogue

Minimum set for MVP (including your original two):

Persona Notification Trigger Channel Priority
Sponsor Service(s) funded – code for Beneficiary Sponsor successfully funds bundle(s) for a beneficiary Email + WhatsApp High
Sponsor Service rendered confirmation Merchant completes redemption & uploads proof Email + WhatsApp High
Sponsor Funding failed / payment declined PSP charge error Email + WhatsApp High
Sponsor Bundle expiring / unused Bundle near expiry or unused for N days Email Medium
Sponsor Subscription renewed Recurring payment success Email (+ optional WhatsApp) Medium
Sponsor Subscription payment failed (dunning) Recurring payment failed Email + WhatsApp High
Sponsor New beneficiary linked Beneficiary app ties code/QR to sponsor account Email Medium
Beneficiary New coverage available New funding order for this beneficiary Email + WhatsApp High
Beneficiary Appointment reminder (optional) Scheduled service at T-24h Email + WhatsApp Medium
Merchant Proof missing / overdue Redeem order without proof after X hours Email High
Admin/Ops High-risk alert created Risk engine rule triggers case Email (Slack later) High
Admin/Ops Settlement batch sent Settlement status → sent/completed Email Medium
Admin/Ops Integration degraded Health check above latency/error thresholds Email High
10.2

DB Tables for Notifications

Create two core tables in Postgres:

  • notification_templates – defines text per type and channel.
  • notification_outbox – queue of messages to send (pending → sent / failed).
CREATE TABLE notification_templates (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  type TEXT NOT NULL,                -- e.g. FUNDING_SUCCESS
  channel TEXT NOT NULL,             -- email, whatsapp
  subject TEXT,                      -- for email
  body TEXT NOT NULL,                -- template with {{placeholders}}
  whatsapp_template_name TEXT,       -- provider template id
  is_active BOOLEAN NOT NULL DEFAULT TRUE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE notification_outbox (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  type TEXT NOT NULL,
  channel TEXT NOT NULL,
  recipient TEXT NOT NULL,           -- email or whatsapp phone
  payload_json JSONB NOT NULL,       -- data for template rendering
  status TEXT NOT NULL DEFAULT 'pending', -- pending | sent | failed
  retries INT NOT NULL DEFAULT 0,
  last_error TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  sent_at TIMESTAMPTZ
);
10.3

Backend Pattern (Emit Then Deliver)

Step 1 – Emit events: inside core business handlers (funding, redemption, KYC, settlements) insert rows into notification_outbox instead of calling SES/Twilio directly.

-- Example: sponsor funding success
INSERT INTO notification_outbox (type, channel, recipient, payload_json)
VALUES (
  'FUNDING_SUCCESS',
  'email',
  'sponsor@example.com',
  jsonb_build_object(
    'sponsorName', 'Jane',
    'beneficiaryName', 'Amina',
    'bundleName', 'Chronic Care Management',
    'code', 'XYZ123'
  )
);

Step 2 – Worker / Lambda: a scheduled job (or SQS-triggered Lambda) reads all pending rows, loads the matching template from notification_templates, renders the message with the JSON payload, sends via:

  • AWS SES for channel = 'email'
  • WhatsApp provider (e.g., Twilio API) for channel = 'whatsapp'
UPDATE notification_outbox
SET status = 'sent',
    sent_at = now()
WHERE id = <id>;
Sponsor Beneficiary Merchant Admin / Ops Notification Outbox & Channels DB outbox → SES (email) / WhatsApp API → Delivery logs Example: Sponsor funds → Outbox emits FUNDING_SUCCESS to Sponsor & Beneficiary; Merchant completes service → Outbox emits SERVICE_COMPLETED to Sponsor & Admin.
Developer instruction: any time you add a new business event (new order type, new risk rule, new KYC outcome), define a type and emit one or more notification_outbox rows. Do not call SES/Twilio directly inside business logic – always go through the outbox pattern so notifications remain observable, retryable, and auditable.

12. Ledger & Chargebacks – Core Financial Spine

The ledger is the single source of truth for all value movements in AmaniBank. Every money-impacting event (funding, redemption, settlement, refund, chargeback) must create a balanced set of ledger entries (double-entry).

12.1

Ledger Conceptual Model

  • ledger_accounts – logical buckets of value (per sponsor, per merchant, platform fees, PSP clearing).
  • ledger_entries – debits and credits that move value between accounts, grouped in balanced sets.
  • chargebacks – card network disputes and refunds, linked to orders and ledger entry groups.

Balancing rule: for each business event (e.g., funding), sum of credits = sum of debits across all affected ledger accounts, tied together via entry_group_id.

12.2

Key Tables & DDL

ledger_accounts

CREATE TABLE ledger_accounts (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  owner_type TEXT NOT NULL,       -- sponsor | beneficiary | merchant | platform | psp
  owner_id UUID,                  -- FK to users/merchants/etc when applicable
  corridor TEXT NOT NULL,         -- e.g. KE, IN, PH, MX
  currency TEXT NOT NULL,         -- e.g. USD, KES, INR
  category TEXT NOT NULL,         -- sponsor_funds | merchant_payable | fee | psp_clearing
  status TEXT NOT NULL DEFAULT 'active', -- active | frozen | closed
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

ledger_entries

CREATE TABLE ledger_entries (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  entry_group_id UUID NOT NULL,        -- links debits & credits for the same event
  ledger_account_id UUID NOT NULL REFERENCES ledger_accounts(id),
  order_id UUID,                       -- nullable; links to orders when relevant
  settlement_id UUID,                  -- nullable; links to settlements
  chargeback_id UUID,                  -- nullable; links to chargebacks
  direction TEXT NOT NULL,             -- debit | credit
  amount_minor BIGINT NOT NULL,        -- in minor units for currency
  currency TEXT NOT NULL,
  corridor TEXT NOT NULL,
  description TEXT,
  posted_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

chargebacks

CREATE TABLE chargebacks (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  psp_charge_id TEXT NOT NULL,         -- PSP reference
  order_id UUID NOT NULL,              -- disputed funding order
  sponsor_id UUID NOT NULL,            -- who made the payment
  merchant_id UUID,                    -- affected merchant (if applicable)
  corridor TEXT NOT NULL,
  amount_minor BIGINT NOT NULL,
  currency TEXT NOT NULL,
  status TEXT NOT NULL DEFAULT 'pending', -- pending | won | lost | cancelled
  reason_code TEXT,                    -- network reason code / PSP reason
  opened_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  closed_at TIMESTAMPTZ,
  raw_psp_payload JSONB,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
12.3

Mapping Flows → Ledger Entries

Example A – Sponsor Funding (Card Charge)

  • Event: sponsor funds a chronic-care bundle.
  • Order: orders.type = 'fund'.
  • Accounts impacted:
    • Sponsor funds account (per corridor).
    • Platform sponsor liability / unearned revenue account.
    • Platform fee revenue account (if fee is taken upfront).
    • PSP clearing account (optional, for reconciliation).

Backend pattern: inside /v1/payments/fund after PSP success:

BEGIN;

-- 1. create orders row
INSERT INTO orders (...) VALUES (...);

-- 2. create ledger entry group
INSERT INTO ledger_entries (...)
VALUES (...multiple rows debiting/crediting relevant accounts...);

COMMIT;

Example B – Merchant Redemption

  • Event: merchant redeems bundle for beneficiary.
  • Order: orders.type = 'redeem'.
  • Accounts impacted:
    • Sponsor funds account (credit reduces sponsor balance).
    • Merchant payable account (debit increases amount owed).

Example C – Settlement Payout

  • Event: payout to merchant bank/MPesa for a settlement batch.
  • Accounts impacted:
    • Merchant payable account (credit reduces payable).
    • Platform cash / PSP clearing account (debit reduces cash).
12.4

Chargebacks & Refunds

Card chargebacks must be fully visible and reversible in the ledger. The safest pattern: treat a chargeback as its own event (linked to the original funding order), with its own entry_group_id.

  1. PSP notifies you of a dispute on a funding transaction.
  2. Create a chargebacks row with status = 'pending'.
  3. Optionally, temporarily freeze the corresponding sponsor and merchant ledger accounts if risk is high.
  4. When the dispute is lost, emit a ledger entry group that:
    • Debits platform cash / PSP clearing (money leaving AmaniBank).
    • Credits sponsor funds account (reversing their funded balance), or records a sponsor receivable (if service already rendered).
    • Optionally debits merchant payable to claw back unsettled funds or create a negative balance (merchant owes AmaniBank).
  5. When the dispute is won, mark chargebacks.status = 'won' and emit entries only if the PSP had temporarily withdrawn funds.
Developer instruction: all chargeback logic lives in a dedicated module (chargebackService.ts) that:
  • applies rules for freezing accounts,
  • creates chargebacks rows, and
  • emits balanced ledger entries.
Admin UI should surface these via a “Chargebacks” tab linked to chargebacks + ledger_entries.

13. Risk & Fraud Engine – Rules, Profiles, Cases

The risk engine protects against abusive flows (stolen cards, coupon abuse, synthetic identities, merchant collusion) and flags anomalies for manual review. For MVP, implement a rules-based engine with clear auditability.

13.1

Conceptual Components

  • risk_rules – configuration for what to watch (e.g. “>3 funding attempts per card per hour”).
  • risk_events – normalized signals (login, payment attempt, KYC result, device change).
  • risk_profiles – aggregated view per subject (sponsor, beneficiary, merchant, device).
  • risk_cases – escalations that show up in Admin “Risk & Ops” tabs.
13.2

Risk Tables & DDL

risk_rules

CREATE TABLE risk_rules (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  code TEXT NOT NULL UNIQUE,         -- e.g. CARD_VELOCITY_1H
  description TEXT NOT NULL,
  subject_type TEXT NOT NULL,        -- sponsor | beneficiary | merchant | device | payment_method
  expression TEXT NOT NULL,          -- DSL or JSON describing condition
  threshold NUMERIC(10,2),           -- optional numeric threshold
  severity TEXT NOT NULL DEFAULT 'medium', -- low | medium | high
  enabled BOOLEAN NOT NULL DEFAULT TRUE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

risk_events

CREATE TABLE risk_events (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  subject_type TEXT NOT NULL,
  subject_id TEXT NOT NULL,          -- can be UUID or external id
  event_type TEXT NOT NULL,          -- LOGIN, FUND_ATTEMPT, REDEEM, KYC_RESULT, etc.
  source TEXT NOT NULL,              -- api | console | psp_webhook | kyc_webhook
  risk_score_delta NUMERIC(10,2) DEFAULT 0,
  details JSONB,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

risk_profiles

CREATE TABLE risk_profiles (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  subject_type TEXT NOT NULL,
  subject_id TEXT NOT NULL,
  aggregate_score NUMERIC(10,2) NOT NULL DEFAULT 0,
  kyc_status TEXT,                   -- not_started | pending | approved | rejected
  pep_flag BOOLEAN DEFAULT FALSE,
  sanctions_flag BOOLEAN DEFAULT FALSE,
  last_event_at TIMESTAMPTZ,
  last_reviewed_at TIMESTAMPTZ,
  notes TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

risk_cases

CREATE TABLE risk_cases (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  title TEXT NOT NULL,
  subject_type TEXT NOT NULL,
  subject_id TEXT NOT NULL,
  rule_code TEXT NOT NULL,           -- which rule triggered this
  severity TEXT NOT NULL,            -- low | medium | high
  status TEXT NOT NULL DEFAULT 'open', -- open | in_review | closed
  assigned_to UUID,                  -- admin user id
  opened_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  closed_at TIMESTAMPTZ,
  resolution TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
13.3

How the Risk Engine Runs

  1. Ingest signals: whenever key events happen (login, payment fund, redemption, KYC), write a risk_events row with relevant details (subject_type, subject_id, event_type, details).
  2. Evaluate rules: a scheduled Lambda (or inline hook) reads recent risk_events and applies risk_rules (SQL, simple DSL, or TypeScript functions).
  3. Update profile: recompute risk_profiles.aggregate_score, set flags (e.g. sanctions_flag), and optionally freeze accounts or require extra proof.
  4. Create cases: when a rule triggers above its severity threshold, create a risk_cases row and emit an Admin notification (HIGH_RISK_ALERT).
  5. Admin console: Risk tab lists open cases, shows timelines from risk_events, and lets Ops mark cases as closed with resolution notes.
Developer instruction: implement the first 3–5 rules as simple SQL/TypeScript checks:
  • Too many failed funding attempts per card per hour.
  • Multiple beneficiaries sharing the same device or email.
  • New merchant with unusually high redemption velocity.
  • Beneficiary whose KYC is rejected but still attempting redemptions.
Keep rules explicit and auditable before moving to ML.

14. Diaspora Sponsor Promise – Reusable Narrative Block

This section is designed to be copy-pasted into marketing pages, decks, and the Sponsor app. It explains, in plain language, what AmaniBank guarantees to diaspora sponsors.

Sponsor Promise

The AmaniBank Diaspora Sponsor Promise

When you fund care, school, or essentials for someone back home, AmaniBank makes three simple promises:

1. Every Shilling/Peso/Rupee is Tracked

Your money never disappears into “family logistics”. Every transaction is logged in a bank-grade ledger, matched to a specific bundle, beneficiary, merchant, and visit date.

  • Itemized statements for each person you support.
  • Proof of service (visit notes, receipts, results) attached.
  • No cash withdrawals into the unknown.

2. Services, Not Just Money

You’re not sending money. You’re unlocking care: clinic visits, diagnostics, tuition, and essentials at vetted local providers.

  • Pre-defined bundles (chronic care, annual checkups, tuition terms).
  • Only approved merchants can redeem your bundles.
  • Beneficiaries see coverage, not balances to spend on anything.

3. Fair FX & Transparent Fees

No surprise charges. No mystery exchange rates. We show you the effective rate and fee before you fund.

  • Upfront display of FX, fees, and what the provider receives.
  • Corridor-specific partners (banks, PSPs) for reliability.
  • An always-available record for your own budgeting and taxes.

Tagline for decks: “You send care, not cash.” AmaniBank handles the hard parts: FX, compliance, risk checks, local payouts, and proof that your loved ones actually received what you paid for.

15. Missing Features & Full Schema Catalog

This final section does two things:

  1. Highlights critical functionalities still missing from Sponsor / Beneficiary / Merchant / Admin apps (per best practice).
  2. Provides a consolidated schema catalog of all tables and fields referenced across this blueprint.
15.1

Recommended Missing Functionalities

Sponsor App – Missing but Important

  • Account statement & export: monthly/annual statements per beneficiary (PDF/CSV).
  • Per-beneficiary budgets & limits: set monthly/annual caps and categories (health vs tuition).
  • Dispute & support center: guided flow to raise concerns about a visit, service quality, or suspected fraud.
  • Multi-factor authentication (MFA): optional OTP / authenticator for large funding events.
  • Saved payment methods & 3DS support: reduce friction while maintaining security.

Backend implementation notes:

  • Statements: use orders + ledger_entries views; optionally persist rendered PDFs in S3.
  • Budgets: add sponsor_limits (per sponsor/beneficiary/bundle).
  • Disputes: use disputes table (below) and link to orders; route to Admin “Support” queue.
  • MFA: either integrate with Cognito MFA or custom OTP table tied to high-value flows.

Beneficiary App – Missing but Important

  • Appointment scheduling: pick providers, dates, and times (for clinics that support booking).
  • Visit history: list of visits, status, and any follow-up recommendations.
  • Languages & localization: corridor-specific language support (e.g., Swahili/Hindi/Tagalog).
  • Safety & red-flag prompts: show instructions when a visit is urgent or canceled.

Backend implementation notes:

  • Appointment scheduling: add appointments table linked to beneficiaries, merchants, and orders (optional).
  • Visit history uses existing orders with type = 'redeem' + service_date.

Merchant App – Missing but Important

  • Service catalog & pricing: define which services they provide per bundle, with internal codes.
  • Booking & capacity management: control how many AmaniBank visits per day/slot.
  • Staff roles: users for cashier, clinician, and manager.
  • Payout visibility: upcoming settlements, historical payouts, and reconciliation tools.

Backend implementation notes:

  • Add merchant_services and merchant_staff tables.
  • Use settlements + ledger_entries to power a “Payouts” tab.

Admin Panel – Missing but Important

  • Risk dashboard: open risk cases, velocity alerts, sanctions flags.
  • Chargeback & dispute queue: end-to-end visibility and resolution workflows.
  • Ledger explorer: search by sponsor, merchant, order, or date, reading from ledger_entries.
  • Feature flags/config: toggle corridors, bundles, and risk rules without redeploying.
15.2

Schema Catalog – All Core Tables

This catalog consolidates all tables required for the MVP described in this blueprint. Use Postgres on AWS RDS.

users

CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  cognito_sub TEXT NOT NULL UNIQUE,
  email TEXT NOT NULL UNIQUE,
  phone TEXT,
  full_name TEXT,
  role TEXT NOT NULL,              -- admin | sponsor | beneficiary | merchant
  status TEXT NOT NULL DEFAULT 'active', -- active | disabled | invited
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

beneficiaries

CREATE TABLE beneficiaries (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id),
  sponsor_id UUID REFERENCES users(id),
  external_ref TEXT,               -- e.g. hospital MRN
  country TEXT NOT NULL,
  city TEXT,
  date_of_birth DATE,
  notes TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

merchants

CREATE TABLE merchants (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id),      -- owner/admin
  legal_name TEXT NOT NULL,
  trading_name TEXT,
  corridor TEXT NOT NULL,
  country TEXT NOT NULL,
  city TEXT,
  address TEXT,
  bank_account_details JSONB,
  mpesa_paybill TEXT,
  status TEXT NOT NULL DEFAULT 'pending', -- pending | active | suspended
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

bundles

CREATE TABLE bundles (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  code TEXT NOT NULL UNIQUE,
  name TEXT NOT NULL,
  corridor TEXT NOT NULL,
  category TEXT NOT NULL,          -- Healthcare, Tuition, etc.
  frequency TEXT NOT NULL,         -- Monthly, Annual, OneTime
  price_minor BIGINT NOT NULL,
  currency TEXT NOT NULL,
  margin_pct NUMERIC(5,2) NOT NULL,
  description TEXT,
  is_active BOOLEAN NOT NULL DEFAULT TRUE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

orders

CREATE TABLE orders (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  type TEXT NOT NULL,              -- fund | redeem
  sponsor_id UUID REFERENCES users(id),
  beneficiary_id UUID REFERENCES beneficiaries(id),
  merchant_id UUID REFERENCES merchants(id),
  bundle_id UUID REFERENCES bundles(id),
  corridor TEXT NOT NULL,
  amount_minor BIGINT NOT NULL,
  currency TEXT NOT NULL,
  status TEXT NOT NULL,            -- pending | succeeded | failed | cancelled
  service_date DATE,
  service_summary TEXT,
  psp_charge_id TEXT,              -- for fund orders
  has_proof BOOLEAN NOT NULL DEFAULT FALSE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

order_proofs

CREATE TABLE order_proofs (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  order_id UUID NOT NULL REFERENCES orders(id),
  s3_key TEXT NOT NULL,
  mime_type TEXT NOT NULL,
  uploaded_by UUID REFERENCES users(id),
  uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

settlements

CREATE TABLE settlements (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  merchant_id UUID NOT NULL REFERENCES merchants(id),
  corridor TEXT NOT NULL,
  currency TEXT NOT NULL,
  total_amount_minor BIGINT NOT NULL, -- sum of included orders
  status TEXT NOT NULL DEFAULT 'pending', -- pending | sent | completed | payout_failed
  psp_payout_id TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

bank_feed

CREATE TABLE bank_feed (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  settlement_id UUID REFERENCES settlements(id),
  bank_txn_id TEXT,
  amount_minor BIGINT NOT NULL,
  currency TEXT NOT NULL,
  value_date DATE,
  description TEXT,
  matched BOOLEAN NOT NULL DEFAULT FALSE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

kyc_cases

CREATE TABLE kyc_cases (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  subject_type TEXT NOT NULL,      -- sponsor | beneficiary | merchant
  subject_id UUID NOT NULL,
  vendor TEXT NOT NULL,            -- Onfido, Veriff, etc.
  vendor_case_id TEXT NOT NULL,
  status TEXT NOT NULL,            -- pending | approved | rejected
  risk_level TEXT,                 -- low | medium | high
  raw_payload JSONB,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

sanctions

CREATE TABLE sanctions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  subject_type TEXT NOT NULL,
  subject_id UUID NOT NULL,
  provider TEXT NOT NULL,          -- provider name
  hit_details JSONB,
  status TEXT NOT NULL DEFAULT 'open', -- open | cleared | false_positive
  cleared_by UUID REFERENCES users(id),
  cleared_reason TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

disputes

CREATE TABLE disputes (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  order_id UUID NOT NULL REFERENCES orders(id),
  opened_by UUID REFERENCES users(id),
  reason TEXT NOT NULL,
  status TEXT NOT NULL DEFAULT 'open', -- open | in_review | resolved
  resolution TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

audit_worm

CREATE TABLE audit_worm (
  id BIGSERIAL PRIMARY KEY,
  entity_type TEXT NOT NULL,       -- config | kyc_case | sanctions | settlement | risk_case
  entity_id TEXT NOT NULL,
  action TEXT NOT NULL,            -- created | updated | approved | rejected | cleared
  actor_id UUID REFERENCES users(id),
  details JSONB,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

config_registry

CREATE TABLE config_registry (
  key TEXT PRIMARY KEY,
  value_json JSONB NOT NULL,
  description TEXT,
  updated_by UUID REFERENCES users(id),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

notification_templates & notification_outbox

(Already defined in Section 10, repeated here for completeness.)

-- notification_templates
CREATE TABLE notification_templates (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  type TEXT NOT NULL,
  channel TEXT NOT NULL,
  subject TEXT,
  body TEXT NOT NULL,
  whatsapp_template_name TEXT,
  is_active BOOLEAN NOT NULL DEFAULT TRUE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- notification_outbox
CREATE TABLE notification_outbox (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  type TEXT NOT NULL,
  channel TEXT NOT NULL,
  recipient TEXT NOT NULL,
  payload_json JSONB NOT NULL,
  status TEXT NOT NULL DEFAULT 'pending',
  retries INT NOT NULL DEFAULT 0,
  last_error TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  sent_at TIMESTAMPTZ
);

ledger_accounts, ledger_entries, chargebacks

(Defined in Section 12; included in catalog above.)

risk_rules, risk_events, risk_profiles, risk_cases

(Defined in Section 13; included in catalog above.)

Developer instruction: keep these DDL files under version control using a migration tool (Prisma / Knex / Sequelize / Flyway). Any new feature (e.g. appointments, merchant_services) should add its own table following the same naming and timestamp conventions.