Payment Architecture
Related docs:
Checkout Flow·Order Lifecycle·Security Compliance·Fraud Detection
1. Overview
Pakashop uses a dual-provider, orchestrated payment architecture with integrated fraud detection:
| Provider | Role |
|---|---|
| PawaPay | Primary mobile money: MTN, Airtel, Zamtel deposits, vendor payouts, refunds |
| Flutterwave | Card payments (Visa/Mastercard) via hosted page; mobile money failover |
| MockAdapter | Local development / integration tests (PAYMENT_PROVIDER=MOCK) |
All provider interactions are hidden behind a unified internal API surface:
App code
└── PaymentService (facade)
└── PaymentOrchestrator (routing + failover)
├── PawaPayAdapter
├── FlutterwaveAdapter
└── MockAdapter
└── FraudService (parallel evaluation)
2. Payment Flow Diagrams
2.1 Mobile Money (PawaPay — Happy Path)
2.2 Card Payment (Flutterwave — SAQ A)
2.3 Mobile Money Failover (PawaPay → Flutterwave)
2.4 Fraud Block Flow
3. Provider Details
3.1 PawaPay
| Aspect | Detail |
|---|---|
| Supported MNOs | MTN Zambia, Airtel Zambia, Zamtel |
| Deposit mechanism | USSD push — customer receives prompt and enters PIN |
| Payout mechanism | POST /payouts — disbursement to vendor MNO wallet |
| Refund mechanism | POST /refunds — reversal to customer wallet |
| Availability check | GET /availability — checked before every deposit attempt |
| Webhook events | deposit.completed, deposit.failed, payout.completed, refund.completed |
| Webhook security | HMAC-SHA256 over raw body; secret from PAWAPAY_WEBHOOK_SECRET |
| Sandbox URL | https://api.sandbox.pawapay.io |
| Production URL | https://api.pawapay.io |
Key environment variables:
PAWAPAY_API_KEY=your_api_key
PAWAPAY_BASE_URL=https://api.sandbox.pawapay.io
PAWAPAY_WEBHOOK_SECRET=your_hmac_secret
3.2 Flutterwave
| Aspect | Detail |
|---|---|
| Card payments | Hosted payment page — PCI-DSS SAQ A compliant |
| MoMo payments | POST /charges?type=mobile_money_zambia |
| Transfers (payouts) | POST /transfers — vendor disbursement |
| Refunds | POST /transactions/{id}/refund |
| Webhook events | charge.completed, transfer.completed |
| Webhook security | verif-hash header compared to FLUTTERWAVE_SECRET_HASH |
| Redirect URL | Backend redirects to /payment/callback?orderId=... after card payment |
Key environment variables:
FLUTTERWAVE_SECRET_KEY=FLWSECK-...
FLUTTERWAVE_SECRET_HASH=your_verif_hash
FLUTTERWAVE_BASE_URL=https://api.flutterwave.com/v3
FLUTTERWAVE_REDIRECT_URL=https://pakashop.store/payment/callback
4. Webhook Processing
4.1 Idempotency
Every webhook event is deduplicated via the WebhookEvent table:
model WebhookEvent {
id String @id @default(cuid())
gateway String // 'PAWAPAY' | 'FLUTTERWAVE'
reference String // depositId / tx_ref
event String // 'deposit.completed' etc.
payload Json
createdAt DateTime @default(now())
@@unique([gateway, reference])
}
If an event with the same (gateway, reference) already exists, the handler returns early without processing.
4.2 Processing Steps (on deposit.completed / charge.completed)
- Verify HMAC / verif-hash signature.
- Insert
WebhookEvent(fail fast if duplicate). - Update
InboundPayment.status = CAPTURED. - Update
Order.paymentStatus = PAID,Order.status = CONFIRMED. - Calculate settlement amounts per
OrderItem(vendor amount, platform fee, VAT). - Set
OrderItem.settlementStatus = HELD. - Trigger ZRA Smart Invoice generation (if enabled).
- Log event via
ComplianceLogger(PII scrubbed).
5. Delayed Settlement Model
Pakashop does not hold customer funds directly (avoiding an escrow licence requirement). Instead:
- PawaPay/Flutterwave collects and holds the funds in their licensed wallets.
- Pakashop instructs disbursement only after delivery confirmation (PIN + digital signature).
- The settlement state machine is:
HELD → RELEASABLE → PAID_OUT
| State | Meaning | Trigger |
|---|---|---|
HELD | Funds in provider wallet; payout not yet authorised | Webhook confirms payment |
RELEASABLE | Admin or system confirms delivery | POST /api/v1/payments/release/:orderId or automatic on delivery confirmation |
PAID_OUT | Payout instruction sent to provider | SettlementLedger.disburseVendorPayouts() (batch job via pakashop-settlement) |
Fee calculation per order item:
vendorAmount = itemTotal × (1 - platformCommissionPct)
platformFee = itemTotal × platformCommissionPct
VAT = itemTotal × 0.16 (BoZ-mandated display; included in price)
PLATFORM_COMMISSION_PCT defaults to 0.05 (5%).
6. Idempotency
Every payment initiation generates an idempotency key:
// SHA-256 hash of (orderId + paymentMethod + timestamp_day)
const key = crypto.createHash('sha256')
.update(`${orderId}:${method}:${dayBucket}`)
.digest('hex');
If a duplicate initiation request arrives within the same day, the existing InboundPayment record is returned.
7. Frontend Payment Method Mapping
| Frontend ID | Backend enum | Gateway enum | Label |
|---|---|---|---|
MTN_MONEY | MTN | MTN | MTN Mobile Money |
AIRTEL_MONEY | AIRTEL | AIRTEL | Airtel Money |
ZAMTEL_MONEY | ZAMTEL | ZAMTEL | Zamtel Kwacha |
CARD | CARD | — | Visa / Mastercard |
8. Local Development
# Use mock provider (no real API calls)
PAYMENT_PROVIDER=MOCK
# MockAdapter returns PAID after 2 seconds
# All webhook flows are simulated
For sandbox testing with real providers, see the payment system README.md in services/backend/src/services/payment/.
For internal use only. Do not distribute outside Pakashop engineering.