hccinfhir 0.1.7__py3-none-any.whl → 0.1.8__py3-none-any.whl
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/extractor_837.py +114 -58
- {hccinfhir-0.1.7.dist-info → hccinfhir-0.1.8.dist-info}/METADATA +1 -1
- {hccinfhir-0.1.7.dist-info → hccinfhir-0.1.8.dist-info}/RECORD +5 -5
- {hccinfhir-0.1.7.dist-info → hccinfhir-0.1.8.dist-info}/WHEEL +0 -0
- {hccinfhir-0.1.7.dist-info → hccinfhir-0.1.8.dist-info}/licenses/LICENSE +0 -0
hccinfhir/extractor_837.py
CHANGED
|
@@ -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,11 +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
|
-
claim_control_number = None
|
|
163
|
+
hierarchy = HierarchyContext()
|
|
164
|
+
claim = ClaimContext()
|
|
154
165
|
|
|
155
166
|
for i, segment in enumerate(segments):
|
|
156
167
|
if len(segment) < 2:
|
|
@@ -158,46 +169,89 @@ def parse_837_claim_to_sld(segments: List[List[str]], claim_type: str) -> List[S
|
|
|
158
169
|
|
|
159
170
|
seg_id = segment[0]
|
|
160
171
|
|
|
161
|
-
#
|
|
162
|
-
if seg_id == '
|
|
163
|
-
|
|
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()
|
|
164
195
|
|
|
196
|
+
# ===== NAME/IDENTIFICATION (NM1 segments) =====
|
|
165
197
|
elif seg_id == 'NM1' and len(segment) > 1:
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
in_rendering_provider_loop = False
|
|
169
|
-
elif segment[1] == '82' and len(segment) > 8 and segment[8] == 'XX': # Rendering Provider
|
|
170
|
-
current_data.performing_provider_npi = get_segment_value(segment, 9)
|
|
171
|
-
in_rendering_provider_loop = True
|
|
172
|
-
elif segment[1] == '85' and len(segment) > 8 and segment[8] == 'XX': # Billing Provider
|
|
173
|
-
current_data.billing_provider_npi = get_segment_value(segment, 9)
|
|
174
|
-
|
|
175
|
-
# Process Provider Specialty
|
|
176
|
-
elif seg_id == 'PRV' and len(segment) > 1 and segment[1] == 'PE' and in_rendering_provider_loop:
|
|
177
|
-
current_data.provider_specialty = get_segment_value(segment, 3)
|
|
198
|
+
qualifier = segment[1]
|
|
199
|
+
claim.last_nm1_qualifier = qualifier
|
|
178
200
|
|
|
179
|
-
|
|
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) =====
|
|
180
233
|
elif seg_id == 'CLM':
|
|
181
|
-
|
|
182
|
-
|
|
234
|
+
claim = ClaimContext()
|
|
235
|
+
claim.claim_id = segment[1] if len(segment) > 1 else None
|
|
183
236
|
|
|
184
237
|
# Parse facility and service type for institutional claims
|
|
185
238
|
if claim_type == "837I" and len(segment) > 5 and segment[5] and ':' in segment[5]:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
#
|
|
190
|
-
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':
|
|
191
244
|
# In 837I, there can be multiple HI segments in the claim
|
|
192
245
|
# Also, in 837I, diagnosis position does not matter
|
|
193
246
|
# We will use continuous numbering for diagnosis codes
|
|
194
247
|
# use the last dx_lookup position as the starting position, and update
|
|
195
248
|
hi_segment = parse_diagnosis_codes(segment)
|
|
249
|
+
# Re-index for multiple HI segments in same claim
|
|
196
250
|
hi_segment_realigned = {
|
|
197
|
-
str(int(pos) + len(
|
|
251
|
+
str(int(pos) + len(claim.dx_lookup)): code
|
|
198
252
|
for pos, code in hi_segment.items()
|
|
199
253
|
}
|
|
200
|
-
|
|
254
|
+
claim.dx_lookup.update(hi_segment_realigned)
|
|
201
255
|
|
|
202
256
|
# Process Service Lines
|
|
203
257
|
#
|
|
@@ -217,54 +271,56 @@ def parse_837_claim_to_sld(segments: List[List[str]], claim_type: str) -> List[S
|
|
|
217
271
|
# SV205 (Required) - Unit Count: Format 9999999.999 (whole numbers only - fractional quantities not recognized)
|
|
218
272
|
# NOTE: Diagnosis Code Pointer is not supported for SV2
|
|
219
273
|
#
|
|
274
|
+
# ===== SERVICE LINE (SV1/SV2 segments - 2400 loop) =====
|
|
220
275
|
elif seg_id in ['SV1', 'SV2']:
|
|
221
|
-
|
|
222
276
|
linked_diagnoses = []
|
|
223
277
|
|
|
224
278
|
if seg_id == 'SV1':
|
|
225
|
-
# SV1 Professional Service
|
|
279
|
+
# SV1 Professional Service
|
|
226
280
|
proc_info = get_segment_value(segment, 1, '').split(':')
|
|
227
281
|
procedure_code = proc_info[1] if len(proc_info) > 1 else None
|
|
228
282
|
modifiers = proc_info[2:] if len(proc_info) > 2 else []
|
|
229
283
|
quantity = parse_amount(get_segment_value(segment, 4))
|
|
230
284
|
place_of_service = get_segment_value(segment, 5)
|
|
231
|
-
|
|
285
|
+
|
|
286
|
+
# Get diagnosis pointers and resolve to actual codes
|
|
232
287
|
dx_pointers = get_segment_value(segment, 7, '')
|
|
233
288
|
linked_diagnoses = [
|
|
234
|
-
|
|
289
|
+
claim.dx_lookup[pointer]
|
|
235
290
|
for pointer in (dx_pointers.split(':') if dx_pointers else [])
|
|
236
|
-
if pointer in
|
|
291
|
+
if pointer in claim.dx_lookup
|
|
237
292
|
]
|
|
238
293
|
else:
|
|
239
|
-
# SV2 Institutional Service
|
|
240
|
-
# Revenue code in SV201
|
|
294
|
+
# SV2 Institutional Service
|
|
241
295
|
revenue_code = get_segment_value(segment, 1)
|
|
242
|
-
# Procedure code in SV202
|
|
243
296
|
proc_info = get_segment_value(segment, 2, '').split(':')
|
|
244
297
|
procedure_code = proc_info[1] if len(proc_info) > 1 else None
|
|
245
298
|
modifiers = proc_info[2:] if len(proc_info) > 2 else []
|
|
246
|
-
# Quantity in SV205
|
|
247
299
|
quantity = parse_amount(get_segment_value(segment, 5))
|
|
248
|
-
place_of_service = None
|
|
249
|
-
# linked diagnoses are not supported for SV2
|
|
250
|
-
|
|
300
|
+
place_of_service = None
|
|
251
301
|
|
|
252
|
-
# Get service line details
|
|
302
|
+
# Get service line details (NDC, dates) - lookback from current segment index
|
|
253
303
|
ndc, service_date = process_service_line(segments, i)
|
|
254
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
|
+
|
|
255
311
|
# Create service level data
|
|
256
312
|
service_data = ServiceLevelData(
|
|
257
|
-
claim_id=
|
|
313
|
+
claim_id=claim.claim_id,
|
|
258
314
|
procedure_code=procedure_code,
|
|
259
315
|
linked_diagnosis_codes=linked_diagnoses,
|
|
260
|
-
claim_diagnosis_codes=list(
|
|
261
|
-
claim_type=
|
|
262
|
-
provider_specialty=
|
|
263
|
-
performing_provider_npi=
|
|
264
|
-
billing_provider_npi=
|
|
265
|
-
patient_id=
|
|
266
|
-
facility_type=
|
|
267
|
-
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,
|
|
268
324
|
service_date=service_date,
|
|
269
325
|
place_of_service=place_of_service,
|
|
270
326
|
quantity=quantity,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
hccinfhir/__init__.py,sha256=G_5m6jm3_BK5NdcZWoi0NEKJEsE_LjAU1RaLaL9xNPU,1043
|
|
2
2
|
hccinfhir/datamodels.py,sha256=EHkuWMhmHBt8GfVP3lrxfSogu-qZQzeforFzp0Bn_bM,7714
|
|
3
3
|
hccinfhir/extractor.py,sha256=xL9c2VT-e2I7_c8N8j4Og42UEgVuCzyn9WFp3ntM5Ro,1822
|
|
4
|
-
hccinfhir/extractor_837.py,sha256=
|
|
4
|
+
hccinfhir/extractor_837.py,sha256=D60gUFtMk2S0NrJ0iq3ENo35yIwBmBQvF5TurJgRIa8,15327
|
|
5
5
|
hccinfhir/extractor_fhir.py,sha256=wUN3vTm1oTZ-KvfcDebnpQMxAC-7YlRKv12Wrv3p85A,8490
|
|
6
6
|
hccinfhir/filter.py,sha256=j_yD2g6RBXVUV9trKkWzsQ35x3fRvfKUPvEXKUefI64,2007
|
|
7
7
|
hccinfhir/hccinfhir.py,sha256=tgNWGYvsQWOlmcnP-3yH3KfXgZtQ3IdxYfGP9SNSJb0,9879
|
|
@@ -44,7 +44,7 @@ hccinfhir/sample_files/sample_eob_1.json,sha256=_NGSVR2ysFpx-DcTvyga6dFCzhQ8Vi9f
|
|
|
44
44
|
hccinfhir/sample_files/sample_eob_2.json,sha256=FcnJcx0ApOczxjJ_uxVLzCep9THfNf4xs9Yf7hxk8e4,1769
|
|
45
45
|
hccinfhir/sample_files/sample_eob_200.ndjson,sha256=CxpjeQ1DCMUzZILaM68UEhfxO0p45YGhDDoCZeq8PxU,1917986
|
|
46
46
|
hccinfhir/sample_files/sample_eob_3.json,sha256=4BW4wOMBEEU9RDfJR15rBEvk0KNHyuMEh3e055y87Hc,2306
|
|
47
|
-
hccinfhir-0.1.
|
|
48
|
-
hccinfhir-0.1.
|
|
49
|
-
hccinfhir-0.1.
|
|
50
|
-
hccinfhir-0.1.
|
|
47
|
+
hccinfhir-0.1.8.dist-info/METADATA,sha256=InnTNmjxePxSY-BJR740w42w5CExg8cRJR9LNXVaAXA,24819
|
|
48
|
+
hccinfhir-0.1.8.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
|
49
|
+
hccinfhir-0.1.8.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
50
|
+
hccinfhir-0.1.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|