hccinfhir 0.1.6__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.
@@ -7,17 +7,23 @@ CLAIM_TYPES = {
7
7
  "005010X223A2": "837I" # Institutional
8
8
  }
9
9
 
10
- class ClaimData(BaseModel):
11
- """Container for claim-level data"""
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
- provider_specialty: Optional[str] = None
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
- claim_type: str
20
- dx_lookup: Dict[str, str] = {}
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
- current_data = ClaimData(claim_type=claim_type)
152
- in_claim_loop = False
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
- # Process NM1 segments (Provider and Patient info)
163
- if seg_id == 'ST':
164
- claim_control_number = segment[2] if len(segment) > 2 else None
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
- if segment[1] == 'IL': # Subscriber/Patient
168
- current_data.patient_id = get_segment_value(segment, 9)
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
- # Process Claim Information
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
- in_claim_loop = True
184
- in_rendering_provider_loop = False
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
- current_data.facility_type = segment[5][0] if segment[5] else None
190
- current_data.service_type = segment[5][1] if len(segment[5]) > 1 else None
191
-
192
- # Process Diagnosis Codes
193
- elif seg_id == 'HI' and in_claim_loop:
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(current_data.dx_lookup)): code
251
+ str(int(pos) + len(claim.dx_lookup)): code
201
252
  for pos, code in hi_segment.items()
202
253
  }
203
- current_data.dx_lookup.update(hi_segment_realigned)
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
- elif seg_id in ['SV1', 'SV2'] and in_claim_loop:
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: SV101=procedure, SV104=quantity, SV106=place_of_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
- # Get diagnosis pointers and linked diagnoses
285
+
286
+ # Get diagnosis pointers and resolve to actual codes
235
287
  dx_pointers = get_segment_value(segment, 7, '')
236
288
  linked_diagnoses = [
237
- current_data.dx_lookup[pointer]
289
+ claim.dx_lookup[pointer]
238
290
  for pointer in (dx_pointers.split(':') if dx_pointers else [])
239
- if pointer in current_data.dx_lookup
291
+ if pointer in claim.dx_lookup
240
292
  ]
241
293
  else:
242
- # SV2 Institutional Service: SV201=revenue, SV202=procedure, SV205=quantity
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 # Not applicable for institutional
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=current_data.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(current_data.dx_lookup.values()), # this is used for risk adjustment
264
- claim_type=current_data.claim_type,
265
- provider_specialty=current_data.provider_specialty,
266
- performing_provider_npi=current_data.performing_provider_npi,
267
- billing_provider_npi=current_data.billing_provider_npi,
268
- patient_id=current_data.patient_id,
269
- facility_type=current_data.facility_type,
270
- service_type=current_data.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,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: hccinfhir
3
- Version: 0.1.6
3
+ Version: 0.1.8
4
4
  Summary: HCC Algorithm for FHIR Resources
5
5
  Project-URL: Homepage, https://github.com/mimilabs/hccinfhir
6
6
  Project-URL: Issues, https://github.com/mimilabs/hccinfhir/issues
@@ -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=Jhxb5glRA8Yi8lbRwdltggXCvr_84nlsZEMRIgxyy4A,13169
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.6.dist-info/METADATA,sha256=biMD3n0KhW7JZ7phDj40yJYxcAryIWdA6S08ffvCMak,24819
48
- hccinfhir-0.1.6.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
49
- hccinfhir-0.1.6.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
50
- hccinfhir-0.1.6.dist-info/RECORD,,
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,,