Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
ALTER TABLE idr.claim
--v2_mdcr_clm_lctn_hstry
ADD COLUMN clm_audt_trl_stus_cd VARCHAR(2),
ADD COLUMN clm_audt_trl_lctn_cd VARCHAR(5),
ADD COLUMN idr_insrt_ts_lctn_hstry TIMESTAMPTZ,
ADD COLUMN idr_updt_ts_lctn_hstry TIMESTAMPTZ;
39 changes: 39 additions & 0 deletions apps/bfd-model-idr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,45 @@ SYNTHETIC_CLM_ANSI_SGNTR.csv

These files represent the schema of the tables the information is sourced from, although for tables other than CLM_DT_SGNTR, the CLM_UNIQ_ID is propagated instead of the 5 part unique key from the IDR.

## Testing Mapping Changes

### Verifying FML Map Changes
To test updates to your FML map files, run `compile_resources.py` to generate a resource in the `out/` directory and verify the output:

```sh
uv run compile_resources.py \
-m maps/ExplanationOfBenefit-Base.map \
-i sample-data/EOB-Carrier-MCS-Sample.json \
-o out/ExplanationOfBenefit-MCS.json \
-r https://bfd.cms.gov/MappingLanguage/Maps/ExplanationOfBenefit-Base \
--test
```
### Updating Structure Definitions
If you add or move a field, you must update the input sample file and the corresponding Structure Definition file (e.g., defining CLM_AUDT_TRL_STUS_CD within the elements of ExplanationOfBenefit-Base.json).
Example element definition:

```text
{
"id": "ExplanationOfBenefit-Base.CLM_AUDT_TRL_STUS_CD",
"path": "ExplanationOfBenefit-Base.CLM_AUDT_TRL_STUS_CD",
"label": "Claim Status Code",
"min": 0,
"max": "1",
"type": [{ "code": "string" }]
}
```

### Resource Augmentation
Due to FML limitations, some complex mappings that require more than simple lookups are handled via augment_sample_resources.py.
For example, CLM_AUDT_TRL_STUS_CD on an EOB is derived by combining the status code, location code (CLM_AUDT_TRL_LCTN_CD), and source (META_SRC_SK). The augmentation script resolves these fields into the claim status code.
To test augmentation logic independently:

```sh
python augment_sample_resources.py {your_sample_file}.json
```

*Note: The uv run compile_resources.py command mentioned above also executes this script. You can inspect the output at out/temporary-sample.json and the compiled resource.*

## Data Dictionary

Generally, the data dictionary will source definitions from the IDR's table definitions. There are instances where this may not be the definition we wish to publish. To overwrite the definition from the IDR, or populate a definition not available from the IDR, populate the "definition" key for the relevant concept in the relevant StructureDefinition.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -749,7 +749,19 @@
"code": "string"
}
]
}
},
{
"id": "ExplanationOfBenefit-Base.CLM_AUDT_TRL_STUS_CD",
"path": "ExplanationOfBenefit-Base.CLM_AUDT_TRL_STUS_CD",
"label": "Claim Status Code",
"min": 0,
"max": "1",
"type": [
{
"code": "string"
}
]
}
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,18 @@
"code": "string"
}
]
},
{
"id": "SupportingInfoComponent.CLM_AUDT_TRL_STUS_CD",
"path": "SupportingInfoComponent.CLM_AUDT_TRL_STUS_CD",
"label": "Claim Status Code",
"min": 0,
"max": "1",
"type": [
{
"code": "string"
}
]
}
]
}
Expand Down
83 changes: 55 additions & 28 deletions apps/bfd-model-idr/augment_sample_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,7 @@ def convert_to_decimal(val: str | None) -> Decimal:
prvdr_hstry_for_npi = json.loads(
df[df["PRVDR_SK"] == str(provider_object.get("PRVDR_SK"))].iloc[0].to_json()
)
provider_object["NPI_TYPE"] = (
"2" if prvdr_hstry_for_npi.get("PRVDR_LGL_NAME") else "1"
)
provider_object["NPI_TYPE"] = "2" if prvdr_hstry_for_npi.get("PRVDR_LGL_NAME") else "1"

provider_object.update(
{
Expand All @@ -97,9 +95,7 @@ def convert_to_decimal(val: str | None) -> Decimal:

if prvdr_hstry_for_npi.get("PRVDR_TXNMY_CMPST_CD"):
taxonomy_val = prvdr_hstry_for_npi.get("PRVDR_TXNMY_CMPST_CD")
taxonomy_codes = [
taxonomy_val[i : i + 10] for i in range(len(taxonomy_val), 10)
]
taxonomy_codes = [taxonomy_val[i : i + 10] for i in range(len(taxonomy_val), 10)]
provider_object["taxonomyCodes"] = taxonomy_codes

# assign care team type + sequence for header-level info
Expand All @@ -116,20 +112,13 @@ def convert_to_decimal(val: str | None) -> Decimal:
# we need to ensure those NPIs match.
if column in item and item.get(column) == cur_sample_data.get(column):
if item.get("careTeamSequences"):
item["careTeamSequence"].append(
provider_object.get("careTeamSequenceNumber")
)
item["careTeamSequence"].append(provider_object.get("careTeamSequenceNumber"))
else:
item["careTeamSequence"] = [
provider_object.get("careTeamSequenceNumber")
]
item["careTeamSequence"] = [provider_object.get("careTeamSequenceNumber")]

# We may want to remove this in the future, depending on requirements
# regarding address info.
if (
column == "PRVDR_BLG_PRVDR_NPI_NUM"
and "CLM_BLG_PRVDR_ZIP5_CD" in cur_sample_data
):
if column == "PRVDR_BLG_PRVDR_NPI_NUM" and "CLM_BLG_PRVDR_ZIP5_CD" in cur_sample_data:
provider_object["prvdr_zip"] = cur_sample_data.get("CLM_BLG_PRVDR_ZIP5_CD")

provider_list.append(provider_object)
Expand Down Expand Up @@ -159,13 +148,9 @@ def convert_to_decimal(val: str | None) -> Decimal:
npis_used.append(provider_object.get("PRVDR_SK"))

prvdr_hstry_for_npi = json.loads(
df[df["PRVDR_SK"] == str(provider_object.get("PRVDR_SK"))]
.iloc[0]
.to_json()
)
provider_object["NPI_TYPE"] = (
"2" if prvdr_hstry_for_npi.get("PRVDR_LGL_NAME") else "1"
df[df["PRVDR_SK"] == str(provider_object.get("PRVDR_SK"))].iloc[0].to_json()
)
provider_object["NPI_TYPE"] = "2" if prvdr_hstry_for_npi.get("PRVDR_LGL_NAME") else "1"
provider_object.update(
{
fld: prvdr_hstry_for_npi[fld]
Expand All @@ -176,9 +161,7 @@ def convert_to_decimal(val: str | None) -> Decimal:

if prvdr_hstry_for_npi.get("PRVDR_TXNMY_CMPST_CD"):
taxonomy_val = prvdr_hstry_for_npi.get("PRVDR_TXNMY_CMPST_CD")
taxonomy_codes = [
taxonomy_val[i : i + 10] for i in range(len(taxonomy_val), 10)
]
taxonomy_codes = [taxonomy_val[i : i + 10] for i in range(len(taxonomy_val), 10)]
provider_object["taxonomyCodes"] = taxonomy_codes

if len(line_columns.get(line_col)) > 0:
Expand All @@ -190,9 +173,7 @@ def convert_to_decimal(val: str | None) -> Decimal:
provider_list.append(provider_object)

elif (
line_col in item
and "careTeamSequence" not in item
and item.get(line_col) in npis_used
line_col in item and "careTeamSequence" not in item and item.get(line_col) in npis_used
):
npi = item.get(line_col)

Expand Down Expand Up @@ -254,6 +235,52 @@ class Diagnosis:

cur_sample_data["diagnoses"] = [asdict(d) for d in diagnosis_codes]


# Resolve claim status code section
def meta_src_prefix(meta_src_sk: str | None) -> str:
return {
"1002": "V",
"1001": "M",
"1003": "F",
}.get(str(meta_src_sk), "")


def build_claim_audit_trail_composite(sample: dict) -> str:
meta_src_sk = str(sample.get("META_SRC_SK", ""))
status = sample.get("CLM_AUDT_TRL_STUS_CD")

if not status:
return None

prefix = meta_src_prefix(meta_src_sk)

# VMS
if meta_src_sk == "1002":
location = sample.get("CLM_AUDT_TRL_LCTN_CD", "")
return f"{prefix}{status}{location}"
# MCS & FISS
return f"{prefix}{status}"


def next_row_num(supporting_info):
row_nums = [
int(si["ROW_NUM"])
for si in supporting_info
if si.get("ROW_NUM") is not None and str(si["ROW_NUM"]).isdigit()
]
return str(max(row_nums, default=0) + 1)


composite_status_code = build_claim_audit_trail_composite(cur_sample_data)
if composite_status_code:
supporting_info = cur_sample_data.get("supportingInfoComponents", [])
supporting_info.append(
{
"ROW_NUM": next_row_num(supporting_info),
"CLM_AUDT_TRL_STUS_CD": build_claim_audit_trail_composite(cur_sample_data),
}
)

# add diagnosisSequence where necessary
for item in cur_sample_data.get("lineItemComponents", []):
if "CLM_LINE_DGNS_CD" in item:
Expand Down
14 changes: 0 additions & 14 deletions apps/bfd-model-idr/claims_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -1414,20 +1414,6 @@ def gen_pac_version_of_claim(claim: _GeneratedClaim, max_date: str):
pac_claim.CLM_FISS["GEO_BENE_SK"] = pac_claim.CLM["GEO_BENE_SK"]
pac_claim.CLM_FISS["CLM_NUM_SK"] = pac_claim.CLM["CLM_NUM_SK"]
pac_claim.CLM_FISS["CLM_TYPE_CD"] = pac_claim.CLM["CLM_TYPE_CD"]
pac_claim.CLM_FISS["CLM_CRNT_STUS_CD"] = random.choice(
[
"A",
"F",
"I",
"S",
"M",
"P",
"R",
"D",
"T",
"U",
]
)
add_meta_timestamps(pac_claim.CLM_FISS, claim.CLM, max_date)

pac_claim.CLM_LCTN_HSTRY = {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2444,16 +2444,23 @@
sourceView: V2_MDCR_CLM_FISS
sourceColumn: CLM_CRNT_STUS_CD
fhirPath: ExplanationOfBenefit.outcome
- inputPath: ExplanationOfBenefit-Professional-Base.CLM_AUDT_TRL_STUS_CD
- inputPath: ExplanationOfBenefit-Base.CLM_AUDT_TRL_STUS_CD
appliesTo:
- Inpatient
- Outpatient
- Hospice
- HHA
- SNF
- Carrier
- DME
sources:
- MCS
- VMS
- FISS
sourceView: V2_MDCR_CLM_LCTN_HSTRY
sourceColumn: CLM_AUDT_TRL_STUS_CD
fhirPath: ExplanationOfBenefit.outcome
notes: The status code can be retrieved via ExplanationOfBenefit.supportingInfo.where(category.coding.where(system='https://bluebutton.cms.gov/fhir/CodeSystem/Supporting-Information').exists() and category.coding.where(code='CLM_AUDT_TRL_STUS_CD').exists()).code.coding.code
- inputPath: ExplanationOfBenefit-Base.PRVDR_RFRG_PRVDR_NPI_NUM
appliesTo:
- Outpatient
Expand Down
26 changes: 12 additions & 14 deletions apps/bfd-model-idr/maps/ExplanationOfBenefit-Base.map
Original file line number Diff line number Diff line change
Expand Up @@ -95,22 +95,20 @@ group createEOBBase(source src: ExplanationOfBenefitBase, target tgt: BFDEOB){
src.providerList as provider where provider.isDuplicate.exists().not() and provider.careTeamType.exists().not() and provider.NPI_TYPE = 2 -> tgt.contained = create('Organization') as tgtOrg then createOrg(provider, tgtOrg) "Add Organization";
} "Add billing provider";


//Partial for PAC data, complete for adjudicated. Of note, we do this by source. So even if final action on PAC source, still "partial"
src.CLM_SRC_ID as sourceSk where sourceSk = '20000' -> tgt.outcome = "complete" "Set NCH outcome to complete.";
src.CLM_TYPE_CD as typeCd where(typeCd >= 1000 and typeCd < 2000) -> tgt.outcome = "partial" "Add phase 1 outcome";
//Check on 1000-2000 phases w/ the CLM_CRNT_STUS_CD. We can
src.CLM_TYPE_CD as typeCd where(typeCd >= 2000 and typeCd < 3000) -> tgt then {
//Bifurcate by if it's institutional or professional.
src.institutionalComponents as instComponents -> tgt then {
instComponents.CLM_CRNT_STUS_CD as curStatusCode -> tgt.outcome = translate(curStatusCode, 'https://bfd.cms.gov/MappingLanguage/Maps/ExplanationOfBenefit-Helper#CLM-CRNT-STUS-CD','code') "Set outcome for phase 2/3 FISS";
} "Set institutional outcome";

src.profComponents as profComponents -> tgt then {
profComponents.CLM_AUDT_TRL_STUS_CD as auditTrailStatusCode -> tgt.outcome = translate(auditTrailStatusCode, 'https://bfd.cms.gov/MappingLanguage/Maps/ExplanationOfBenefit-Helper#CLM_AUDT_TRL_STUS_CD','code') "Set outcome for phase 2/3 MCS";
} "Set professional outcome";
src.CLM_TYPE_CD as typeCd where(typeCd >= 1000 and typeCd < 3000) -> tgt then {
src -> tgt.outcome = "partial" "Default pac outcome to partial";
src.META_SRC_SK as srcSk where srcSk = '1001' -> tgt then {
src.CLM_AUDT_TRL_STUS_CD as auditTrailStatusCode -> tgt.outcome = translate(auditTrailStatusCode, 'https://bfd.cms.gov/MappingLanguage/Maps/ExplanationOfBenefit-Helper#CLM_AUDT_TRL_STUS_CD_MCS','code') "Set outcome for phase 2/3 MCS";
} "Set outcome for MCS";
src.META_SRC_SK as srcSk where srcSk = '1003' -> tgt then {
src.CLM_AUDT_TRL_STUS_CD as auditTrailStatusCode -> tgt.outcome = translate(auditTrailStatusCode, 'https://bfd.cms.gov/MappingLanguage/Maps/ExplanationOfBenefit-Helper#CLM_AUDT_TRL_STUS_CD_FISS','code') "Set outcome for phase 2/3 FISS";
} "Set outcome for FISS";
src.META_SRC_SK as srcSk where srcSk = '1002' -> tgt then {
src.CLM_FINAL_ACTION as finalAction where finalAction = Y -> tgt.outcome = "complete" "Set VMS outcome to complete.";
src.CLM_FINAL_ACTION as finalAction where finalAction = N -> tgt.outcome = "partial" "Set VMS outcome to partial.";
} "Set outcome for VMS";
} "Complete additional outcomes";
//When we get to carrier/DME, we'll do the same with MCS/VMS status.

//Building out procedure, diagnosis, supportingInfo, item, and then careteam

Expand Down
Loading