Auth guard: callback-service identity (the static Keycloak
kronos-callbacks client), enforced via actor.require_callback_service().
Admin tokens are also accepted for manual operator replays during
incidents — same convention as every other role-specific gate
(require_platform, require_benefit_provider, …).Overview
Mandate execution is the runtime side of the mandate lifecycle — where a registered mandate (from the Mandate Module) is actually charged. Two routes:POST /mandate/{mandate_id}/execute— Kronos fires this once per autopay cycle. Each firing claims amandate_executionsrow (idempotent onx-kronos-idempotency-key), calls Juspay’s/txnsto debit, and stamps the txn status.POST /mandate/{mandate_id}/execution/{execution_id}/status_check— Kronos fires this +27h after a non-terminal debit to reconcile the txn status. If still non-terminal, it reschedules itself every 15min up to 6 attempts before giving up withPaymentStatus::Other("status_unknown").
(execution_id, attempt) pair prevents duplicate scheduling.
Two dashboard read routes (bearer auth, not Kronos) surface those rows:
GET …/executions lists a mandate’s attempts (each joined to its order’s
Juspay external_order_status), and GET …/executions/{execution_id} returns
one by id (carrying the linked order_id). Both fail closed — the mandate must
belong to user_id, and the execution to that mandate — so a guessed id
returns 404, not another user’s row.
Endpoints
POST /mandate/{mandate_id}/execute
Run a single autopay debit. Returns 201 on first claim, 200 on idempotent replay.
POST /mandate/{mandate_id}/execution/{execution_id}/status_check
Reconcile a single execution row against Juspay. Body carries
attempt.GET /users/{user_id}/mandates/{mandate_id}/executions
Dashboard list of a mandate’s execution attempts, each joined to its order’s Juspay
external_order_status. Bearer auth.GET /users/{user_id}/mandates/{mandate_id}/executions/{execution_id}
Single execution by id, carrying the linked
order_id. Fails closed on cross-ownership. Bearer auth.Lifecycle
If Juspay reports a still-non-terminal status on the +27h check, Aarokya schedules a +15min retry withattempt = 2, and so on up
to status_check_max_attempts (default 6).
Two-layer idempotency
Kronos retries on transient failures, so duplicate firings are expected. Two layers prevent double-debits:- DB claim —
INSERT ... ON CONFLICT (job_execution_id) DO NOTHING RETURNING *. Exactly one concurrent caller wins; the loser reads the winner’s row and returns it. - Juspay-side dedup — Juspay deduplicates on
order_id. We passorder_id =the linked orders-ledger row id (orders.id), stable across retries, so a firing-retry that somehow slips past the DB claim still doesn’t double-charge. A resumedInitiatedfiring reuses its existingorder_id(no duplicate order).
Success | Failed), the handler returns the row as-is without
hitting Juspay or rescheduling.
Status vocabulary
The execution row’sstatus is the internal firing lifecycle
(common_enums::MandateExecutionStatus) — repo-owned vocabulary, not
the Juspay status. The Juspay-side status lives on the linked funding order
(orders.external_status, reached via mandate_executions.order_id), so the
execution row never stores provider strings.
| Status | Meaning | Terminal? |
|---|---|---|
Initiated | Firing claimed; charge not yet confirmed (re-drivable on retry) | No |
Pending | Charge dispatched to Juspay; reconciliation cron owns settling it | No |
Success | Debit settled (/orders returned Charged) | Yes |
Failed | Debit declined, or the order never reached Juspay (/orders 404) | Yes |
ExternalOrderStatus to this enum is
From<&ExternalOrderStatus>: Charged → Success, Failed → Failed, and any
non-terminal Juspay state (PendingVbv, Other(_)) → Pending. The wire
value is snake-case (initiated / pending / success / failed).
Trust subsidy
The amount Aarokya charges per firing is computed from the user’s single Issued insurance policy:trust_contribution_bps is a basis-points config knob (0..=10_000).
Default 5000 (50%). Integer division rounds down so the user
always pays slightly less than the exact mathematical share when
there’s a remainder.
If the user has zero or multiple Issued policies, execute_mandate
fails with a 400 — there’s no policy linkage on the mandate row
today.
Configuration
The[mandate_execution] TOML block has been retired — every runtime knob is
served by Superposition (static fallback in
backend/config/fallback.superposition.toml), so values change from the admin
UI without a redeploy.
| Superposition key | Default | Meaning |
|---|---|---|
mandate_execution.autopay.endpoint_name | aarokya-execute-mandate | Kronos-registered endpoint for autopay firings |
mandate_execution.status_check.endpoint_name | aarokya-mandate-execution-status-check | Kronos endpoint for the reconciliation CRON |
mandate_execution.autopay.initial_delay_secs | 3600 | Offset from now for the first autopay firing |
mandate_execution.autopay.interval_minutes_override | 0 | 0 → daily-IST baseline; n > 0 → fire every n minutes (*/n * * * *) |
mandate_execution.status_check.initial_delay_secs | 97200 | 27h — first reconciliation poll |
mandate_execution.status_check.retry_interval_secs | 900 | 15min between reconciliation polls |
mandate_execution.status_check.max_attempts | 6 | Caps total reconciliation firings |
kronos.org_id, kronos.workspace_id) is also
Superposition-served; the Kronos base_url and secret api_key stay in TOML.