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?