Skip to content
This repository was archived by the owner on Sep 27, 2026. It is now read-only.

Payments

Syed Ahmer Shah edited this page Sep 27, 2026 · 1 revision

Payments

Cash at pickup is always available — the MarketLink brief asks for pay-at-the-stall, and GleanGrid keeps it. On top of that, customers can pay online with Easypaisa, JazzCash or a debit / credit card. Each method can be switched off in Admin → Settings.

The flow

  1. Checkout reserves the stock. An online checkout creates the orders (one per stall) and one payments row for the whole checkout, then holds the stock for a payment window (default 20 minutes, adjustable from 5 to 120 in Admin → Settings). Farmers aren't notified and can't accept until it's paid.
  2. The payment page. Cards: a 3D card that tilts with the pointer and flips for the CVC. Wallets: a phone that shows the approval request. A countdown shows how long the stock is held.
  3. Paid. Every order in the checkout becomes paid, farmers get their New pre-order e-mail, and the customer gets a receipt.
  4. Not paid in time. gleangrid:expire-payments (scheduled every minute) releases the stock and returns any coupon.
  5. Refunds are automatic when the customer cancels before the cut-off or the farmer declines. Admins can refund any payment from Admin → Payments, which shows every attempt, decline, capture and refund on a timeline.

Why it can't double-charge

Risk Defence
Double tap on Pay, or two tabs The payment row is locked FOR UPDATE and moves pending → processing exactly once; a second attempt gets payment in progress
A flaky connection resends the request Every attempt carries a one-time idempotency key under a unique index; a replay returns the existing result and charges nothing
A tampered amount Amounts are always recomputed on the server from the orders — the browser never says how much to charge
A forged gateway callback Callbacks are CSRF-exempt but must carry a valid HMAC-SHA256 signature (compared in constant time) and the matching amount — otherwise 403 / 422
The gateway confirms after the window closed The stock may already be back on sale: the late capture is detected (payment no longer processing), logged as late_capture and refunded automatically
A refund running twice Refunds use a conditional UPDATE … WHERE payment_status = 'paid'
Card data leaking Card numbers and CVCs are validated (Luhn, expiry, CVC length) and passed to the gateway but never stored, logged or flashed back; only the brand and last four digits are kept. Wallet numbers are masked (0300•••4567)
Brute-forcing the pay button 8 attempts a minute and 40 an hour per account, 5 per checkout, 90 status polls a minute

Gateways

  • PAYMENTS_MODE=sandbox (default) runs a built-in simulator, so the whole flow can be tried without real money. Test data on the payment page: 4242 4242 4242 4242 approves, 4000 0000 0000 0002 is declined, 4000 0000 0000 9995 has no funds; any 03XXXXXXXXX wallet number approves after a few seconds, numbers ending in 0000 are declined.
  • PAYMENTS_MODE=live with JAZZCASH_MERCHANT_ID, JAZZCASH_PASSWORD and JAZZCASH_INTEGRITY_SALT uses JazzCash's hosted checkout (pp_SecureHash, HMAC-SHA256, verified again on the signed callback).
  • Live Easypaisa and card acquiring need a merchant agreement; they plug into the same App\Payments\PaymentGateway interface and stay hidden in live mode until a driver is configured.

See also: Race Conditions · Security Model · Order Lifecycle

Clone this wiki locally