PageSourceSearch

https://safarishare.com/_next/static/chunks/2614.cf86bf21c90c382b.js

js safarishare.com collected 2026-10-09 23:28:35 UTC 7,256 bytes, 1 lines download raw bytes

1"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2614],{20233:(e,n,t)=>{t.r(n),t.d(n,{markdown:()=>a});let a='---\ntitle: Payment Efficiency\nstatus: deployed\narea: payments\npriority: high\nlabels:\n  - payments\n  - workers\n  - queues\n  - realtime\nowner: null\ndepends_on: []\nblocked_by: []\nrelated:\n  - africas-talking-sms-auth\n  - admin-auth-hardening\nschedule:\n  target_start: null\n  target_end: null\n  milestone: null\nupdated: 2026-05-31\n---\n\n# Payment Efficiency\n\nImprove the reliability, speed, and resilience of the payment flow across bus ticketing and carpooling.\n\n> **Diagram key**\n> `—>>` solid arrow = HTTP / API call\n> `- ->>` dashed arrow = Supabase Realtime event (push, no request)\n\n## How Payments Work Today\n\n```mermaid\nsequenceDiagram\n    participant User\n    participant App\n    participant Paytota\n    participant Worker\n    participant DB\n\n    User->>App: Confirms booking\n    App->>Paytota: initiateCollection()\n    App->>Paytota: triggerStkPush()\n    Note over App,Paytota: STK response is ignored\n    App->>User: "Waiting for payment..."\n\n    loop Every 10 seconds — forever\n        Worker->>Paytota: collectionStatus()\n        Paytota->>Worker: "pending"\n        Worker->>DB: write status\n        DB--\x3e>App: \uD83D\uDCE1 Realtime — pending\n    end\n\n    Paytota->>Worker: "paid"\n    Worker->>DB: update transaction → succeeded\n    Worker->>DB: update booking → paid\n    Note over Worker,DB: Two separate writes — can desync on crash\n    DB--\x3e>App: \uD83D\uDCE1 Realtime — succeeded\n    App->>User: "Booking confirmed"\n    Note over App,User: If Realtime drops, event is lost — user never sees confirmation\n```\n\n## Issues\n\n### 1. Supabase Realtime is the wrong tool for payment confirmation\n\nRealtime is a WebSocket designed for collaborative features — live dashboards, presence, seat availability. It has no delivery guarantee. If the connection drops for even a second while the payment event fires, it is gone. The frontend has no way to recover.\n\nFor a payment, the user must always be able to ask "did I pay?" and get a definitive answer — regardless of whether they were connected at the right moment, backgrounded the app, or refreshed the page.\n\n```mermaid\nflowchart TD\n    A[Payment succeeds] --\x3e B[\uD83D\uDCE1 Realtime event broadcast]\n    B --\x3e C{Was app connected\\nat that exact moment?}\n    C --\x3e|Yes| D[User sees confirmation ✓]\n    C --\x3e|No — connection dropped,\\napp backgrounded,\\npage refreshed| E[Event lost\\nSpinner hangs forever ✗]\n    style E fill:#f88,stroke:#c00\n    style C fill:#f88,stroke:#c00\n```\n\n**Fix:** replace Realtime on the payment confirmation flow with frontend polling. The app calls `GET /api/bookings/:id` every 5 seconds until the status is terminal. This works regardless of connection state and is trivially retried after a refresh.\n\nRealtime remains appropriate for other features — live seat availability on route search, operator dashboard booking feeds, admin monitoring — where a missed event is not consequential.\n\n### 2. The poller runs forever\n\nIf the user dismisses the STK prompt or the payment stalls, the worker keeps asking Paytota every 10 seconds with no limit. The payment never resolves and the user is stuck.\n\n```mermaid\nflowchart TD\n    A[Payment initiated] --\x3e B{collectionStatus?}\n    B --\x3e|pending| C[Wait 10s]\n    C --\x3e B\n    B --\x3e|paid| D[Mark succeeded]\n    B --\x3e|error| E[Mark failed]\n    style C fill:#f88,stroke:#c00\n    style B fill:#f88,stroke:#c00\n```\n\n**Fix:** cap the poller at 18 attempts (3 minutes). On timeout, mark the transaction `failed` and let the frontend surface a clear retry message.\n\n### 3. STK push result is ignored\n\nThe app fires the STK push (the phone prompt) but never checks if it actually worked. If it silently fails, the poller starts anyway — polling a payment the user was never asked to confirm.\n\n```mermaid\nflowchart LR\n    A[triggerStkPush] --\x3e B[Start poller]\n    A -.->|error ignored| B\n    style A fill:#f88,stroke:#c00\n```\n\n**Fix:** check the STK response before enqueuing the poller. On failure, mark the transaction `failed` immediately and tell the user.\n\n### 4. Booking update can desync\n\nThe worker updates the transaction and the booking in two separate database calls. If the process crashes between them, the transaction shows `succeeded` but the booking stays `pending` — with no automatic recovery.\n\n```mermaid\nsequenceDiagram\n    participant Worker\n    participant DB\n\n    Worker->>DB: update transaction → succeeded ✓\n    Note over Worker: crash / network drop\n    Worker--xDB: update booking → paid ✗\n    Note over DB: Transaction succeeded — booking still pending\n```\n\n**Fix:** wrap both updates in a single Postgres RPC so they succeed or fail together.\n\n## Proposed Solution\n\n```mermaid\nsequenceDiagram\n    participant User\n    participant App\n    participant Paytota\n    participant Webhook\n    participant Worker\n    participant DB\n\n    User->>App: Confirms booking\n    App->>Paytota: initiateCollection()\n    App->>Paytota: triggerStkPush()\n    Paytota->>App: STK response — now checked\n\n    alt STK failed\n        App->>User: "Could not send payment prompt — please retry"\n    else STK succeeded\n        App->>User: "Check your phone to confirm payment"\n        App->>Worker: Enqueue poller (max 18 attempts / 3 min)\n\n        alt Happy path — webhook\n            Paytota->>Webhook: POST /api/webhooks/paytota\n            Webhook->>DB: RPC — update transaction + booking atomically\n        else Fallback — polling\n            loop Every 10s, max 18 attempts\n                Worker->>Paytota: collectionStatus()\n            end\n            Worker->>DB: RPC — update transaction + booking atomically\n        end\n\n        loop Every 5s until terminal status\n            App->>App: GET /api/bookings/:id\n        end\n\n        alt Status = paid\n            App->>User: "Booking confirmed ✓"\n        else Timeout — 3 min, no payment\n            App->>User: "Payment timed out — please try again"\n        end\n    end\n```\n\n## Scope\n\n| File | Change |\n|------|--------|\n| `workers/src/poll-payment/index.ts` | Add 18-attempt cap; check STK response before starting poller; remove Realtime broadcast |\n| `supabase/schemas/` | New RPC to update transaction + booking atomically |\n| `web/src/app/api/webhooks/paytota/route.ts` | New webhook handler (if Paytota supports it) |\n| `web/src/app/api/bookings/[id]/route.ts` | Ensure status is returned for frontend polling |\n| `workers/src/queues.ts` | Add `failed` event handler; remove Realtime dependency from payment flow |\n| Frontend booking confirmation | Replace Realtime listener with `GET /api/bookings/:id` polling every 5s |\n\n## Questions Before Implementation\n\n1. Does Paytota support outbound webhooks, and what is the signature verification scheme?\n2. Is 3 minutes the right timeout before a payment is considered failed?\n3. Should the user be able to re-trigger the STK push themselves if it fails, or redirect to a fresh payment attempt?\n'}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.