Auth: all routes use a benefit-provider actor. Reads (
get/list) accept
readonly admins too. There is no user_id in the path — the owning user is
derived server-side — and the authorization boundary is a strict per-order
check against the order’s benefit_provider_id (require_benefit_provider_strict):
admins are cross-tenant, a benefit-provider actor is pinned to its own provider.Overview
A benefit provider order is an external purchase placed with a benefit provider. Today the only flavour is an insurance-premium payment: the order names abenefit_entity_id (an insurance policy id), the server derives the
owning user and the provider behind the policy, charges the premium out of the
user’s PBA-backed (HSA) account via PBA MakePayment, and tracks the outcome
in the wallet_txn_data column.
There is no user_id in any path. On create the user is read from the named
policy’s primary_user_id; on get/sync it is read from the order row.
benefit_entity_id
benefit_entity_id is a bare UUID everywhere on the wire — request body,
response, and the list query filter alike:
The column is a native
uuid; the entity’s type is recorded once on
benefit_entities.entity_type, which the server reads on create to decide how
to process the order. Sending anything that is not a UUID to the list filter is
a 400.
Because the response carries only the id, it does not tell you which kind of
entity was purchased. Only insurance policies are payable today, so every
benefit_entity_id on an order is an insurance policy id — the type is
implied. If more benefit types become payable, this response will need a type
discriminator; it does not have one today.Endpoints
POST /benefit_provider_orders
Create an order, charge the premium from the user’s PBA account, and record the outcome.
GET /benefit_provider_orders/{id}
Read a single order by its globally-unique id.
GET /benefit_provider_orders
Paginated list, scoped to the providers the actor can access. Optional filters.
PATCH /benefit_provider_orders/{id}/sync
Recovery — re-attempt a create-time payment that failed or never reached PBA.
POST /admin/benefit_provider_orders/backfill_split
Admin one-time backfill — fill the self/others payment split on settled orders that predate the split columns.
Create request
Unlike the user-facing modules, the create
amount is in minor units
(paise), not rupees. This is a service-to-service API, so the amount is an
exact integer paired with currency — no major-unit float rounding at the
boundary.Order lifecycle
- Create —
POST /benefit_provider_orders. The body names the entity viabenefit_entity_id. The server loads the policy (must exist and not beExpired/Rejected), derives the owning user (primary_user_id) and the provider (policy → benefit → provider), authorizes the actor strictly against that provider, inserts the row withwallet_txn_data = initiated, pays the premium from the user’s HSA account, then updateswallet_txn_datatosuccess:<payment_id>/failed. An indeterminate PBA upstream error leaves the row at its inserted state forsyncto recover. - Get —
GET /benefit_provider_orders/{id}. Reads a single order by its globally-unique id. - List —
GET /benefit_provider_orders. Reads from the local DB with optionalbenefit_entity_id,benefit_provider_id,start_time/end_time, andlimit/offsetfilters; results are further filtered to the providers the actor can access. (benefit_provider_idis honoured only for admin/trusted callers — a benefit-provider actor is always force-scoped to its own provider.) Paginated: returnsdata,total,limit,offset. - Sync —
PATCH /benefit_provider_orders/{id}/sync. Recovery endpoint — re-attempts the PBA payment for an order whose create-time payment failed or never reached PBA. The order id is the PBA idempotency key, so this is idempotent; a no-op oncesuccess.
wallet_txn_data
The PBA payment outcome. On the response it is a tagged object
{ status, detail }, where status is one of INITIATED / PENDING /
SUCCESS / VOIDED / FAILED and detail carries the PBA payment id when
present:
In storage the same value is a tagged string in a
VARCHAR column —
initiated / pending:<id> / success:<id> / voided:<id> / failed. The
wallet_txn_data name is a column on the benefit_provider_orders table; there
is no standalone wallet surface. The debit always hits the user’s PBA-backed
(HSA) account.
Response shape
Self / others payment split
Each order records how PBA drew the charge across the account’s contribution pools —from_self and from_others (each an AmountResponse, together summing
to amount). PBA is the source of truth: its payment response carries the split,
and create / sync persist it on payment success.
Both are null on orders whose payment never settled, and on legacy rows created
before the split was tracked. The admin backfill fills the legacy rows:
POST /admin/benefit_provider_orders/backfill_split
Admin-only, sweep-all. For each settled order still missing the split, PBA’s
payment is re-queried idempotently (keyed on the order id, so it can never
double-charge) and the split is persisted. Returns summary counts
(
total_processed / filled / skipped / failed) plus a results list of
the non-filled orders — each { order_id, outcome, detail } with outcome a
plain string (SKIPPED / FAILED). Optional limit caps a run. Safe to
re-run — filled rows drop out of the candidate set.