OCP → MD Clarity · Data-engineer runbook

Onboarding Mapping Spec

The executable companion to the crosswalk: for every MD Clarity silver entity — what grain, what key, what join, and how we know it's right. Read target-first; each mapping is flagged by requirement tier and by how well we proved the OCP side.

30
OCP tables
12
MDC entities
133
column mappings
92/133
row-proven
$6.40M total charges = $2.19M payments + $4.02M adjustments + $0.17M balance
00

How to read this — the confidence model

Every mapping carries two flags. Tier comes from the target spec: required a consumer task or the delta machinery breaks without it; recommended build it whenever OCP can; optional a standard name to use if you need the concept. Evidence is how well we proved the OCP side: row validated against real values; schema inferred from names/structure; ref a KB rule.

The validation ceiling — governs everything below OCP's fact tables were each sampled from a different tenant, so cross-fact joins do not resolve on these rows. The end-to-end bill→bill_item→production_summary→claim chain is schema-tier, not row-proven. Every join spine below is the intended transform SQL — correct by construction, proven only within each table until we get one coherent single-tenant extract (§6). Dimension tables are complete and join at 100% — those hops are row.
01

Grain & key decision records

The core. Pick the wrong grain for the account and every rollup — balance, denials, AR — is wrong. Each record: the target grain, the OCP source & key, the join spine you'd write, and the trap you must not walk past. ⚠ warn cards are the two that decide the whole onboarding.

DR-1 TransformAccount bill PK bill_id
grain one row per billed account (claim/invoice/encounter)
merge key AccountID = bill_id

The account is bill, NOT claim.

One encounter → many claimsrow
73 bill_items map to 2 claims each (primary + crossover). A claim grain double-counts the charge and service date; bill does not.
The spine is bill_insurance_idrow
claim is 1:1 with bill_insurance (5,826 = 5,826), and that key threads charges, payments_posted, payer_adjustments, production_summary.
The ledger lives on billrow
bill carries total_charges / total_payments / applied_adjustments_total / balance; the account books are a bill attribute.
-- TransformAccount: one row per bill
SELECT
  b.bill_id                             AS AccountID,     -- already -pod<NN>
  split_part(b.bill_id,'-pod',2)        AS DataSource,    -- 'pod12' -> §2
  b.patient_id                          AS PatientID,
  b.service_date                        AS ServiceDate,   -- non-null in target; assert
  b.bill_status                         AS EncounterStatus,
  TRY_CAST(b.balance       AS DOUBLE)   AS Balance,
  TRY_CAST(b.total_charges AS DOUBLE)   AS AmountCharged,
  b.primary_provider_id                 AS ProviderID,    -- -> Provider (100% FK)
  b.billing_facility_id                 AS FacilityID,    -- -> Location  (100% FK)
  b.last_modified_date                  AS UpdatedOn      -- delta spine; only on real change
FROM bill b;
Trap / open questionclaim_type ∈ {NEW, REPLACEMENT, CROSSOVER, VOIDED}. Confirm whether a rebill reuses the same bill_id (stays one account ✓) or spawns a new bill — this decides whether rebills collapse. And map UpdatedOn to a real source-change timestamp, never ingestion_date: it drives AccountDelta, so churned timestamps force a full legacy reprocess.
DR-2 TransformCharge bill_item PK bill_item_id
grain one charge line
merge key (AccountID, ChargeID) = (bill_id, bill_item_id)

One row per bill_item line; ChargeNumber 0 stays reserved for zero/claim-level.

-- TransformCharge: one row per bill_item line
SELECT
  bi.bill_id                            AS AccountID,     -- parent = the bill
  bi.bill_item_id                       AS ChargeID,
  CAST(bi.position AS INT)              AS ChargeNumber,   -- keep 0 free for zero-charge
  bi.code                               AS ProcedureCode,  -- CPT/HCPCS: 99213, J3301, 20610
  TRY_CAST(bi.charge  AS DOUBLE)        AS AmountCharged,
  TRY_CAST(bi.balance AS DOUBLE)        AS Balance,
  bi.service_date_from                  AS ServiceDate,
  bi.first_modifier                     AS Modifier1,
  CAST(bi.units AS INT)                 AS Units,
  bi.date_modified                      AS UpdatedOn
FROM bill_item bi;
Trap / open questionAmountCharged on the account must equal Σ bill_item.charge. The consumer loads AccountCharge before Account because Account depends on charge sums — the two must reconcile. revenue_code/status are constant/empty in this pro-fee sample; don't wire consumers to them.
DR-3 TransformTransaction production_summary PK transaction_identifier
grain one money-movement measure
merge key (AccountID, TransactionID) = (original_bill_id, transaction_identifier)

production_summary is already the unified charge+payment+adjustment ledger.

-- TransformTransaction: one row per posted money movement
SELECT
  ps.original_bill_id                   AS AccountID,
  ps.transaction_identifier             AS TransactionID,  -- stable 40-char hash
  ps.bill_item_id                       AS ChargeID,       -- NULL => account-level
  ps.coverage_type                      AS COB,            -- Self-Pay->P, Primary->1...
  ps.activity_type                      AS TransactionCategory,
  COALESCE(TRY_CAST(ps.ledger_payment_amount    AS DOUBLE),
           TRY_CAST(ps.ledger_adjustment_amount AS DOUBLE)) AS Amount, -- SIGN: verify
  ps.ledger_activity_date               AS TransactionDate,
  pa.bill_item_adjustment_group_code    AS GroupCode,      -- CO/PR/OA/PI
  pa.bill_item_adjustment_carc          AS AdjustmentCode, -- CARC 45, 2, 3, 253
  par.rarc_code                         AS RemarkCodes
FROM production_summary ps
LEFT JOIN payer_adjustments      pa  ON pa.bill_item_id = ps.bill_item_id
LEFT JOIN payer_adjustments_rarc par ON par.<key> = pa.<key>;
-- AdjustmentCategory: resolve CARC via CommonData.dbo.AdjustmentCodeRollup (ref)
Trap / open questionThree real traps: (1) sign convention is not standardized — verify against the legacy family before trusting any rollup; (2) ChargeID is NULL for account-level postings → ChargeNumber 0 — confirm the rule; (3) AdjustmentCategory is never local — route CARC through CommonData.dbo.AdjustmentCodeRollup.
DR-4 TransformAccountInsurance bill_insurance PK bill_insurance_id
grain payer stack as billed on the claim
merge key (AccountID, COB) = (bill_id, coverage_type→1/2/3)

The grain lives in a bill_insurance table not present in the export — reachable only via its FK.

-- TransformAccountInsurance: payer stack as billed, reconstructed
SELECT DISTINCT
  c.bill_id                             AS AccountID,
  CASE c.coverage_type WHEN 'Primary' THEN 1 WHEN 'Secondary' THEN 2
                       WHEN 'Tertiary' THEN 3 END           AS COB,
  c.payer_id                            AS SourceInsuranceID,
  c.insurance_policy_id                 AS SourcePatientInsuranceID,
  c.responsible_insurance_policy_number AS MemberID,
  c.policy_group_number                 AS GroupNumber,
  cl.claim_identifier                   AS ICN             -- claim control # per COB
FROM charges c
LEFT JOIN claim cl ON cl.bill_insurance_id = c.bill_insurance_id;
Trap / open questionThis is the weakest hop in the whole map and the one to raise with the OCP data owner. The AccountInsurance grain (member/group/ICN per COB) canonically lives in bill_insurance, which we see only by its FK. Confirm whether OCP can export bill_insurance — it's the difference between reconstructing from charges (schema) and reading it directly (row).
DR-5 TransformPatient + TransformPatientInsurance patient / insurance_policy PK patient_id / insurance_policy_id
grain one patient per source · current enrollment stack
merge key PatientID · (PatientID, COB, DataSource)

Nearly all patient fields are row-validated. Consent = email_opt_in — a consumer-required column.

-- TransformPatient (excerpt) + enrollment COB
patient.patient_id      -> PatientID
patient.email_opt_in    -> ElectronicCommunicationConsent  -- LoadPatients expects this
insurance_policy.ranking-> COB   -- ENROLLMENT order (1/2/3): 8,348 / 1,577 / 74  [row]
Trap / open questionDon't conflate the two COBs. TransformPatientInsurance.COB = insurance_policy.ranking (what the patient has); TransformAccountInsurance.COB = payer position on the specific bill. A patient can be BCBS-primary in enrollment but have a bill sent Medicare-primary.
DR-6 TransformVisit + TransformVisitInsurance appointment / aips PK appointment_id
grain one scheduled appointment · visit coverage stack
merge key SourceID = appointment_id · (SourceID, COB) hard-delete

The Flow lane (scheduling/estimates), loaded by LoadVisits — separate delta from the account lane.

appointment.appointment_id       -> SourceID
appointment.patient_id           -> Patient
appointment.appointment_start_date -> AppointmentTime
appointment.appointment_status   -> AppointmentStatus  -- CHECKED_OUT/CANCELLED/NO_SHOW
-- VisitInsurance: (SourceID, COB) merged HARD-DELETE (stack fully replaced)
Trap / open questionTransformVisitInsurance is hard-delete (the whole stack replaced each load), unlike the account-side insurance. The visit lane has its own VisitDelta — the account delta does not cover it.
DR-7 Dimensions: Provider / Insurance / Location / Chargemaster staff / payer / facility / bill_item PK staff_id / payer_id / facility_id
grain reference entities — complete, 100% FK
merge key entity Key

The safe, load-first layer — complete in the export, joins at 100%.

Provider ← staffrow
npi, role, specialty, nucc_code→TaxonomyCode. staff is the role hub — ~60 FK columns point to it (rendering/ordering/supervising provider, biller, creator), all 100% containment.
Insurance ← payerrow
payer_name, active, financial_category_name→FinancialClass, plan type, is_medical/is_workers_comp/is_auto_pip→CoverageType.
Location ← facilityrow
location_npi (type-2 org NPI), tax_id, place_of_service_code.
Chargemaster ← bill_itemschema
No dedicated chargemaster table. Derive from quick_code/code/code_description. Open Q: can OCP export a fee schedule separately?
02

Keys & uniqueness

"Sometimes it's easy (an encounter id), sometimes it's bill + superbill + whatever." OCP is on the easy end — every table has a clean surrogate *_id PK. The -pod<NN> suffix on every id is the DataSource: MDC keys compose as CONCAT(source_id,'-',DataSource), and OCP ships them pre-composed (13496801-pod10). Keep the suffix or split it — be consistent.

MDC entityMerge keyOCP composition
TransformAccountAccountIDbill_id
TransformCharge(AccountID, ChargeID)(bill_id, bill_item_id)
TransformTransaction(AccountID, TransactionID)(original_bill_id, transaction_identifier)
TransformAccountInsurance(AccountID, COB)(bill_id, coverage_type→1/2/3)
TransformPatientInsurance(PatientID, COB, DataSource)(patient_id, ranking, pod)
TransformVisitSourceIDappointment_id
Consumer hard requirements (LoadAccountData — asserts, not style) Every one of the four account queries must return an AccountID column (the pager pages on it; missing ⇒ task fails). Column matching is by name, case-insensitive; a missing non-nullable column (AccountID, ServiceDate) throws. Merge is delete-all-rows-for-the-AccountID then re-insert — your queries must emit the complete current state per account.
03

Load-order DAG — crawl out from bill

Dimensions first (complete; everything references them), then the account fan-out. Inside LoadAccountData the internal order is fixed: Charge → Account (Account depends on charge sums) → Transaction → AccountInsurance. The Flow lane has its own delta.

1
Dimensions & entitiescomplete, 100% FK — load first
staff → Provider
payer → Insurance
facility → Location
patient → TransformPatient
insurance_policy → PatientInsurance
2
Process-AccountsLoadAccountData — fixed internal order
TransformCharge ← bill_item
TransformAccount ← bill
TransformTransaction ← production_summary
TransformAccountInsurance ← bill_insurance
3
Flow laneseparate delta — LoadVisits
TransformVisit ← appointment
TransformVisitInsurance ← aips
04

The money model — books, balance, billed, returned

The bill ledger obeys the RCM identity (validated on the bill sample):

$6.40M total charges = $2.19M payments + $4.02M adjustments + $0.17M balance
QuestionWhere it lives
Books — how money movesproduction_summary is already the unified ledger (charge/payment/adjustment amount, activity_type) → TransformTransaction.
Balance — what fills itaccount bill.balance; line bill_item.balance; patient/insurance split via accounts_receivable + bill_item.patient_responsible_from_*.
Billed — what's gone outbill.billed_status/billed_days_date; claim_bill_item.first_claim_submission_date → InitialBillDate; .claim_submission_date → LastBillDate.
Returned — denialspayer_adjustments group code → CO/PR; CARC → AdjustmentCode; payer_adjustments_rarc → RemarkCodes; claim_bill_item_denial_category + claim_was_ever_denied. Categorize via CommonData.dbo.AdjustmentCodeRollup.

microworld.json walks a $588 knee-injection encounter through this identity step-by-step (contractual adj → primary pay → coinsurance transfer → secondary pay → $0), plus a denial scenario — use it to sanity-check the sign & category mapping.

05

Column-level crosswalk — all 133 mappings

The machine-readable source of truth (mdc-crosswalk.csv), filterable. Coverage by evidence: 92 row · 40 schema · 1 ref.

entity
evidence
EntityMDC columnTierOCP tableOCP columnEv.Notes
06

Coverage checklist & validation

The "do they have X?" ticklist against consumer-required fields. ICN — yes (claim.claim_identifier). Consent — yes (email_opt_in, a column LoadPatients already expects).

Concept the spec wantsOCP source?Evidence
Account / encounter idbill.bill_idrow
Account ↔ patient linkbill.patient_idrow
Service date (non-null req.)bill.service_daterow
Delta timestamp (UpdatedOn)bill.last_modified_dateschema
Charge lines + CPT/amountbill_itemrow
Unified transaction ledgerproduction_summaryrow
Transaction sign conventionproduction_summarygap
Payer stack as billed (COB)bill_insurance (FK only)gap
Patient enrollment stackinsurance_policy.rankingrow
GroupCode / CARC / RARCpayer_adjustments(_rarc)row
ICN / payer control #claim.claim_identifierschema
E-comm consent (LoadPatients req.)patient.email_opt_inrow
Provider / Insurance / Location dimsstaff / payer / facilityrow
Chargemaster / fee schedulebill_item onlygap

Validation query pack — upgrade schemarow

One coherent single-tenant extract (one pod's bill + bill_item + production_summary + claim + claim_bill_item + charges + insurance_policy, sampled together) proves almost everything. Run these:

-- One coherent single-tenant extract upgrades almost everything below to `row`.
-- (one pod's bill + bill_item + production_summary + claim + claim_bill_item + charges + insurance_policy)

-- V1. GRAIN: is bill the account?  (upgrades DR-1)
SELECT bi.bill_id, COUNT(DISTINCT cbi.claim_id) AS claims_per_encounter
FROM bill_item bi JOIN claim_bill_item cbi ON cbi.bill_item_id = bi.bill_item_id
GROUP BY bi.bill_id HAVING COUNT(DISTINCT cbi.claim_id) > 1;   -- non-empty proves the double-count trap

-- V2. BALANCE ROLLUP: identity holds per bill on real rows?  (upgrades DR-1, money model)
SELECT b.bill_id,
       TRY_CAST(b.total_charges AS DOUBLE)
         - (TRY_CAST(b.total_payments AS DOUBLE)
            + TRY_CAST(b.applied_adjustments_total AS DOUBLE)
            + TRY_CAST(b.balance AS DOUBLE)) AS residual
FROM bill b QUALIFY ABS(residual) > 0.01;                      -- empty = identity holds

-- V3. COB FAN-OUT: does bill_insurance_id thread claim 1:1?  (upgrades DR-4)
SELECT COUNT(*) AS claims, COUNT(DISTINCT bill_insurance_id) AS distinct_bi FROM claim; -- expect equal

-- V4. TRANSACTION SIGN: which sign does the legacy family expect?  (resolves open Q1)
SELECT activity_type, coverage_type,
       MIN(TRY_CAST(ledger_payment_amount AS DOUBLE))    AS min_pay,
       MIN(TRY_CAST(ledger_adjustment_amount AS DOUBLE)) AS min_adj
FROM production_summary GROUP BY 1,2;

-- V5. ChargeID NULL rule: when is a txn posted at account level?  (resolves open Q4)
SELECT (bill_item_id IS NULL) AS account_level, activity_type, COUNT(*)
FROM production_summary GROUP BY 1,2;

Open questions for the OCP data owner

  1. Transaction amount sign in production_summary — verify vs the legacy family. Do not assume.
  2. bill_insurance as a first-class table — can OCP export it? (member/group/ICN per COB live there).
  3. Account vs claim rebill chains — does bill_id reuse on rebill, or does a new claim capture it?
  4. Charge- vs account-level transactions — confirm when production_summary.bill_item_id is NULL.
  5. Chargemaster / fee schedule — can OCP export one separately, or is bill_item the only source?
  6. InitialBillDate sourceclaim_bill_item.first_claim_submission_date vs bill.billed_days_date.