Checkout Flow
Related docs:
Payment Architecture·Order Lifecycle·Loyalty & Coupons·Inventory System
1. Overview
The Pakashop checkout is a multi-step process composed as a single page with conditional rendering:
- Delivery Information — address collection.
- Order Summary — cart items, VAT breakdown, trust badges.
- Coupon & Loyalty — coupon code entry, loyalty points redemption.
- Payment Method — method selection, wallet number (MoMo), consent, and order submission.
The page assembles these components from frontend/src/components/checkout/:
| Component | Responsibility |
|---|---|
DeliveryForm.js | Zambian address form with all 10 provinces |
OrderSummary.js | Cart items, VAT breakdown, wholesale tier pricing, trust badges |
CouponInput.js | Coupon code validation and application |
LoyaltyRedemption.js | Points balance display and redemption |
PaymentMethodSelector.js | Animated grid of MTN / Airtel / Zamtel / Card options |
MobileMoneyOverlay.js | Full-screen USSD waiting overlay with polling |
2. Step-by-Step User Journey
3. Delivery Form
Collects Zambian-specific delivery details. All labels use "delivery" (never "shipping").
Fields:
| Field | Validation |
|---|---|
| First Name | Required |
| Last Name | Required |
| Required, valid format | |
| Mobile Number | Required (delivery contact) |
| Delivery Address | Required |
| City | Required |
| Province | Required — select from all 10 Zambian provinces |
| Country | Fixed: Zambia |
On submission, formData is passed to Step 2 and the wallet number field is pre-filled from formData.phone.
4. Order Summary
Displays a comprehensive breakdown of the order:
| Line Item | Calculation |
|---|---|
| Subtotal | Sum of (item price × quantity) |
| VAT (16%) | Subtotal × 0.16 |
| Delivery Fee | Calculated by pricing engine based on location |
| Coupon Discount | Applied if valid coupon code entered |
| Loyalty Points | Deducted if redeemed (1 point = K 0.01) |
| Total | Subtotal + VAT + Delivery Fee - Discount - Points |
Multi-vendor support: Each item shows its originating shop. Settlement is calculated per shop.
Wholesale tier pricing: If the customer qualifies for a wholesale tier (based on quantity), the discounted price is shown with a strikethrough on the standard price.
5. Coupon & Loyalty
5.1 Coupon Validation
// Frontend: validate coupon before order creation
const { valid, discount, message } = await validateCoupon(authFetch, {
code: couponCode,
cartItems: cart.items,
subtotal: cart.subtotal
});
if (valid) {
setAppliedCoupon({ code: couponCode, discount });
} else {
toast.error(message);
}
Coupon types:
PERCENTAGE: e.g., 10% offFIXED_AMOUNT: e.g., K 50 offFREE_DELIVERY: Waives delivery fee
Restrictions: min order amount, max discount, usage limit, expiry date, shop-specific, category-specific.
5.2 Loyalty Points Redemption
// Frontend: redeem loyalty points
const { pointsBalance, conversionRate } = await getLoyaltyPoints(authFetch, shopId);
const maxRedeemable = Math.min(pointsBalance, Math.floor(cart.subtotal * 0.5 / conversionRate));
- Maximum 50% of subtotal can be paid with points
- Points are shop-specific (earned and redeemed per shop)
- Points expire after 12 months of inactivity
6. Payment Method Selection
The PaymentMethodSelector renders a 2×2 grid of clickable cards:
| Method ID | Label | Type | Logo path |
|---|---|---|---|
MTN_MONEY | MTN Mobile Money | USSD push | /images/logos/.../mtn.svg |
AIRTEL_MONEY | Airtel Money | USSD push | /images/logos/.../airtel.svg |
ZAMTEL_MONEY | Zamtel Kwacha | USSD push | /images/logos/.../zamtel.png |
CARD | Visa / Mastercard | Redirect | visa.svg + mastercard.svg |
State: selectedMethod (default: MTN_MONEY).
7. Mobile Money Wallet Number
Conditionally rendered when selectedMethod !== 'CARD'.
- Label: "Enter Mobile Money Number"
- Pre-fill:
formData.phonefrom delivery form - Validation: Zambian number format (10 digits, starts with 097/096/095/077/076/075/095)
- Storage:
formData.mobileWalletNumber
8. DPA Consent Checkbox
Required for mobile money payments.
Text: "I confirm that the mobile money wallet is registered in my name and I agree to Pakashop's [Terms of Service]."
The checkbox enforces the Zambia Data Protection Act 2021 requirement for explicit consent before processing personal financial data.
9. Order Submission Logic
// 1. Create Order
const order = await createOrder(authFetch, {
items: cart.items.map(item => ({
productId: item.productId,
shopId: item.shopId,
quantity: item.quantity,
price: item.product.price,
variantId: item.variantId
})),
shippingAddress: { // kept for DB backward-compat; treated as delivery address
firstName, lastName, email, phone,
address, city, province, country: 'Zambia'
},
paymentMethod: selectedMethod, // e.g. 'MTN_MONEY'
couponCode: appliedCoupon?.code,
loyaltyPointsToRedeem: redeemedPoints,
total: calculatedTotal
});
// 2. Initiate Payment
const result = await initiatePayment(
authFetch,
order.id,
selectedMethod, // maps to backend enum: MTN | AIRTEL | ZAMTEL | CARD
mobileWalletNumber // omitted for CARD
);
// 3. Handle response
if (result.type === 'redirect') {
window.location.href = result.redirectUrl; // Flutterwave hosted page
} else {
showUssdOverlay(result.message);
startPolling(order.id);
}
Backend order creation flow:
- Validate cart items (stock availability, pricing)
- Apply coupon discount (if valid)
- Apply loyalty points redemption (if valid)
- Calculate VAT (16%)
- Calculate delivery fee (pricing engine)
- Create Order record (PENDING)
- Create OrderItem records (per shop)
- Reserve inventory (stock decrement)
- Return order with calculated total
10. USSD Polling Logic
const POLL_INTERVAL_MS = 5000; // 5 seconds
const POLL_TIMEOUT_MS = 3 * 60 * 1000; // 3 minutes
let attempts = 0;
const maxAttempts = POLL_TIMEOUT_MS / POLL_INTERVAL_MS; // 36
const poll = setInterval(async () => {
attempts++;
const { paymentStatus, fraudCheck } = await getPaymentStatus(authFetch, orderId);
if (paymentStatus === 'PAID') {
clearInterval(poll);
await clearCart();
router.push(`/orders/${orderId}`);
} else if (paymentStatus === 'FAILED') {
clearInterval(poll);
setUssdOverlay(false);
toast.error('Payment failed. Please try another method.');
} else if (fraudCheck?.status === 'BLOCKED') {
clearInterval(poll);
setUssdOverlay(false);
toast.error('Payment blocked for security review. Contact support.');
} else if (attempts >= maxAttempts) {
clearInterval(poll);
setUssdOverlay(false);
toast.error('Payment timed out. Check your order for updates.');
}
}, POLL_INTERVAL_MS);
11. Payment Callback Page (/payment/callback)
Landing page for card payment redirects. Also handles manual navigation for edge cases.
Logic:
- Extract
orderIdfromsearchParams. - If no
orderId: show generic error (mobile money payments are confirmed via polling on the checkout page, not here). - Call
GET /api/v1/payments/status/:orderIdonce. - If
PAID→ show success UI, clear cart, link to order. - If
PENDING→ start polling (3 s interval, 2 min timeout). - If
FAILED→ show failure UI, link back to checkout. - If
BLOCKED(fraud) → show security review message, link to support.
Success message: "Payment successful! Your order is being processed and will be delivered soon."
12. Error Handling
| Scenario | Behaviour |
|---|---|
| Order creation fails | Toast error, button re-enabled, user stays on checkout |
| Payment initiation fails | Toast with backend message, button re-enabled |
| Coupon invalid | Toast error, coupon cleared, user can retry |
| Loyalty points insufficient | Toast error, points field reset |
| Inventory insufficient | Toast error, out-of-stock items highlighted in cart |
| USSD timeout (3 min) | Overlay dismissed, link to order page for status check |
| Card redirect fails | User lands on callback page with FAILED/PENDING status |
| Fraud block | Payment blocked, user notified, admin review triggered |
| Network error during poll | Poll continues; errors logged to console |
For internal use only. Do not distribute outside Pakashop engineering.