hccinfhir 0.1.6__tar.gz → 0.1.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/PKG-INFO +1 -1
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/extractor_837.py +115 -62
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/pyproject.toml +1 -1
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/.gitignore +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/LICENSE +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/README.md +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/__init__.py +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/data/__init__.py +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/data/hcc_is_chronic.csv +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/data/hcc_is_chronic_without_esrd_model.csv +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/data/ra_coefficients_2025.csv +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/data/ra_coefficients_2026.csv +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/data/ra_dx_to_cc_2025.csv +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/data/ra_dx_to_cc_2026.csv +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/data/ra_eligible_cpt_hcpcs_2023.csv +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/data/ra_eligible_cpt_hcpcs_2024.csv +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/data/ra_eligible_cpt_hcpcs_2025.csv +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/data/ra_eligible_cpt_hcpcs_2026.csv +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/data/ra_hierarchies_2025.csv +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/data/ra_hierarchies_2026.csv +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/datamodels.py +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/extractor.py +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/extractor_fhir.py +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/filter.py +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/hccinfhir.py +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/model_calculate.py +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/model_coefficients.py +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/model_demographics.py +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/model_dx_to_cc.py +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/model_hierarchies.py +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/model_interactions.py +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/sample_files/__init__.py +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/sample_files/sample_837_0.txt +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/sample_files/sample_837_1.txt +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/sample_files/sample_837_10.txt +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/sample_files/sample_837_11.txt +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/sample_files/sample_837_12.txt +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/sample_files/sample_837_2.txt +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/sample_files/sample_837_3.txt +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/sample_files/sample_837_4.txt +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/sample_files/sample_837_5.txt +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/sample_files/sample_837_6.txt +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/sample_files/sample_837_7.txt +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/sample_files/sample_837_8.txt +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/sample_files/sample_837_9.txt +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/sample_files/sample_eob_1.json +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/sample_files/sample_eob_2.json +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/sample_files/sample_eob_200.ndjson +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/sample_files/sample_eob_3.json +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/samples.py +0 -0
- {hccinfhir-0.1.6 → hccinfhir-0.1.8}/hccinfhir/utils.py +0 -0
|
@@ -7,17 +7,23 @@ CLAIM_TYPES = {
|
|
|
7
7
|
"005010X223A2": "837I" # Institutional
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
class
|
|
11
|
-
"""
|
|
12
|
-
claim_id: Optional[str] = None
|
|
13
|
-
patient_id: Optional[str] = None
|
|
14
|
-
performing_provider_npi: Optional[str] = None
|
|
10
|
+
class HierarchyContext(BaseModel):
|
|
11
|
+
"""Tracks the current position in the 837 hierarchy"""
|
|
15
12
|
billing_provider_npi: Optional[str] = None
|
|
16
|
-
|
|
13
|
+
subscriber_patient_id: Optional[str] = None
|
|
14
|
+
patient_patient_id: Optional[str] = None
|
|
15
|
+
current_hl_level: Optional[str] = None
|
|
16
|
+
current_hl_id: Optional[str] = None
|
|
17
|
+
|
|
18
|
+
class ClaimContext(BaseModel):
|
|
19
|
+
"""Claim-level data that resets for each CLM segment"""
|
|
20
|
+
claim_id: Optional[str] = None
|
|
21
|
+
dx_lookup: Dict[str, str] = {}
|
|
17
22
|
facility_type: Optional[str] = None
|
|
18
23
|
service_type: Optional[str] = None
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
performing_provider_npi: Optional[str] = None
|
|
25
|
+
provider_specialty: Optional[str] = None
|
|
26
|
+
last_nm1_qualifier: Optional[str] = None
|
|
21
27
|
|
|
22
28
|
def parse_date(date_str: str) -> Optional[str]:
|
|
23
29
|
"""Convert 8-digit date string to ISO format YYYY-MM-DD"""
|
|
@@ -146,12 +152,16 @@ def parse_837_claim_to_sld(segments: List[List[str]], claim_type: str) -> List[S
|
|
|
146
152
|
├── Service Line 2 (2400)
|
|
147
153
|
└── Service Line N (2400)
|
|
148
154
|
|
|
155
|
+
Properly handles multiple loops at each hierarchy level:
|
|
156
|
+
- Multiple Billing Providers (2000A)
|
|
157
|
+
- Multiple Subscribers per Billing Provider (2000B)
|
|
158
|
+
- Multiple Patients per Subscriber (2000C)
|
|
159
|
+
- Multiple Claims per Patient/Subscriber (2300)
|
|
160
|
+
- Multiple Service Lines per Claim (2400)
|
|
149
161
|
"""
|
|
150
162
|
slds = []
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
in_rendering_provider_loop = False
|
|
154
|
-
claim_control_number = None
|
|
163
|
+
hierarchy = HierarchyContext()
|
|
164
|
+
claim = ClaimContext()
|
|
155
165
|
|
|
156
166
|
for i, segment in enumerate(segments):
|
|
157
167
|
if len(segment) < 2:
|
|
@@ -159,48 +169,89 @@ def parse_837_claim_to_sld(segments: List[List[str]], claim_type: str) -> List[S
|
|
|
159
169
|
|
|
160
170
|
seg_id = segment[0]
|
|
161
171
|
|
|
162
|
-
#
|
|
163
|
-
if seg_id == '
|
|
164
|
-
|
|
172
|
+
# ===== HIERARCHY LEVEL TRACKING (HL segments) =====
|
|
173
|
+
if seg_id == 'HL' and len(segment) >= 4:
|
|
174
|
+
hl_id = segment[1]
|
|
175
|
+
parent_id = segment[2] if segment[2] else None
|
|
176
|
+
level_code = segment[3]
|
|
177
|
+
|
|
178
|
+
hierarchy.current_hl_id = hl_id
|
|
179
|
+
hierarchy.current_hl_level = level_code
|
|
180
|
+
|
|
181
|
+
if level_code == '20': # New Billing Provider
|
|
182
|
+
hierarchy.billing_provider_npi = None
|
|
183
|
+
hierarchy.subscriber_patient_id = None
|
|
184
|
+
hierarchy.patient_patient_id = None
|
|
185
|
+
claim = ClaimContext()
|
|
186
|
+
|
|
187
|
+
elif level_code == '22': # New Subscriber
|
|
188
|
+
hierarchy.subscriber_patient_id = None
|
|
189
|
+
hierarchy.patient_patient_id = None
|
|
190
|
+
claim = ClaimContext()
|
|
191
|
+
|
|
192
|
+
elif level_code == '23': # New Patient
|
|
193
|
+
hierarchy.patient_patient_id = None
|
|
194
|
+
claim = ClaimContext()
|
|
165
195
|
|
|
196
|
+
# ===== NAME/IDENTIFICATION (NM1 segments) =====
|
|
166
197
|
elif seg_id == 'NM1' and len(segment) > 1:
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
in_claim_loop = False
|
|
170
|
-
in_rendering_provider_loop = False
|
|
171
|
-
elif segment[1] == '82' and len(segment) > 8 and segment[8] == 'XX': # Rendering Provider
|
|
172
|
-
current_data.performing_provider_npi = get_segment_value(segment, 9)
|
|
173
|
-
in_rendering_provider_loop = True
|
|
174
|
-
elif segment[1] == '85' and len(segment) > 8 and segment[8] == 'XX': # Billing Provider
|
|
175
|
-
current_data.billing_provider_npi = get_segment_value(segment, 9)
|
|
176
|
-
|
|
177
|
-
# Process Provider Specialty
|
|
178
|
-
elif seg_id == 'PRV' and len(segment) > 1 and segment[1] == 'PE' and in_rendering_provider_loop:
|
|
179
|
-
current_data.provider_specialty = get_segment_value(segment, 3)
|
|
198
|
+
qualifier = segment[1]
|
|
199
|
+
claim.last_nm1_qualifier = qualifier
|
|
180
200
|
|
|
181
|
-
|
|
201
|
+
# Billing Provider (2010AA in 2000A)
|
|
202
|
+
if qualifier == '85' and len(segment) > 8 and segment[8] == 'XX':
|
|
203
|
+
hierarchy.billing_provider_npi = get_segment_value(segment, 9)
|
|
204
|
+
|
|
205
|
+
# Subscriber or Patient (2010BA in 2000B)
|
|
206
|
+
elif qualifier == 'IL':
|
|
207
|
+
patient_id = get_segment_value(segment, 9)
|
|
208
|
+
if hierarchy.current_hl_level == '22': # Subscriber level
|
|
209
|
+
hierarchy.subscriber_patient_id = patient_id
|
|
210
|
+
hierarchy.patient_patient_id = None
|
|
211
|
+
elif hierarchy.current_hl_level == '23': # Patient level
|
|
212
|
+
hierarchy.patient_patient_id = patient_id
|
|
213
|
+
else:
|
|
214
|
+
# Fallback: assume subscriber
|
|
215
|
+
hierarchy.subscriber_patient_id = patient_id
|
|
216
|
+
|
|
217
|
+
# Patient (2010CA in 2000C)
|
|
218
|
+
elif qualifier == 'QC':
|
|
219
|
+
hierarchy.patient_patient_id = get_segment_value(segment, 9)
|
|
220
|
+
|
|
221
|
+
# Performing/Rendering Provider (2310D in 2300)
|
|
222
|
+
elif qualifier == '82' and len(segment) > 8 and segment[8] == 'XX':
|
|
223
|
+
claim.performing_provider_npi = get_segment_value(segment, 9)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ===== PROVIDER SPECIALTY (PRV segment) =====
|
|
227
|
+
elif seg_id == 'PRV' and len(segment) > 3 and segment[1] == 'PE':
|
|
228
|
+
# Only apply if last NM1 was performing provider (82)
|
|
229
|
+
if claim.last_nm1_qualifier == '82':
|
|
230
|
+
claim.provider_specialty = get_segment_value(segment, 3)
|
|
231
|
+
|
|
232
|
+
# ===== CLAIM LEVEL (CLM segment - starts 2300 loop) =====
|
|
182
233
|
elif seg_id == 'CLM':
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
current_data.claim_id = segment[1] if len(segment) > 1 else None
|
|
234
|
+
claim = ClaimContext()
|
|
235
|
+
claim.claim_id = segment[1] if len(segment) > 1 else None
|
|
186
236
|
|
|
187
237
|
# Parse facility and service type for institutional claims
|
|
188
238
|
if claim_type == "837I" and len(segment) > 5 and segment[5] and ':' in segment[5]:
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
#
|
|
193
|
-
elif seg_id == 'HI'
|
|
239
|
+
claim.facility_type = segment[5][0] if segment[5] else None
|
|
240
|
+
claim.service_type = segment[5][1] if len(segment[5]) > 1 else None
|
|
241
|
+
|
|
242
|
+
# ===== DIAGNOSIS CODES (HI segment) =====
|
|
243
|
+
elif seg_id == 'HI':
|
|
194
244
|
# In 837I, there can be multiple HI segments in the claim
|
|
195
245
|
# Also, in 837I, diagnosis position does not matter
|
|
196
246
|
# We will use continuous numbering for diagnosis codes
|
|
197
247
|
# use the last dx_lookup position as the starting position, and update
|
|
198
248
|
hi_segment = parse_diagnosis_codes(segment)
|
|
249
|
+
# Re-index for multiple HI segments in same claim
|
|
199
250
|
hi_segment_realigned = {
|
|
200
|
-
str(int(pos) + len(
|
|
251
|
+
str(int(pos) + len(claim.dx_lookup)): code
|
|
201
252
|
for pos, code in hi_segment.items()
|
|
202
253
|
}
|
|
203
|
-
|
|
254
|
+
claim.dx_lookup.update(hi_segment_realigned)
|
|
204
255
|
|
|
205
256
|
# Process Service Lines
|
|
206
257
|
#
|
|
@@ -220,54 +271,56 @@ def parse_837_claim_to_sld(segments: List[List[str]], claim_type: str) -> List[S
|
|
|
220
271
|
# SV205 (Required) - Unit Count: Format 9999999.999 (whole numbers only - fractional quantities not recognized)
|
|
221
272
|
# NOTE: Diagnosis Code Pointer is not supported for SV2
|
|
222
273
|
#
|
|
223
|
-
|
|
224
|
-
|
|
274
|
+
# ===== SERVICE LINE (SV1/SV2 segments - 2400 loop) =====
|
|
275
|
+
elif seg_id in ['SV1', 'SV2']:
|
|
225
276
|
linked_diagnoses = []
|
|
226
277
|
|
|
227
278
|
if seg_id == 'SV1':
|
|
228
|
-
# SV1 Professional Service
|
|
279
|
+
# SV1 Professional Service
|
|
229
280
|
proc_info = get_segment_value(segment, 1, '').split(':')
|
|
230
281
|
procedure_code = proc_info[1] if len(proc_info) > 1 else None
|
|
231
282
|
modifiers = proc_info[2:] if len(proc_info) > 2 else []
|
|
232
283
|
quantity = parse_amount(get_segment_value(segment, 4))
|
|
233
284
|
place_of_service = get_segment_value(segment, 5)
|
|
234
|
-
|
|
285
|
+
|
|
286
|
+
# Get diagnosis pointers and resolve to actual codes
|
|
235
287
|
dx_pointers = get_segment_value(segment, 7, '')
|
|
236
288
|
linked_diagnoses = [
|
|
237
|
-
|
|
289
|
+
claim.dx_lookup[pointer]
|
|
238
290
|
for pointer in (dx_pointers.split(':') if dx_pointers else [])
|
|
239
|
-
if pointer in
|
|
291
|
+
if pointer in claim.dx_lookup
|
|
240
292
|
]
|
|
241
293
|
else:
|
|
242
|
-
# SV2 Institutional Service
|
|
243
|
-
# Revenue code in SV201
|
|
294
|
+
# SV2 Institutional Service
|
|
244
295
|
revenue_code = get_segment_value(segment, 1)
|
|
245
|
-
# Procedure code in SV202
|
|
246
296
|
proc_info = get_segment_value(segment, 2, '').split(':')
|
|
247
297
|
procedure_code = proc_info[1] if len(proc_info) > 1 else None
|
|
248
298
|
modifiers = proc_info[2:] if len(proc_info) > 2 else []
|
|
249
|
-
# Quantity in SV205
|
|
250
299
|
quantity = parse_amount(get_segment_value(segment, 5))
|
|
251
|
-
place_of_service = None
|
|
252
|
-
# linked diagnoses are not supported for SV2
|
|
253
|
-
|
|
300
|
+
place_of_service = None
|
|
254
301
|
|
|
255
|
-
# Get service line details
|
|
302
|
+
# Get service line details (NDC, dates) - lookback from current segment index
|
|
256
303
|
ndc, service_date = process_service_line(segments, i)
|
|
257
304
|
|
|
305
|
+
# Determine effective patient ID (prefer patient level, fallback to subscriber)
|
|
306
|
+
effective_patient_id = (
|
|
307
|
+
hierarchy.patient_patient_id or
|
|
308
|
+
hierarchy.subscriber_patient_id
|
|
309
|
+
)
|
|
310
|
+
|
|
258
311
|
# Create service level data
|
|
259
312
|
service_data = ServiceLevelData(
|
|
260
|
-
claim_id=
|
|
313
|
+
claim_id=claim.claim_id,
|
|
261
314
|
procedure_code=procedure_code,
|
|
262
315
|
linked_diagnosis_codes=linked_diagnoses,
|
|
263
|
-
claim_diagnosis_codes=list(
|
|
264
|
-
claim_type=
|
|
265
|
-
provider_specialty=
|
|
266
|
-
performing_provider_npi=
|
|
267
|
-
billing_provider_npi=
|
|
268
|
-
patient_id=
|
|
269
|
-
facility_type=
|
|
270
|
-
service_type=
|
|
316
|
+
claim_diagnosis_codes=list(claim.dx_lookup.values()),
|
|
317
|
+
claim_type=claim_type,
|
|
318
|
+
provider_specialty=claim.provider_specialty,
|
|
319
|
+
performing_provider_npi=claim.performing_provider_npi, # ✅ Correct field
|
|
320
|
+
billing_provider_npi=hierarchy.billing_provider_npi,
|
|
321
|
+
patient_id=effective_patient_id,
|
|
322
|
+
facility_type=claim.facility_type,
|
|
323
|
+
service_type=claim.service_type,
|
|
271
324
|
service_date=service_date,
|
|
272
325
|
place_of_service=place_of_service,
|
|
273
326
|
quantity=quantity,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|