# STK Fast Completion Implementation Report
# Generated: 2026-09-13
# Documents the fast STK completion design

## Current vs Target Completion Time

| Metric | Before | After | Improvement |
|--------|--------|-------|-------------|
| First poll delay | 5s | 2s | 60% faster |
| Poll delay schedule | [10,20,40,80,140,220]s | [3,5,10,15,20,30]s | 84% faster |
| Maximum completion time | 515s (8.6 min) | 83s (1.4 min) | 84% faster |
| Duplicate job dispatch | Every status request | 5s throttle guard | Eliminated |
| Status response fields | status + amount only | + pollingInterval + purpose | Frontend optimized |

## Fast Completion Architecture

```
User clicks Pay/Contribute/Pay Penalty
  ↓
POST /api/payments/stk-push
  ↓
PaymentController::stkPush()
  ↓
PaymentService::initiateStkPush()
  ↓
1. Validate amount > 0 (server-side)
2. assertCanWrite(identity, memberId, paymentType) — ownership check
3. DB transaction: create pending payment record
4. Generate checkoutRequestId = ws_CO_{random}
5. Log initiation
6. CoopBankTransactionService::initiateStkPush() → POST /FT/stk/1.0.0
7. Create CoopTransferRequest (status=submitted)
8. Map bank response to bank_status
9. PollStkStatus::dispatch()->delay(2s) ← REDUCED from 5s
10. Return {success, checkoutRequestId, status:'processing', pollingIntervalSeconds:3}
  ↓
Frontend: show "Check your phone for M-Pesa prompt"
Frontend: poll every 3 seconds (from pollingIntervalSeconds)
  ↓
PollStkStatus job (T+2s):
  1. Acquire Redis lock (stk_poll_lock:{checkoutRequestId}, 120s)
  2. Check CoopTransferRequest status (skip if terminal)
  3. POST /Enquiry/STK/1.0.0/ with MessageReference
  4. Map response: SUCCESS/PENDING/FAILED/UNKNOWN
  5. If SUCCESS: processStkBankStatus() → PaymentLifecycleService
  6. If PENDING: scheduleNextPoll() with [3,5,10,15,20,30]s
  7. If FAILED: mark failed, complete payment with failure
  8. If UNKNOWN: schedule next poll
  ↓
On SUCCESS:
  PaymentService::processStkBankStatus()
    → PaymentLifecycleService::completeSuccessfulPayment()
      → completeContributionPayment() / completeDonationPayment() / ...
      → DB transaction: update status + allocate/credit + invalidate cache
    → CoopTransferRequest updated to 'successful'
  ↓
Frontend receives success from poll
  ↓
Frontend: close form, show success, refresh dashboard
```

## Frontend Polling Contract

The backend now returns `pollingIntervalSeconds: 3` in every status response. The frontend should:
1. Poll every 3 seconds (from pollingIntervalSeconds)
2. Stop polling immediately on SUCCESS or FAILED
3. Continue polling on PENDING/PROCESSING
4. Use `paymentPurpose` to display context-appropriate messages
5. Not call the bank directly — all bank calls go through backend jobs

## Status Endpoint Guard

The `dispatchPollIfNeeded()` method prevents duplicate PollStkStatus dispatch:
- Checks CoopTransferRequest.updated_at for last provider check
- If last check was within 5 seconds, does NOT dispatch new poll
- This prevents job spam from frontend polling while preserving:
  - One active polling job per payment
  - Distributed lock safety
  - Scheduled reconciliation as safety net

## Co-op Bank Rate Consideration

With new delays [3,5,10,15,20,30]:
- 6 polls per payment over 83 seconds
- Rate: ~1 poll per 14 seconds average
- Well within typical Co-op Bank inquiry limits
- MUST confirm with Co-op Bank before production deployment

## Scheduler Remains Safety Net

The scheduler (`* * * * * php artisan schedule:run`) still runs every minute. Scheduled reconciliation (`coop:reconcile-transfers` or equivalent) is NOT the primary completion mechanism — it's a safety net for payments that timed out or were missed by PollStkStatus.
