Tertiary waterfall and auto-resume
Outcome
A tertiary claim auto-generates when the secondary 835 posts. Pending waterfalls (PENDED primary that later pays) resume on the next 835. Manual force-runs are documented for coverage-change edge cases.
Prerequisites
billing.claim.writeto manually invoke the waterfall.- Familiarity with COB snapshot and MSP ordering.
How the auto-trigger works
processERA collects every matched claim that transitioned to PAID,
DENIED, or PARTIAL inside its transaction. After the transaction
commits:
| Path | When |
|---|---|
| Cascade | For each PAID or DENIED claim. Primary 835 → generates secondary. Secondary 835 → generates tertiary. Ancestor exclusion guarantees the original primary never resurfaces as a tertiary candidate. |
| Resume | Always. No-op when nothing pending; flips PENDING → RESUMED/CANCELLED otherwise. |
PARTIAL is treated as non-terminal for waterfall purposes.
Diagnose a silently-skipped tertiary
If a secondary 835 posted but no tertiary appeared:
Confirm the secondary transitioned to PAID (not PARTIAL):
SELECT status_currentFROM billing.claimWHERE claim_id = '<secondary-id>';Confirm a higher-priority coverage exists and is active on the service date:
SELECT policy_id, payer_id, priority_order, is_active,effective_from, effective_toFROM member.coverage_policyWHERE member_id = '<member-id>'AND is_active = trueORDER BY priority_order;Confirm the tertiary payer isn't already an ancestor (would indicate a duplicate or misrouted claim):
WITH RECURSIVE chain AS (SELECT claim_id, parent_claim_id, payer_idFROM billing.claim WHERE claim_id = '<secondary-id>'UNION ALLSELECT c.claim_id, c.parent_claim_id, c.payer_idFROM billing.claim cJOIN chain ON c.claim_id = chain.parent_claim_id)SELECT payer_id FROM chain;Check
ERAProcessingResult.cobWaterfallErrorslogs from the latest ERA run — a thrownCONFLICTmeans the tertiary already exists; checkbilling.claim_relationship.If the guard fired, see Medicaid last resort suppression audit.
Force-run the waterfall after a coverage change
If ops updates a member's coverage after the primary 835 has already posted — e.g., the tertiary Medicaid coverage was missed at original submission and added later — the auto-trigger won't re-fire on its own. Force the waterfall:
import { processSecondaryClaimWaterfallDetailed } from '@rcm/core/.../secondary-claim-service';
await processSecondaryClaimWaterfallDetailed(
db,
{
claimId: '<current-claim-id>',
paidAmountCents: <paid>,
remittanceId: '<remit-id-or-null>',
},
{ userId: '<ops-user>', type: 'USER' },
);
Pass the current claim id (the one whose 835 should have driven the next
step). The driver re-reads coverages and emits the next claim or a
SUPPRESSED outcome with a reason.
Validation
| Check | Expected |
|---|---|
New tertiary billing.claim row | Created after secondary 835 |
claim_relationship row | Type=SECONDARY linking secondary→tertiary |
cobWaterfallErrors on ERA result | Empty |
| Pending row | Flipped to RESUMED or CANCELLED after resume |
Known limits
- Chain depth.
PAYER_SEQUENCE_MAPonly maps priority 1..3 toP/S/T. Quaternary payers aren't supported. - Ancestor walk depth cap.
collectAncestorPayerIdsstops at 8 hops as a cycle guard. The unique(parent, child, type)index onbilling.claim_relationshipprevents real cycles; the cap is defence-in-depth. - In-transaction scope. The auto-trigger runs after the ERA
transaction commits. A waterfall failure leaves the posted 835 intact
and surfaces the error on
cobWaterfallErrors.
Cross-references
- COB adjustment snapshot for snapshot semantics.
- Payer priority & Medicaid-last-resort for MSP and suppression guards.