Skip to main content

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:

  1. Delivery Information — address collection.
  2. Order Summary — cart items, VAT breakdown, trust badges.
  3. Coupon & Loyalty — coupon code entry, loyalty points redemption.
  4. Payment Method — method selection, wallet number (MoMo), consent, and order submission.

The page assembles these components from frontend/src/components/checkout/:

ComponentResponsibility
DeliveryForm.jsZambian address form with all 10 provinces
OrderSummary.jsCart items, VAT breakdown, wholesale tier pricing, trust badges
CouponInput.jsCoupon code validation and application
LoyaltyRedemption.jsPoints balance display and redemption
PaymentMethodSelector.jsAnimated grid of MTN / Airtel / Zamtel / Card options
MobileMoneyOverlay.jsFull-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:

FieldValidation
First NameRequired
Last NameRequired
EmailRequired, valid format
Mobile NumberRequired (delivery contact)
Delivery AddressRequired
CityRequired
ProvinceRequired — select from all 10 Zambian provinces
CountryFixed: 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 ItemCalculation
SubtotalSum of (item price × quantity)
VAT (16%)Subtotal × 0.16
Delivery FeeCalculated by pricing engine based on location
Coupon DiscountApplied if valid coupon code entered
Loyalty PointsDeducted if redeemed (1 point = K 0.01)
TotalSubtotal + 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% off
  • FIXED_AMOUNT: e.g., K 50 off
  • FREE_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 IDLabelTypeLogo path
MTN_MONEYMTN Mobile MoneyUSSD push/images/logos/.../mtn.svg
AIRTEL_MONEYAirtel MoneyUSSD push/images/logos/.../airtel.svg
ZAMTEL_MONEYZamtel KwachaUSSD push/images/logos/.../zamtel.png
CARDVisa / MastercardRedirectvisa.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.phone from delivery form
  • Validation: Zambian number format (10 digits, starts with 097/096/095/077/076/075/095)
  • Storage: formData.mobileWalletNumber

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:

  1. Validate cart items (stock availability, pricing)
  2. Apply coupon discount (if valid)
  3. Apply loyalty points redemption (if valid)
  4. Calculate VAT (16%)
  5. Calculate delivery fee (pricing engine)
  6. Create Order record (PENDING)
  7. Create OrderItem records (per shop)
  8. Reserve inventory (stock decrement)
  9. 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:

  1. Extract orderId from searchParams.
  2. If no orderId: show generic error (mobile money payments are confirmed via polling on the checkout page, not here).
  3. Call GET /api/v1/payments/status/:orderId once.
  4. If PAID → show success UI, clear cart, link to order.
  5. If PENDING → start polling (3 s interval, 2 min timeout).
  6. If FAILED → show failure UI, link back to checkout.
  7. 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

ScenarioBehaviour
Order creation failsToast error, button re-enabled, user stays on checkout
Payment initiation failsToast with backend message, button re-enabled
Coupon invalidToast error, coupon cleared, user can retry
Loyalty points insufficientToast error, points field reset
Inventory insufficientToast error, out-of-stock items highlighted in cart
USSD timeout (3 min)Overlay dismissed, link to order page for status check
Card redirect failsUser lands on callback page with FAILED/PENDING status
Fraud blockPayment blocked, user notified, admin review triggered
Network error during pollPoll continues; errors logged to console

For internal use only. Do not distribute outside Pakashop engineering.