Claims
The claims service owns the medical claim and prior-authorization lifecycles. It accepts claims submitted complete (there is no DRAFT), drives them through a status machine from SUBMITTED to PAID/CLOSED, itemises them into ClaimLine rows, and emits state-change events for Billing and Notifications to consume. Prior authorization lives in the same service as a parallel, independent workflow.
Field reference: full columns, types and nullability live in the catalog: glossary terms
Claim,ClaimLine,ClaimEvent,ClaimDocument,PriorAuth. This page is the narrative; the companion domain page is Claims Lifecycle.
As-built adjudication is a pass-through
The wired review path auto-approves every line at the billed amount. A rule-engine adjudicator (internal/adjudication) that would compute per-line allowed amounts (deny, exclude, cap, scale) exists but is never called on the live path. Treat rule-driven adjudication, prior-auth gating, and pre-approval eligibility checks as built-but-not-wired. See caveats.
What it owns
- Claim intake from members, providers and internal callers, complete at submission.
- The claim status machine and its append-only audit trail.
ClaimLineitemisation (one row per service); the claim header carries no monetary total.- Supporting claim documents (metadata + URL).
- Prior-authorization records and their own
SUBMITTED → PENDING_REVIEW → APPROVED/DENIEDflow. - A transactional outbox that publishes claim and prior-auth events to Kafka.
Database
Schema claims. Column-level detail is in the catalog.
| Table | Purpose |
|---|---|
claims | Root claim aggregate: status, policy/term, claimant, incident date, opaque document blob. No monetary total. |
claim_lines | Service line items: amount_claimed/amount_allowed/amount_paid (NUMERIC(14,4)), element_id, coverage_term_key, reason_code. |
claim_events | Append-only audit log of every claim state transition (event_type, from_status, to_status, actor, note). |
claim_documents | Supporting documents attached to a claim (filename, content type, URL). |
prior_auths | Prior-authorization requests and decisions. |
outbox | Transactional outbox for reliable Kafka publishing. |
The claims row holds only the current status; the full history is the claim_events trail. Every status change writes one claim_events row and one outbox row inside the same transaction as the status update.
Claim state machine
ClaimStatus = SUBMITTED · UNDER_REVIEW · PENDING_INFO · APPROVED · REJECTED · PAID · CLOSED (packages/go/domain/enums.go). REJECTED is the only adverse status; there is no IN_REVIEW and no DENIED on a claim (DENIED exists only on PriorAuth).
review (ReviewClaim) is the adjudication entry point. On the wired path it always returns the auto-approve decision, so it transitions straight to APPROVED. It only routes to UNDER_REVIEW (emitting claim.review_required) when the adjudicator returns MANUAL_REVIEW_REQUIRED, which requires the rule engine that is not active today. In practice auto-approve fires when a claim line is added (a claim is created empty, so line submission is the adjudication moment). requestInfo moves a claim to PENDING_INFO. approve accepts SUBMITTED, UNDER_REVIEW, PENDING_INFO, and APPROVED (the last as an idempotent re-entry for replay/repair). reject is allowed from SUBMITTED, UNDER_REVIEW or PENDING_INFO.
API routes
All /claims* routes are JWT-protected and tenancy-scoped (see Tenancy). /prior-auth* routes require a privileged (admin) token; employer-tenant tokens get 403 because prior-auth rows carry no employer linkage.
| Method | Path | Auth | Description |
|---|---|---|---|
POST | /claims | JWT | Submit a new claim |
GET | /claims | JWT | List claims (filter by policy, member, or status) |
GET | /claims/list | JWT | Alias of GET /claims (web-admin via APISIX) |
GET | /claims/{locator} | JWT | Get claim by locator |
PATCH | /claims/{locator}/review | JWT | Adjudicate: auto-approve, or move to UNDER_REVIEW if manual review is flagged |
PATCH | /claims/{locator}/approve | JWT | Approve a claim in UNDER_REVIEW |
PATCH | /claims/{locator}/reject | JWT | Reject with a reason code |
PATCH | /claims/{locator}/requestInfo | JWT | Move to PENDING_INFO |
PATCH | /claims/{locator}/close | JWT | Close an approved or rejected claim |
POST | /claims/{locator}/number/generate | JWT | Return a display claim number (returns the locator) |
POST | /claims/{locator}/lines | JWT | Add a ClaimLine |
GET | /claims/{locator}/lines/list | JWT | List a claim's lines |
GET | /claims/{locator}/events/list | JWT | 501 stub, not implemented |
POST | /claims/{locator}/documents | JWT | Attach a document |
GET | /claims/{locator}/documents/list | JWT | List a claim's documents |
POST | /prior-auth | JWT | Submit a prior-authorization request |
GET | /prior-auth | JWT | List prior authorizations |
GET | /prior-auth/list | JWT | Alias of GET /prior-auth (web-admin via APISIX) |
GET | /prior-auth/{locator} | JWT | Get prior auth by locator |
PATCH | /prior-auth/{locator}/review | JWT | SUBMITTED → PENDING_REVIEW |
PATCH | /prior-auth/{locator}/approve | JWT | PENDING_REVIEW → APPROVED |
PATCH | /prior-auth/{locator}/deny | JWT | PENDING_REVIEW → DENIED |
GET | /internal/claims/{locator} | X-Internal-Service secret | Retrieve claim for Billing |
PATCH | /internal/claims/{locator}/pay | X-Internal-Service secret | Mark claim PAID (called by Billing) |
GET /healthz and GET /readyz are unauthenticated health probes. The /internal/* routes are guarded twice: the APISIX edge returns 404 for /claims/internal/*, and the service itself requires an X-Internal-Service header matching INTERNAL_SVC_SECRET (constant-time compare; missing/wrong header gets 401). The guard fails closed: if the secret is unset every internal request gets 503, so a misconfigured deploy cannot silently re-expose pay.
Tenancy
Employer-facing routes are scoped by the org_locator JWT claim (the employer party locator). The claim is resolved to the employer's member set via group-scheme; a caller with neither org_locator nor an admin role gets 403. org_locator is checked before admin roles, so a stray employer-admin role cannot widen a tenant token.
- Lists (
GET /claims): any query filter is overridden by the caller's verified member set. Privileged (admin) callers are unrestricted. - By-locator routes (get, review, approve, reject, requestInfo, close, lines, documents): a claim outside the caller's tenancy returns 404, identical to a claim that does not exist, so sequential locators are not an existence oracle.
- Submit: the body's
memberLocatormust belong to the caller's org, else403(the caller asserted the key, so no oracle to protect). /internal/*routes carry no user JWT and are deliberately outside tenancy; they are protected by the edge block plus theX-Internal-Serviceguard.
Events
Claims both publishes and consumes. Every published message is the canonical envelope (packages/go/domain/event_envelope.go): eventId, eventType, occurredAt, correlationId, causationId, client lineage (sessionId / activityId / activityName), payload, and state, the subject entity frozen at emit time (event-carried state transfer). Envelope details live in the Kafka Event Catalog; per-type contracts live in the Event Registry.
Publishes: topic claims.events
Claim and prior-auth events are enqueued in the transactional outbox (subject snapshot in the state column) and published to claims.events (KAFKA_CLAIMS_TOPIC, default claims.events) by the outbox worker. The worker stamps eventId from the outbox row id (stable across re-publication), lifts correlationId and client lineage from the trace_context captured at enqueue, and forwards state. The verb lives in the eventType field, not the topic name.
One event bypasses the outbox: record.viewed describes a read, so it has no transaction to attach to and goes straight to Kafka through the direct producer (internal/kafka/producer.go), lineage lifted from W3C baggage, unkeyed, with no correlationId and no state.
eventType | Emitted when | state subjects |
|---|---|---|
claim.submitted | A claim is submitted | claim |
claim.review_required | Review flags manual review (→ UNDER_REVIEW); today only via the high-value threshold | claim |
claim.approved | A claim reaches APPROVED (line-add auto-approve, review, or approve) | claim |
claim.rejected | A claim is rejected (manual reject with reasonCode, or auto-reject on policy cancellation without one) | claim |
claim.info_requested | A claim moves to PENDING_INFO | claim |
claim.closed | An approved or rejected claim is closed | claim |
claim.payment_initiated | Payment starts as the claim leaves APPROVED (same transaction as claim.paid) | claim |
claim.paid | Billing confirms payment via the internal pay endpoint | claim |
prior_auth.submitted | A prior-auth is submitted | priorAuth |
prior_auth.escalated | A prior-auth is routed into review (SUBMITTED → PENDING_REVIEW) | priorAuth |
prior-auth.decided | A prior-auth is approved or denied (carries a decision field) | priorAuth |
record.viewed | A 2xx read of a claim record (ENG-406 audit trail, shared audit.RecordView middleware) | none |
state.claim / state.priorAuth is the full subject row as in scope at the emit site, Document decoded; on transition events it is the pre-transition snapshot. There is no aggregate claims.adjudicated event; an approval is signalled by claim.approved. Note the as-built spelling split kept for wire compatibility: prior_auth.submitted and prior_auth.escalated use underscores, prior-auth.decided a hyphen.
Consumes: topic enrollment.events
The service runs a Kafka consumer (group claims-service, KAFKA_ENROLLMENT_TOPIC, default enrollment.events) and switches on eventType:
eventType | Reaction |
|---|---|
policy.cancelled | Auto-reject every open claim (SUBMITTED/UNDER_REVIEW/PENDING_INFO) for that policy, emitting claim.rejected per claim. Idempotent on replay. |
policy.issued | No-op (Phase 1). |
Contracts are the registry's: payload schema, state subjects, lineage requirements, producers and consumers as code refs, and golden examples all live in packages/go/domain/eventregistry/registry/<eventType>/.
Prior authorization
PriorAuth is a pre-approval for a procedure, lifecycle SUBMITTED (on create) → PENDING_REVIEW (on review) → APPROVED | DENIED. The migration default and an older domain comment say PENDING; that value is a legacy/seed fallback only (approve/deny still accept it), runtime records open as SUBMITTED.
Prior auth is an independent workflow. It does not gate claim adjudication today: the claim review/approve path contains no prior-auth lookup. Enforcing a prior-auth requirement before a claim can advance is planned, not implemented, and likely belongs upstream in eligibility/enrollment.
Dependencies
| Service | How used |
|---|---|
| Eligibility | When a claim reaches APPROVED (auto-approve on line add, review, or approve), claims calls ApplyAccumulators (PATCH /internal/members/{locator}/accumulators/{termLocator}/apply) to record consumption against the member's term. The apply is idempotent per claim line on the eligibility side, so replays cannot double-count. A pre-approval coverage/eligibility check exists in the client (CheckEligibility, GetCoverage) but is not wired into the claim flow. |
| Enrollment | On review/approve, fetches the policy to enrich the outbox payload (policy_locator, account_id) for Billing. Also the source of the consumed enrollment.events. |
| Policy Admin | Client exists for benefit-rule/element lookup feeding the rule-engine adjudicator. Not exercised on the wired pass-through path. |
| Billing | Calls the internal endpoints to fetch a claim and confirm payment; consumes claims.events. |
External clients are nil-safe: when a URL is unconfigured (tests, minimal deploys) the dependent step is skipped.
Invariants
- The claim header stores the current
statusand no monetary total; amounts live onClaimLine. REJECTEDis the only adverse claim status;DENIEDis aPriorAuthstatus.- One
claim_eventsrow plus oneoutboxrow are written per status change, in the same transaction as the status update. amount_alloweddefaults toamount_claimed(un-adjudicated lines allow the billed amount);amount_paidis set toamount_allowedon thePAIDtransition.- All events flow through one topic (
claims.events); the verb is theeventTypefield.
Caveats
- Rule-driven adjudication is dead code on the wired path.
internal/adjudication.Adjudicateand the rule engine are never invoked byReviewClaim; it hard-codesAUTO_APPROVED, so a reviewed claim auto-approves at the billed amount. - No prior-auth gate. Prior auth runs in parallel and never blocks a claim.
- No pre-approval eligibility check. Only
ApplyAccumulators(the on-approval write-back) is called; coverage validation at date of service is built into the client but unused. claims.adjudicateddoes not exist. Earlier docs invented it; there is no such topic and noadjudicatedeventType.- Manual approval and accumulator reversal are open. Auto-approve is on for now; rejecting or voiding an approved claim does not yet reverse the applied accumulator amounts (#1434).
- Appeals are not implemented: no appeal status, event, or re-review path.
