Auth guard: any trusted-backend actor — admin keys, benefit-provider human/service users, and platform human/service users. App users (JWT) are forbidden.
Overview
A sponsor is a donor entity — an individual person or an organisation (trust, CSR fund, society, etc.) — that contributes funds toward matching driver insurance premiums. Each registered sponsor has an external “normal account” on the ledger provider (PBA today) that holds its balance. Subsidies later flow out of the sponsor’s normal account into individual users’ purpose-bound (PB) accounts via separate transfer operations. This module covers registration only — the contribution / transfer ledger is a separate concern.- Generic discriminant:
SponsorKind = INDIVIDUAL | ORGANISATION. Per-kind detail payloads live in a typed JSONB union. - Three-stage lifecycle:
PENDING → ACTIVE → INACTIVE. Sponsors are born PENDING and transition to ACTIVE when their PBA normal-account is provisioned and linked. - Account row, not column: a sponsor’s PBA
provider_account_idlives as a row in theaccountstable (holder = sponsor:<sponsor_id>+account_type = SPONSOR). Thesponsorsrow itself carries identity and lifecycle only. - Recovery path:
POST /sponsors/{id}/syncre-links a sponsor whosecreatesucceeded at PBA but failed locally afterwards (idempotent on ACTIVE).
Lifecycle
ACTIVE → PENDING and INACTIVE → anywhere are rejected with409 SPE_1404 (SponsorNotActivatable).
POST flow
Two writes are split for safety:- Sponsor row INSERT — single statement, outside any transaction. Gated by partial-unique indexes on
nameandslug. - PBA
CreateNormalAccount— network call. Held outside any DB transaction so the connection pool doesn’t stall on PBA latency. - Atomic local link — inside
transaction_async: INSERT theaccountsrow (holder = sponsor:<id>,account_type = SPONSOR,external_account_id = pba_resp.provider_account_id) and flipsponsor.status = ACTIVE.
accounts row — recover via POST /sponsors/{id}/sync. If step 3 fails after step 2 succeeded, the same /sync path recovers (it re-attempts the atomic link).
Status enum
Sponsor kind + details
The
sponsor_details JSON discriminator (sponsor_kind key) must match the top-level sponsor_kind field. A mismatch on create is a 400 SPE_1405 (validation error); a mismatch on update (sponsor_details variant vs the stored sponsor_kind) is a 400 SPE_1402.
Auth Guards
Endpoints
POST /sponsors
Register a new sponsor + optionally provision its external normal account inline.
GET /sponsors
Paginated list. Filter by
statuses and/or sponsor_kinds (comma-separated, multi-select).POST /sponsors/{id}/sync
Recovery path. Link a PENDING sponsor to its existing PBA normal account.
GET /sponsors/{id}
Fetch a single sponsor by UUID.
PATCH /sponsors/{id}
Update mutable fields.
slug and sponsor_kind are immutable.DELETE /sponsors/{id}
Soft-delete (
status → INACTIVE).GET /sponsors/{id}/balance
Current balance on the sponsor’s normal account at PBA.
GET /sponsors/{id}/transactions
Paginated ledger from PBA. Supports date-range filters.
POST /sponsors/{id}/deposit
Add funds to the sponsor’s normal account.
POST /sponsors/{id}/contribute
Sponsor → user HSA transfer for a specific
insurance_policy_id.Fields
PII / Secret Policy
Examples
provider_account_id is not on this response — it lives on the linked accounts row (holder = sponsor:<id>, account_type = SPONSOR). Fetch via GET /accounts?holder=sponsor:<id> (admin) if needed.
Reconciliation flow
If PBA’sCreateNormalAccount succeeds but the local link transaction fails (rare), the sponsor row stays in PENDING with no accounts row. PBA has the account; we just couldn’t write the linkage locally.
Recovery — POST /sponsors/{id}/sync:
- Operator pulls the
provider_account_idfrom PBA’s admin surface. POST /sponsors/{id}/syncwith{ "external_account_id": "<pba_account_id>" }.- Server verifies PBA’s
holder_idmatches oursponsor_id, then atomically INSERTs theaccountsrow and flips the sponsor to ACTIVE. - Idempotent — if the sponsor is already ACTIVE, returns the current row as 200 without any PBA call.
TODO(pba-find-by-holder) in core::sponsor::sync_sponsor).