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 a benefit_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

  1. CreatePOST /benefit_provider_orders. The body names the entity via benefit_entity_id. The server loads the policy (must exist and not be Expired/Rejected), derives the owning user (primary_user_id) and the provider (policy → benefit → provider), authorizes the actor strictly against that provider, inserts the row with wallet_txn_data = initiated, pays the premium from the user’s HSA account, then updates wallet_txn_data to success:<payment_id> / failed. An indeterminate PBA upstream error leaves the row at its inserted state for sync to recover.
  2. GetGET /benefit_provider_orders/{id}. Reads a single order by its globally-unique id.
  3. ListGET /benefit_provider_orders. Reads from the local DB with optional benefit_entity_id, benefit_provider_id, start_time/end_time, and limit/offset filters; results are further filtered to the providers the actor can access. (benefit_provider_id is honoured only for admin/trusted callers — a benefit-provider actor is always force-scoped to its own provider.) Paginated: returns data, total, limit, offset.
  4. SyncPATCH /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 once success.

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.