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 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
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
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, -- inminor 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()
);
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:
- PSP sends webhook →
POST /v1/webhooks/psp/chargeback. - Backend validates signature and PSP.
- Insert / upsert record in
psp_disputes. - Lookup matching
ordersrow usingpsp_charge_id. - Create or update
chargebacksrow. - Emit
chargeback_eventsentry (type=opened). - 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:
- Create a
ledger_eventof typeCHARGEBACK_LOSS. - Post entries reversing the relevant portion of the FUND in the sponsor/corridor accounts.
- Optionally, post a
RECOVERY_FROM_MERCHANTevent if a merchant recovery is pursued. - Freeze or reduce the affected bundle(s) for the beneficiary.
- 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.statusandchargebacks.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.
- “Accept loss” → ledger reversal, status=
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
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:
- Handler builds risk payload and calls
/internal/risk/evaluate. - Risk engine aggregates signals and profile to return decision.
- Handler:
- allow → proceed with PSP charge or redemption.
- review → create
risk_casesrow, put transaction on hold, inform user. - block → deny action, optionally escalate KYC or lock account.
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: createpayment_methodstable and add PSP tokenization fields; keep sensitive card data at PSP only. -
Subscriptions / recurring funding
Backend: createsubscriptionstable for auto-renew bundles (monthly chronic care, annual tuition). Scheduler / worker triggers/v1/payments/fund. -
Statements & receipts
Backend: expose/v1/sponsor/statementthat composes fromorders,ledger_events, andsettlements, with filters (month, corridor, beneficiary). -
Device & session management
Backend: record device IDs indevice_insights, add endpoints to revoke sessions / flag suspicious devices, reusing the Risk engine.
15.2 Beneficiary App – Recommended Additions
-
Appointment booking with merchants
Backend: createappointmentstable tied tobeneficiariesandmerchants. Add endpoints/v1/appointments(CRUD). Allow merchants to confirm / reschedule. -
Coverage & visit history
Backend: extendorderswithservice_dateand human-readable service summary. Expose/v1/beneficiaries/:id/history. -
Consent & sharing preferences
Backend: add a smallbeneficiary_preferencestable (fields: share_details_with_sponsor, language, contact_opt_in).
15.3 Merchant / Provider App – Recommended Additions
-
Team members & roles (merchant-side RBAC)
Backend: createmerchant_userstable (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: createmerchant_servicestable 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: extendorderswithparent_order_idfor corrections and add aclaim_statusfield (submitted, adjusted, denied).
15.4 Admin / Ops Console – Recommended Additions
-
Support tickets & notes
Backend: createsupport_ticketstable for issues raised by sponsors, beneficiaries, merchants. Integrate withnotification_outboxfor updates. -
Feature flags & corridor configuration
Backend: expandconfig_registrywith 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).
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()
);
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.
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. |
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
ordersrows, and -
Emit
ledger_entriesin the double-entry ledger (covered in Section 11 – Ledger & Chargebacks).
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).
|
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. |
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.
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.
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.
Replace mock store with API hooks
-
Create a small
api.ts(orapi.js) module that wrapsfetchwith:- Base URL:
https://api-dev.amanibank.com -
Attaches
Authorization: Bearer <token>from Cognito to each request. - JSON parsing & centralized error handling.
- Base URL:
-
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");
Login Screen → Cognito
Current mock login:
- Accepts
admin@amanibank.comand just sets state.
Replace with:
-
Call Cognito Hosted UI or
InitiateAuthwith email/password. - Store ID token in memory or localStorage (short-lived) and refresh token securely (if used).
-
On app mount, call
/v1/auth/meto fetch user profile and role; store in a global context. -
Enforce role-based access on the frontend via the existing
can("ACTION")helper.
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_wormentries for config changes, KYC decisions, sanctions overrides, and settlement approvals.
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_proofsrows. - 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).
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.
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 | 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 | 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 | 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 | Medium | |
| Admin/Ops | Integration degraded | Health check above latency/error thresholds | High |
DB Tables for Notifications
Create two core tables in Postgres:
-
notification_templates– defines text pertypeandchannel. -
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
);
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>;
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).
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.
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() );
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).
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.
- PSP notifies you of a dispute on a funding transaction.
-
Create a
chargebacksrow withstatus = 'pending'. - Optionally, temporarily freeze the corresponding sponsor and merchant ledger accounts if risk is high.
-
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).
-
When the dispute is won, mark
chargebacks.status = 'won'and emit entries only if the PSP had temporarily withdrawn funds.
chargebackService.ts) that:
- applies rules for freezing accounts,
- creates
chargebacksrows, and - emits balanced ledger entries.
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.
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.
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() );
How the Risk Engine Runs
-
Ingest signals: whenever key events happen
(login, payment fund, redemption, KYC), write a
risk_eventsrow with relevant details (subject_type,subject_id,event_type,details). -
Evaluate rules: a scheduled Lambda (or inline
hook) reads recent
risk_eventsand appliesrisk_rules(SQL, simple DSL, or TypeScript functions). -
Update profile: recompute
risk_profiles.aggregate_score, set flags (e.g.sanctions_flag), and optionally freeze accounts or require extra proof. -
Create cases: when a rule triggers above its
severity threshold, create a
risk_casesrow and emit an Admin notification (HIGH_RISK_ALERT). -
Admin console: Risk tab lists open cases,
shows timelines from
risk_events, and lets Ops mark cases asclosedwith resolution notes.
- 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.
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.
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:
- Highlights critical functionalities still missing from Sponsor / Beneficiary / Merchant / Admin apps (per best practice).
- Provides a consolidated schema catalog of all tables and fields referenced across this blueprint.
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_entriesviews; optionally persist rendered PDFs in S3. -
Budgets: add
sponsor_limits(per sponsor/beneficiary/bundle). -
Disputes: use
disputestable (below) and link toorders; 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
appointmentstable linked tobeneficiaries,merchants, andorders(optional). -
Visit history uses existing
orderswithtype = '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_servicesandmerchant_stafftables. -
Use
settlements+ledger_entriesto 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.
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.)