Commonwealth Medallion Model
How the Commonwealth dbt project defines its grains, uses stewardship master entities, and computes the attributes that pick a reimbursement rule set before pricing runs, and where Phoenix adds or repeats business logic on top of gold.
Grains
An encounter is an Athena claim family. A claim is one coordination position on one Athena claim. A service line is one non-voided charge.
Master entities
Location, provider, practice and TIN masters come from the stewardship ledger through checked-in crosswalks. Their IDs are everywhere; their values are read in only a few places.
Scored attributes
Eight attributes, computed in dbt silver, score every encounter against every rule set. The pricing scorer only reads the candidates they produce.
Phoenix
Reads seven gold serving models and nothing else. It runs its own opportunity rule, which disagrees with gold's, and repeats several gold rules on the server and again in the browser.
PART 1Grains
Every grain is built on Athena's claim table and its originalclaimid family link. The three core grains are the encounter, the claim and the service line. Every other grain is one of those plus a COB position.
Encounter
The silver docs define it as "a single billable event scoped to who is billing": a surgery produces separate encounters for the surgeon, the anesthesiologist and the facility. Athena has no such object, so the claim family stands in for it.
How a family is formed (models/silver/staging/stg_claim_family.sql):
root_claim_id = coalesce(nullif(originalclaimid, ''), claimid). A claim with no original is its own root.encounter_id = sk(root_claim_id), which ismd5('athena:' || context_id || ':' || root).- The family is built over raw
bronze_claim, not over submissions. The docs count 119,433 claims never billed at any position, and they are still encounters.
| Flag | Meaning | Rule |
|---|---|---|
| is_root | The family anchor | source_claim_id = root_claim_id |
| is_primary_claim | The live primary submission | Latest lastbilleddate1, then highest claim id. None in a family never billed primary. |
| is_attribute_claim | The one claim per family whose attributes stand in for the encounter | Primary submission if one exists, else the root, else the highest id |
Patient, department, provider, TIN, diagnosis, admit dates and primary coverage all come from the attribute claim. encounter_coverage selects on the same flag, so the encounter and its coverage can't describe different claims. Only the service date span (min and max over every charge in the family), has_remit and drg_code are aggregated across the family. AR balances are taken from the attribute claim, because summing a rebill and the claim it replaced would double-count.
Who gets in. A family is published only if its location, provider, practice and TIN all resolve to masters that are not hidden, and its primary coverage is not a package Athena types as non-insurance (payer.noninsurancetype). Every other family goes to excluded_encounter with a reason. tests/encounter_population_reconciles.sql checks that each family lands in exactly one of the two.
Claim
"A coordination position on an athena claim": one row per (claim, cob), built in stg_claim_position.sql and claim.sql. claim_id = sk([source_claim_id, cob]), so one Athena claim produces up to three rows and source_claim_id alone is not unique.
| cob | Position | Exists when | Pay-to TIN / NPI |
|---|---|---|---|
| 1 | Primary | lastbilleddate1 is set, or a primary policy is configured | paytotaxid1 / paytonpi1 |
| 2 | Secondary | lastbilleddate2 is set, or a secondary policy is configured | paytotaxid2, falling back to slot 1 |
| 0 | Patient | Always | paytotaxid1 / paytonpi1 |
- "Configured" means
is_configured_slot(): not null, not blank, and not the'0'sentinel. - Positions are independent. A claim can be billed secondary with no primary, so nothing may assume cob 1 exists before reading cob 2.
is_billedmarks a position that was actually submitted. Unbilled positions are kept because payers remit against positions that were never billed.is_effectivemarks the live row for each (family, cob): the latest submission, with billed positions ahead of configured-only ones.parent_claim_idpoints to the root claim at the same cob, and is set only on rebills.frequency_codecomes frommedicaidresubmissioncode, elseoriginalfor the root andreplacementotherwise.- Remits find their position by matching
patientinsuranceidagainst the claim's slots (stg_claim_coverage_slot), never by date. A policy at two slots ranks primary, then secondary, then patient.
Service line
"A single billed service within an encounter": one row per non-voided CHARGE transaction (stg_tx_charge.sql, service_line.sql). service_line_id = sk(parentchargeid), the same id space as erarecord.chargeid, which is how remits attach to a line. The line records only what was billed; payer answers live in service_line_adjudication.
claim_idattaches to one position only: the claim's billed position, trying primary, then secondary, then patient. If the claim was never submitted, it is null and the line stays on its encounter.line_numberis a display ordinal by charge id. Athena has no LX line number or REF*6R control number.- Procedure code and modifiers are split once from Athena's compound code (for example
99213,25,59) plusothermodifier. Modifier 99 setsmodifier_overflow. - Units are replaced with timesheet minutes for anesthesia.
Payer answers and money
| Model | Grain | Notes |
|---|---|---|
| encounter_coverage | (encounter, cob) | Policies from the attribute claim's slots, as of the date of service. No tertiary on the claim side. |
| service_line_adjudication | (line, cob) | The current answer by adjudication_recency(). 861,326 of 3,402,341 adjudicated lines are answered at two positions. |
| claim_adjudication | (claim, cob) = claim_id | Remit rows with no charge id: 537,948 rows, $37.4M allowed, $27.2M paid. |
| transaction | One money movement | Remits and practice postings unioned. is_superseded marks which copy to believe, and the remit wins. claim_id or service_line_id is null where they don't resolve. |
Gold (shared/dbt-medallion/models/gold) keeps these grains one to one. dim_/fact_encounter is one row per encounter, dim_claim one row per position (unbilled included), dim_/fact_service_line one row per charge, and each *_adjudication model one row per (grain, cob). The calculation facts are one row per (encounter or line, run, scenario). fact_encounter_variance covers only scored encounters, and only against cob 1.
Keys
sk()hashes in the source system and tenant:md5('athena:' || context_id || ':' || id). Athena ids are unique only within one context, so never join on a bare Athena id.global_key()is unscoped and used only for identities the outside world assigns (TIN, address), so one real entity reached from two systems lands on one row.- The legacy pipeline uses a different grain. A legacy account is a clinical encounter or appointment (
transform_account_grain), with a<claimid>-CLAIMONLYfallback, keeping one claim per account.encounter.legacy_account_idmaps between them, but several families on one appointment can share a value.
PART 2Master entities
Four entity types are mastered: location, provider, practice and billing entity (TIN). Master IDs are attached all through silver and gold. Master attribute values are read in only a few places. Much of an encounter's context still comes straight from Athena.
Where masters come from
bronze_stewardship_ledger → stg_stewardship_current. Append-only; latest event per entity.stg_mdc_location, _provider, _practice, _tin unpack the JSON payload.crosswalk/*.csv → stg_crosswalk. Kept only if the master exists in the ledger.stg_hidden_master. Ledger hidden flag wins; otherwise the seed CSV.published_crosswalk(): the crosswalk minus rows pointing at a hidden master.The checked-in crosswalks hold 151 location, 316 provider, 1 practice and 2 TIN rows. hidden_master.csv lists 9 locations, all with reason "AllyMed".
| Entity | Observation key | Source column |
|---|---|---|
| location | sk(departmentid) | The claim's servicedepartmentid |
| provider | sk(providerid) | The claim's renderingproviderid |
| practice | sk(context_id) | The whole Athena context counts as one practice |
| tin | global_key('tin', raw tax id) | paytotaxid1, plus paytotaxid2 on claim cob ≥ 2 |
Where master IDs attach
| Model | location_id | rendering_provider_id | practice_id | billing_entity_id | Hidden master |
|---|---|---|---|---|---|
| stg_encounter_resolution → encounter | ✓ | ✓ | ✓ | ✓ | Gates publication; the family goes to excluded_encounter |
| claim | ✓ | ✓ | – | ✓ (cob 2 uses paytotaxid2) | Becomes a null ID |
| service_line | ✓ | ✓ | – | – | Becomes a null ID. Resolved from the line's own claim, not the root. |
| billing_entity_location | ✓ | – | – | ✓ | Edges from claim (TIN × department) pairs |
| provider_location | ✓ | ✓ | – | – | Edges from the provider payload's location_ids, filtered to published masters |
In gold, the fact_* models carry these IDs forward from encounter, or from service_line for line-grain facts. The encounter's scopes column ([location_id, practice_id]) drives row-level access on every gold model.
Where master values are read
| Master | Payload fields → silver column | Consumers |
|---|---|---|
| location | name, address, city, state→state_code, zip→zip_code (5 digits), place_of_service, active | dim_location; encounter_details.location_name; scoring zip_code and state_code; pricing scorer facility stateCode and zipCode (scorer/conform.py:64) |
| provider | npi→individual_npi, first and last name, specialist_code→specialty, credential→provider_type, taxonomy_code→taxonomy, location_ids | dim_provider, dim_provider_location; encounter_details provider name, type and NPI (feeds the "provider" filter); pricing scorer charge-level NPI and taxonomy (conform.py:77) |
| practice | name, tin, active | dim_practice only |
| tin | tax_id with non-digits removed, name→display_name, active | dim_billing_entity; encounter_details tax_id and billing_entity_name (feeds the "tax_id" filter) |
Where source values are used instead
| Value | Taken from | Used by | Master equivalent bypassed |
|---|---|---|---|
| encounter.tax_id | Raw paytotaxid1 | Scoring tax_id attribute | billing_entity.tax_id, which encounter_details uses |
| place_of_service | Athena department (stg_department) | dim_encounter, dim_claim, encounter_details, scoring pos_code | location.place_of_service |
| place_of_service_type, care_setting, type_of_bill | Athena department | dim_encounter; pricing facilityType | None in the payload |
| dos_state, locality | Athena department (GPCI) | dim_encounter | location.state_code |
| billing_npi | Raw paytonpi1 (paytonpi2 on claim cob 2) | dim_encounter, dim_claim | None; the TIN payload has no NPI |
| line place_of_service | Athena charge, and the charge's department | dim_service_line, service_line_details, pricing charge placeOfService | – |
| observed_location / _provider / _billing_entity | Entirely source data, with resolves and hidden flags | Gold observed_* (passed through); Dagster check location_resolution | Deliberately source-side: this is the review queue |
entity_encounter would mix both kinds (source department_name beside master city and state), but it is disabled and isn't built.
PART 3Scored attributes
Rule set selection runs in dbt silver, before the pricing scorer. encounter_rule_set_score scores every (encounter, cob) against every rule set's include and exclude lists. The pricing scorer (defs/pricing/assets.py with shared/pricing-scorer) reads the candidates above its consistency threshold and resolves calculations from them.
The attribute set
Defined by three macros in projects/commonwealth/dbt/macros/. scored_attributes() joins them in this order. Weights are versioned by scoring_weight_version: "2026-09-24" and scoring_algo_version: "1.5.0" in dbt_project.yml.
| Group | Macro | Attribute | Weight | Include gates? |
|---|---|---|---|---|
| coverage | coverage_attributes() | payer_id | 8.0 | Yes |
| coverage | lob | 6.0 | Yes | |
| coverage | plan_type | 5.0 | Yes | |
| coverage | plan_descriptor | 9.0 | Yes | |
| billing_entity | billing_entity_attributes() | tax_id | 4.0 | No, credit only |
| location | location_attributes() | zip_code | 4.0 | No, credit only |
| location | pos_code | 4.0 | No, credit only | |
| location | state_code | 3.0 | No, credit only |
Where each value comes from
encounter_coverage_resolved.sql is the input contract: one row per (encounter, cob) with a service date.
| Attribute | Derivation | Origin |
|---|---|---|
| payer_id | Regex rules over insurancereportingcategory, then insurancepackagename, give a brand name. medallion.payer_reference() maps it to a UUID. Medicare Set-Aside, TRICARE, VA and Medicaid deliberately resolve to no payer. | Computed, source_payer.sql + routing_rules.sql |
| lob | Name keywords first (military, workers' comp, auto), then product type, package type, IRC group and the package name's trailing parenthetical (plan_type_hint) | Computed, same files |
| plan_type | insuranceproducttype, then plan_type_hint | Computed, same files |
| plan_descriptor | insurancepackagename, passed through | Source |
| tax_id | The claim's paytotaxid1 (encounter.sql:21) | Source |
| pos_code | Athena department's placeofservicecode | Source |
| zip_code | Mastered location, 5 digits | Master |
| state_code | Mastered location | Master |
For each routing target, the matching rule with the lowest priority wins. The rule that fired is recorded in source_payer.routing_evidence as JSON, which Phoenix shows. The payer, line-of-business and plan-type vocabularies are generated from shared/payer-reference/payers.json.
Normalization
macros/scored_attribute_value.sql reduces each value to one comparable form. The encounter side and the rule set selector side both pass through it (normalized_selector_values()), so the two vocabularies can't drift. A value that doesn't fit its shape becomes null; it is never forced into one.
| Attribute | Canonical form |
|---|---|
| tax_id | Nine digits with an optional dash after the first two; the dash is removed |
| payer_id | A UUID is lowercased; anything else compares exactly. A null becomes the sentinel 'unresolved'. |
| lob, plan_type | Trimmed, lowercased |
| plan_descriptor | Trimmed, uppercased |
| zip_code | ZIP+4, 9, 5 or 4 digits; keeps 5, left-padded with zeros |
| pos_code | 1–2 digits, left-padded to 2 |
| state_code | Two letters, uppercased. Never derived from the ZIP. |
How scoring works
- Matching (
selector_matches()):plan_descriptormatches if the value contains any listed substring. Every other attribute needs an exact match. - Credit: a match earns 1.0 and a miss 0.
zip_codeearns 0.5 when the first three digits match. A null value earns no credit and counts as unknown weight. - Gates: every exclude drops the encounter. Only coverage includes gate. A null coverage value passes its include as unknown only when the payer resolved; an unresolved payer is admitted only by an include that names
'unresolved'. - Outputs:
consistency= match_score / applicable_weight;completeness= applicable / (applicable + unknown);identity_confirmedis true when any coverage attribute matches;attribute_outcomeslists match, conflict, partial or unknown per attribute. Rows with zero applicable weight are dropped.
Other derived fields
care_setting: a CASE overplaceofservicetypeinstg_department.sql(asc, outpatient_hospital, inpatient_hospital, office).opportunity_classification(): No Response, Denial, Underpayment or No Opportunity, with the threshold fromorg.underpayment_threshold.procedure_code_system(): CPT, HCPCS or LOCAL by code range and pattern.resolved_encounter_money(),primary_allowed_by_encounter()anddenied_amount_by_encounter(): the money measures behindfact_encounter_variance.payment_varianceandvariance_pct.
PART 4Phoenix
Phoenix is the RevFind app: a .NET API and a React web app over the customer's gold layer. Its docs say gold "arrives precomputed" and that pricing math belongs to the medallion stack. In practice Phoenix owns a set of rules of its own, and in several places it recomputes something gold already publishes, sometimes with a different answer.
What it reads and writes
Phoenix reads gold only, through DuckDB over parquet (src/Phoenix.Infrastructure/DataLake/GoldDbContext.cs). It reads no silver and none of the star schema. Every read is scoped by the row's scopes array: a caller sees a row if any of their practice or location ids is in it.
| Direction | Gold model or landing file | Used for |
|---|---|---|
| Reads | encounter_details | Encounter page, search, project and recovery figures |
| Reads | service_line_details | Charge and remit tab, first-line CPT and billed sum per encounter |
| Reads | encounter_transaction_details | Transaction ledger, denial reasons, allowed as of a date |
| Reads | encounter_coverage_details | Coverage panel, primary payer, source-payer to payer mapping |
| Reads | calculation_details | Rate benchmark table (line estimates only) |
| Reads | encounter_filter_options | Search filter lists |
| Reads | patient_encounter_details | Other Encounters tab |
| Writes | encounter-label-event.ndjson | Automatic, every 30 minutes and on shutdown. Feeds silver encounter_label. |
| Writes | appeal-stage-event.ndjson | Automatic, same schedule. Feeds silver encounter_appeal_stage. |
| Writes | underpayment-threshold.ndjson | On every threshold change. Feeds silver org. |
| Writes | stewardship-ledger.ndjson | Manual only, from a button on the internal setup page. Feeds every master entity (Part 2). |
Each write is a full snapshot to landing/mdc/dt=YYYY-MM-DD/ in the customer bucket. Notes, projects, packages, outcomes, lump sums and templates are not written back, so gold doesn't see them.
Three opportunity rules
"Is this encounter an underpayment or a denial?" is answered three ways, and none of them reads gold's opportunity_type.
Gold opportunity_type | Phoenix server | Phoenix web app | |
|---|---|---|---|
| Where | macros/opportunity_classification.sql on fact_encounter, fact_service_line | DataLake/VarianceOpportunity.cs and again in Core/Projects/AppealEligibility.cs | ChargeRemitTab.tsx, EncounterTable.tsx, ProjectEncounterTable.tsx, RateBenchmarkTable.tsx |
| Values | One of No Response, Denial, Underpayment, No Opportunity | Any of underpayment and denial, both possible, or none | Red or not |
| No remit | No Response | No such state. Variance equals expected, so it reads as an underpayment. | Same as the server |
| Denial | The payer's denied money > 0 (CARC Denial category, excluding 18 and B13) | Variance on denied lines > $0.005 | Line variance > threshold, regardless of denial |
| Underpayment | Primary allowed − primary paid > threshold | Expected − allowed − denied-line variance > threshold | Line variance > threshold (charge tab), or variance > 0 (search and project tables) |
| Threshold | Lake org.underpayment_threshold; the macro treats a missing value as 0 | Postgres Customers.UnderpaymentThreshold, default 1 | Same as the server, or 0 until it loads |
The gold and Phoenix rules measure different gaps. Gold compares what the payer allowed with what it paid. Phoenix compares what we expected with what the payer allowed. They can agree on an encounter by coincidence, but they are not two versions of one rule. The threshold value is the same number in both places, because Phoenix writes it to the lake, but each side reads its own copy.
Phoenix's rule decides real behavior: the search filter, the opportunity badges on the encounter page, whether an encounter can join a project, and whether the appeal deadline is shown at all.
Where Phoenix reimplements gold
| Phoenix logic | Where | Gold equivalent | Verdict |
|---|---|---|---|
| Opportunity types | VarianceOpportunity.cs, AppealEligibility.cs | opportunity_type | Contradicts |
| Allowed as of a date: latest cob-1 remit row per (line, claim) on or before the cutoff, summed | DuckDbGoldEncounterReader.cs:235-264 | adjudication_recency() ranks by processed position, posted date, transaction date, id, then reversal | Reimplements recency with only date and id |
| Charge tab totals: sum of line columns | EncounterEndpoints.cs:309-317 | Header money on encounter_details; gold warns line variance doesn't sum to it | Contradicts the header by construction |
| Encounter billed amount: sum of lines | DuckDbGoldEncounterReader.cs:205 | fact_encounter.billed_amount (not read) | Same math, different model |
| Latest claim answer: order by date, take first | DuckDbGoldEncounterReader.cs:310-312 | Already resolved in encounter_details | Redundant, harmless |
| Filter options fallback when the file is missing | GoldEncounterSearch.cs:201-231 | encounter_filter_options | Full copy of the gold model |
Primary coverage: cob == 1 | EncounterEndpoints.cs:686, SourcePayers.cs, AppealEncounterList.cs, and again in the web app | encounter_details.source_payer_* already names the primary | Two sources for one value |
| Sort orders for transactions, coverages, other encounters, calculation steps | Several readers | Gold pre-sorts each model | Redundant, consistent |
Rules Phoenix owns
These have no gold equivalent. Most are platform state that gold deliberately leaves out (fact_encounter_variance.sql says validated opportunity and appeal state are "platform state").
- Recovered amount, two ways. Encounter page, project stats and the appeal package use
actual_reimbursement − initial_allowed_amount, the change since the payer's first answer. The Recovery page uses current allowed minus allowed as of the filing date. Lump sums are added to project stats and listed separately on the Recovery page. - Validated opportunity. Sum of
payment_variancefor project members past In Review and still active. - Project payer. A project appeals one payer: the most common cob-1
payer_idamong its members. Candidates with a different or unresolved payer are rejected. - Appeal eligibility. One active project per appeal type per encounter, and the encounter must pass Phoenix's opportunity rule for the project's type.
- Appeal stages. 19 stages, a three-level ladder plus legal, and "a spent level is never reused." Manual stage changes skip the ladder rules on purpose.
- Source payer to payer mapping. Built from cob-1 coverage rows, because
dim_source_payerhas nopayer_id. The join uses the insurance id alone, without the source system. - Age at service in whole years, with a 90+ band when PHI is masked, and an age filter in search.
- Appeal package CSV. "Practice" is filled with the billing entity name or TIN. "Geography" is always blank, though
dim_encounter.dos_stateexists; it just isn't onencounter_details.
Phoenix as the stewardship writer
Phoenix is where master entities are created and edited (Part 2). The pipeline folds the ledger the same way Phoenix does: latest event by occurred_at desc, id desc. The payload keys mostly match, with two gaps.
| Entity | Phoenix writes | Pipeline also reads | Effect |
|---|---|---|---|
| location | name, address1, address2, city, state, zip, practice_id, npi, place_of_service, active | hidden | Hiding comes only from hidden_master.csv |
| provider | first_name, last_name, credential, npi, taxonomy_code, location_ids, active | specialist_code, hidden | provider.specialty is always null in silver and gold |
| practice | name, tin (digits only), active | hidden | Keys match |
| tin | tax_id (digits only), name, location, active | hidden | Keys match; both sides strip non-digits |
- Phoenix never writes the crosswalk. Silver keeps a master only if a checked-in
crosswalk/*.csvrow points at it. A location created in Phoenix stays out of silver and gold until someone edits a CSV in the repo, and the ledger itself reaches the lake only when someone presses the push button. - Phoenix validates state codes, ZIP (
^\d{5}(-\d{4})?$), 10-digit NPIs, taxonomy shape, and TINs (^\d{2}-?\d{7}$). Location, practice and provider names allow only letters, digits, spaces and periods, so&, apostrophes, hyphens, commas and slashes are rejected. - Phoenix matches locations by name, not id. The location picker and the search location filter join gold
location_nameto Phoenix locations by normalized name (LocationEndpoints.cs:59-71,GoldEncounterSearch.cs:56-57), though gold carrieslocation_idinscopes. - Appeal stages lose their project in gold. Phoenix tracks stage per (project, encounter). Gold
fact_encounterkeeps one stage per encounter witharg_max(appeal_stage, stage_changed_at), so an encounter in two projects shows whichever changed last.
Rules in the web app
The React app (web/src/) adds another layer. Some of it duplicates server rules; some is found nowhere else.
| Rule | Where | Server or gold source |
|---|---|---|
| Appeal deadline as days left, or "overdue", on the browser's clock | encounter/EncounterDetailsPage.tsx:426-432 | Gold has the date only (and it is null for Commonwealth today) |
| Stage labels and stage sets (active, before filing, needs attestation, ready) | projects/appealStages.ts, SetStageModal.tsx | Copies of Core/Projects/AppealStage.cs |
| Stage-change planner (forward, escalate, rewind, missing-level warnings) | projects/appealStages.ts:182-248 | Client only. Counts the Ready shelves as the level being prepared; the server's LevelOf counts them as the level already reached. |
| Root cause editable only in In Review | encounter/ProjectsTab.tsx:230 | The server and the appeal package page allow any stage before filing |
| Recovery page totals, average days, by payer, by month | encounter/RecoveryPage.tsx | The API returns rows only |
| Project % complete, package total variance, recovered % | api.ts:2288-2292, projects/AppealPackagePage.tsx | Package total reads live variance, even for a filed package |
| Rate benchmark: current rate, deltas, "paid at this rate" within $0.005, "within tolerance" | encounter/RateBenchmarkTable.tsx | Repeats the server's half-cent floor and applies the threshold to overpayments too |
| Place of service names | codeSets.ts and settings/placesOfService.ts | Two lists with different names; gold has codes only |
| Care setting, claim form, COB and variance-type labels | api.ts:573-597, format.ts:71-77 | Hardcoded. The encounter page and the search filter label care setting and claim form differently. |
| Appeal field catalog and letter length limits | projects/appealFields.ts | Copy of Core/Projects/AppealFieldCatalog.cs |
PART 5Findings
Things that look inconsistent, unverified, unused or wrong. Some may be intentional; most need a decision about which side is right before anything is fixed.
Pipeline
The pricing request's facility comes from two places Mismatch
stateCode and zipCode come from the master location, while facilityType comes from the Athena department. A stewardship correction changes the first two and not the third.
Scoring reads source data for tax_id and pos_code Mismatch
tax_id is raw paytotaxid1 and pos_code is the department's. Only zip and state come from the master. In the same encounter, encounter_details shows the master TIN, and the master location.place_of_service is not used by scoring.
Two states per encounter Mismatch
dim_encounter.dos_state comes from the department and dim_location.state_code from the master. Nothing checks that they agree.
Claim and encounter can resolve different TINs Mismatch
For cob 2, claim resolves the TIN from paytotaxid2, but encounter publication checks only paytotaxid1. A secondary claim can carry a null or different billing_entity_id from its encounter.
Lines resolve location and provider from their own claim Mismatch
The encounter uses the attribute claim's department and provider; a line uses the department and provider of the claim it sits on. After a rebill that moves department, a line's location_id can differ from its encounter's.
TIN observations are matched on the raw string Fragile
'41-1234567' and '411234567' are separate observations. Both rows in crosswalk_tin.csv map to one master, which is probably this case. Every spelling has to be listed, or a new spelling leaves encounters unresolved.
Families assume originalclaimid is flat Fragile
The family groups claims by originalclaimid, which assumes it always points at the very first claim. Nothing tests that. If Athena ever chains it (a rebill pointing at the previous rebill), one real encounter splits into several.
Practice adds nothing beyond the context Fragile
The practice observation is sk(context_id) with one crosswalk row, so every Commonwealth encounter gets the same practice and scopes depends entirely on location. A second context or practice split will need a real observation key.
An encounter mixes one claim's attributes with family-wide dates Fragile
Attributes come from the attribute claim, but service_date_from/to span every charge in the family, including charges on claims the attribute claim replaced.
A line has one claim_id but can be answered at two positions Fragile
Use the claim_id on the adjudication row, not the line's, when you need the position that answered.
The legacy account and the encounter are different grains Fragile
Several families on one appointment share a legacy_account_id, so old-versus-new comparisons at encounter grain won't reconcile row for row.
Master payload fields nothing reads Unused
location.npi (billing_entity_location.location_npi is hardcoded null), location.practice_id (so no practice-to-location link is published), and tin.location. practice.tin and location.place_of_service reach only their dimensions.
The claim position docs overstate the slot guarantee Docs
The docs say stg_claim_position's population "matches stg_claim_coverage_slot's by construction." In the code, positions are a superset: billed-but-unconfigured positions and unconfigured cob 0 exist only as positions. The guarantee that holds is slot ⊆ position, which is the one that prevents orphaned fact rows.
Phoenix
Phoenix's opportunity rule disagrees with gold's Mismatch
Gold's opportunity_type is never read. An encounter with no remit is No Response in gold and an underpayment in Phoenix, because its variance equals the full expected amount. The two underpayment tests measure different gaps (allowed − paid versus expected − allowed). One of them is the definition the business means; the other should go.
The same rule is written several times Fragile
VarianceOpportunity and AppealEligibility each implement denial and underpayment with their own $0.005 floor. The web app colors variance by a third and fourth rule (over threshold, or just over 0), and repeats the floor in the rate benchmark. Stage sets and the appeal field catalog are also copied into the web app.
A master created in Phoenix doesn't reach gold on its own Fragile
Phoenix writes the ledger but never the crosswalk, and silver drops any master without a checked-in crosswalk row. The ledger also lands in the lake only when someone presses the internal push button.
The pipeline reads payload fields Phoenix never writes Unused
specialist_code is never written, so provider specialty is null all the way to dim_provider. hidden is never written either, so hiding works only through the seed CSV.
Locations are matched to gold by name Fragile
The location picker and search filter join on normalized location_name, not location_id. A rename on either side, or two locations with one name, moves encounters into the wrong group or the "unmapped" group. Phoenix's name rule also rejects &, apostrophes, hyphens, commas and slashes.
Allowed-as-of-filing uses its own recency rule Mismatch
Gold's adjudication_recency() ranks a below-primary answer filed at cob 1 last, then orders by posted date. Phoenix orders by transaction date and id only, because the ledger model doesn't carry the other columns. The Recovery page's baseline can therefore pick a different answer than gold would.
Two definitions of "recovered" and of "last remit" Mismatch
The encounter page, project stats and appeal package use allowed now minus the first allowed. The Recovery page uses allowed now minus allowed at filing. The package page's "disbursed" date is the claim-level answer date; the Recovery page uses the latest answer including line level.
Charge tab totals won't match the encounter header Mismatch
The totals row sums the line columns. Gold's own comment says line variance doesn't sum to the encounter's, because claim-level answers are invisible to lines.
Search totals count unremitted encounters Mismatch
The positive-variance total includes encounters the payer hasn't answered, where variance is the full expected amount. Gold says a variance means nothing until the payer has answered, but encounter_details doesn't expose has_remit to filter on.
Search by account id doesn't work Docs
Gold publishes root_claim_id and source_clinical_encounter_id with the comment "Phoenix searches this … for an encounter by account id." Phoenix searches encounter id, provider name and payer name only, and doesn't map root_claim_id.
Denial appeals get underpayment letters Bug
The generated letter always argues "reimbursed below the negotiated fee schedule." The package handler never branches on appeal type.
Missing money shows as $0.00 Bug
Four tables coalesce null to 0 for sorting and then render the coalesced value, so an unscored variance or unknown recovery reads "$0.00" and sorts between overpaid and underpaid rows. The search CSV export does the same.
The denial callout shows the billed amount Bug
"Denied · $X" renders line.billed. Gold has the line's denied_amount and the reader loads it, but the API doesn't pass it to the page. Likewise gold's is_denial flag isn't passed, so denial adjustments get the plain "adjustment" pill.
Every estimate carries the encounter's own payer Docs
calculation_details.source_insurance_id is the encounter's primary payer on every row. The C# comment calls it "the rule set payer's insurance id," so the rate benchmark's coverage sort does nothing and every row is labeled with the same payer.
Root cause editing differs between screens Mismatch
The encounter's Projects tab allows it only in In Review. The server and the appeal package page allow any stage before filing.
Client and server count ladder levels differently Fragile
The web app's stage planner groups "Ready for Level 1" under Level 1; the server's LevelOf puts it at level 0, the level already reached. The same one-step offset applies to every Ready shelf. The planner's escalate, rewind and missing-level warnings can disagree with what the server allows.
Gold keeps one appeal stage per encounter Mismatch
Phoenix stage belongs to the (project, encounter) pair. fact_encounter.appeal_stage takes the latest change across all projects, so Omni shows whichever project moved last.
Labels disagree between pages Mismatch
There are two place-of-service lists with different names. The encounter page shows care setting "Asc" and claim form "837P"; the search filter shows "Surgery Center" and "CMS-1500" for the same values.
The coverage switcher assumes the first row is primary Fragile
It opens on index 0. Positions are independent, so a secondary-only encounter opens on secondary while Plan, Group Name and Contract still come from the cob-1 fields.
Project "Total Denial variance" isn't denial variance Mismatch
The stat sums each member's full payment variance whatever the project type, under a label that names the type.