Actors

ActorDescription
AppDriver’s mobile app — initiates autopay setup directly against Aarokya Backend and hosts the Juspay SDK for mandate signing.
Aarokya BackendAarokya Autopay Backend (this repository, money-flows) — orchestrates mandate registration, schedules debits, reconciles transactions, and emits ledger events.
DBAarokya Backend’s PostgreSQL — stores mandate, transaction, and job state.
Payment ServersJuspay Payment Gateway — handles session creation, SDK payloads, UPI mandate consent, order status polling, and mandate-activation webhooks. Involved in steps 1–19.
KronosJuspay Kronos — recurring job scheduler. Only involved from step 20 onwards: schedules debit jobs, executes /txn/s, fires order settlement webhooks.
DashboardOperator / ops dashboard used to kick off recurring debits.
PBAPartner Backend API — the partner’s ledger. Aarokya Backend notifies it only at the end of a debit cycle (ledger pending → ledger completed). PBA is not involved in mandate setup or activation.

High-Level Architecture

The App and Dashboard talk directly to Aarokya Backend. PBA is only contacted at the tail end of a debit cycle — once to record the pending ledger entry when the debit is dispatched, and again to mark it completed after the settlement webhook arrives. Mandate setup, activation, and scheduling do not touch PBA.

End-to-End Sequence

The complete journey — setup, activation, scheduling, and debit — in one view.

Flow ① — Mandate Registration (Setup Autopay)

The driver opts into autopay. Aarokya Backend persists the mandate shell, creates a Juspay session scoped to the mandate amount, and returns an SDK payload that the app will hand to the Juspay SDK in the next step. Key writes on Aarokya Backend side: a mandates row in PENDING state with the order_id returned by Payment Servers.
With the SDK payload in hand, the app opens the Juspay SDK’s payment page. The user picks their UPI app; the SDK deep-links into it with an autopay consent intent; the user approves the mandate inside their UPI app; control returns to Aarokya. Aarokya Backend is not called during this phase — all interactions are between the app, the SDK, and the UPI app / rails.
The UPI app talks to Payment Servers (not to Aarokya) when the user approves. Aarokya Backend only learns that the mandate is active via the status poll and/or the Payment Servers MANDATE_ACTIVE webhook in the next flow.

Flow ③ — Mandate Activation — Status Poll

After returning from the UPI app, the app polls Aarokya Backend for the mandate status. Aarokya Backend queries Payment Servers, stores the order data, and returns the status to the app.

Flow ④ — Mandate Activation — Webhook

Independently of the poll, Payment Servers fire a MANDATE_ACTIVE webhook to Aarokya Backend, which writes the final mandate status to DB.
Both the poll (Flow ③) and this webhook can arrive in either order — both paths are idempotent. Use order_id + status as the idempotency key when writing mandates.status.

Flow ⑤ — Start Autopay (Schedule Recurring Job)

Ops (or an automated upstream) flips a mandate from “active” to “actively debiting” by scheduling a job on Kronos. Kronos owns the cron; Aarokya Backend owns the state. State transitions: ACTIVE → SCHEDULING → SCHEDULED. The intermediate SCHEDULING state lets us recover if the Kronos call fails — a retry worker can re-issue the schedule without double-booking.

Flow ⑥ — Recurring Debit — Job Fires

Kronos fires the scheduled job. Aarokya Backend fetches mandate data, creates a txn entry, calls Payment Servers with the mandate txn order ID, and records a pending ledger entry with PBA.

Flow ⑦ — Settlement & Ledger Completion

Payment Servers fire an ORDER webhook once the debit settles. Aarokya Backend updates the txn and closes the ledger entry with PBA.
Why two ledger calls? The PENDING call (Flow ⑥) gives the partner a real-time hold on funds the moment we dispatch to Payment Servers. The COMPLETED call only fires after the ORDER webhook confirms money has moved. If the webhook reports a failure, Aarokya Backend calls PBA with FAILED instead — the partner never sees a phantom settlement.

State Machine — Mandate


State Machine — Debit Transaction


Idempotency & Recovery

ScenarioGuard
Duplicate MANDATE_ACTIVE webhookUPDATE ... WHERE status = 'PENDING' — a second write is a no-op.
Status poll lands before webhookSame row keyed by order_id; first writer wins, second is a no-op.
/schedule times out after Kronos acceptedMandate is stuck in SCHEDULING; reconciler compares Aarokya Backend state against Kronos GET /jobs/:id and heals.
Poll and webhook both report ACTIVEIdempotent — state transition is PENDING → ACTIVE only; duplicate ACTIVE signals from Payment Servers are ignored.
Duplicate ORDER webhookmandate_txn transitions are one-way (PENDING → CHARGED / PENDING → FAILED); re-delivery is a no-op.
Duplicate PBA ledger callPBA keys ledger entries by txn_id; both PENDING and COMPLETED calls are idempotent on re-delivery.