# Purchase Flow (Data — Airtime/Bills follow the same shape)

```
Customer                Laravel App                  Payment Gateway         Queue/Worker            VTU Provider
   │  select plan+phone      │                              │                     │                       │
   │──POST /api/data/orders─▶│                              │                     │                       │
   │                         │ createOrder(): re-price from │                     │                       │
   │                         │ DB, insert transaction        │                     │                       │
   │                         │ (payment_status=CREATED)      │                     │                       │
   │                         │──initialize()───────────────▶│                     │                       │
   │                         │◀──checkout_url, gw_reference──│                     │                       │
   │                         │ save Payment(status=pending)   │                     │                       │
   │                         │ transaction.payment_status =   │                     │                       │
   │                         │   PAYMENT_PENDING               │                     │                       │
   │◀──{reference, checkout_url}                              │                     │                       │
   │──redirect to checkout──────────────────────────────────▶│                     │                       │
   │  (pays)                                                  │                     │                       │
   │                         │◀────────── webhook: payment event ───────────────────│                       │
   │                         │ verify signature (raw body)    │                     │                       │
   │                         │ check Payment not already      │                     │                       │
   │                         │   'verified' (idempotent)       │                     │                       │
   │                         │──verify(gw_reference)─────────▶│                     │                       │
   │                         │◀──amount+status (ground truth)─│                     │                       │
   │                         │ amount matches? mark            │                     │                       │
   │                         │  PAYMENT_SUCCESS (DB txn+lock)   │                     │                       │
   │                         │──dispatch ProcessVtuPurchase───────────────────────▶│                       │
   │                         │                              │                     │ lock row, check not   │
   │                         │                              │                     │  already SUCCESS/     │
   │                         │                              │                     │  PROCESSING             │
   │                         │                              │                     │──purchase(reference)─▶│
   │                         │                              │                     │  (idempotency key =    │
   │                         │                              │                     │   transaction ref)     │
   │                         │                              │                     │◀── success/fail/timeout│
   │                         │                              │                     │ update service_status   │
   │                         │                              │                     │ SUCCESS ⇒ send receipt, │
   │                         │                              │                     │   SMS/email, event fired│
   │                         │                              │                     │ TIMEOUT ⇒ left for retry│
   │                         │                              │                     │   job / reconciliation, │
   │                         │                              │                     │   never auto-resent     │
   │──GET /transactions/{ref}?phone=…──────────────────────▶│                     │                       │
   │◀──{payment_status, service_status}                       │                     │                       │
```

## Failure & retry rules
- If the payment webhook never arrives, a scheduled job polls `verify()` for any `PAYMENT_PENDING` transaction older than 2 minutes — same code path as the webhook handler, so verification logic lives in exactly one place.
- If a VTU provider times out, the transaction is left at `API_TIMEOUT`; a separate `RetryFailedTransaction` job re-attempts through `VtuProviderManager` (which will try the next provider by priority) up to a configured max, after which it becomes `PENDING_REVIEW` for manual admin action. It never retries a transaction already at `PROCESSING` from another worker (row lock + status check prevents this).
- Reconciliation job (nightly + on-demand) cross-checks `payments` (money received) against `transactions.service_status = SUCCESS` (service delivered) and surfaces mismatches — this is the safety net, not the primary control.
