Skip to main content

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:

ProviderRole
PawaPayPrimary mobile money: MTN, Airtel, Zamtel deposits, vendor payouts, refunds
FlutterwaveCard payments (Visa/Mastercard) via hosted page; mobile money failover
MockAdapterLocal 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

AspectDetail
Supported MNOsMTN Zambia, Airtel Zambia, Zamtel
Deposit mechanismUSSD push — customer receives prompt and enters PIN
Payout mechanismPOST /payouts — disbursement to vendor MNO wallet
Refund mechanismPOST /refunds — reversal to customer wallet
Availability checkGET /availability — checked before every deposit attempt
Webhook eventsdeposit.completed, deposit.failed, payout.completed, refund.completed
Webhook securityHMAC-SHA256 over raw body; secret from PAWAPAY_WEBHOOK_SECRET
Sandbox URLhttps://api.sandbox.pawapay.io
Production URLhttps://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

AspectDetail
Card paymentsHosted payment page — PCI-DSS SAQ A compliant
MoMo paymentsPOST /charges?type=mobile_money_zambia
Transfers (payouts)POST /transfers — vendor disbursement
RefundsPOST /transactions/{id}/refund
Webhook eventscharge.completed, transfer.completed
Webhook securityverif-hash header compared to FLUTTERWAVE_SECRET_HASH
Redirect URLBackend 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)

  1. Verify HMAC / verif-hash signature.
  2. Insert WebhookEvent (fail fast if duplicate).
  3. Update InboundPayment.status = CAPTURED.
  4. Update Order.paymentStatus = PAID, Order.status = CONFIRMED.
  5. Calculate settlement amounts per OrderItem (vendor amount, platform fee, VAT).
  6. Set OrderItem.settlementStatus = HELD.
  7. Trigger ZRA Smart Invoice generation (if enabled).
  8. Log event via ComplianceLogger (PII scrubbed).

5. Delayed Settlement Model

Pakashop does not hold customer funds directly (avoiding an escrow licence requirement). Instead:

  1. PawaPay/Flutterwave collects and holds the funds in their licensed wallets.
  2. Pakashop instructs disbursement only after delivery confirmation (PIN + digital signature).
  3. The settlement state machine is:
HELD → RELEASABLE → PAID_OUT
StateMeaningTrigger
HELDFunds in provider wallet; payout not yet authorisedWebhook confirms payment
RELEASABLEAdmin or system confirms deliveryPOST /api/v1/payments/release/:orderId or automatic on delivery confirmation
PAID_OUTPayout instruction sent to providerSettlementLedger.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 IDBackend enumGateway enumLabel
MTN_MONEYMTNMTNMTN Mobile Money
AIRTEL_MONEYAIRTELAIRTELAirtel Money
ZAMTEL_MONEYZAMTELZAMTELZamtel Kwacha
CARDCARDVisa / 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.