hccinfhir 0.1.7__py3-none-any.whl → 0.1.9__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/datamodels.py CHANGED
@@ -145,4 +145,66 @@ class RAFResult(BaseModel):
145
145
  diagnosis_codes: List[str] = Field(default_factory=list, description="Input diagnosis codes")
146
146
  service_level_data: Optional[List[ServiceLevelData]] = Field(default=None, description="Processed service records")
147
147
 
148
- model_config = {"extra": "forbid", "validate_assignment": True}
148
+ model_config = {"extra": "forbid", "validate_assignment": True}
149
+
150
+ class EnrollmentData(BaseModel):
151
+ """
152
+ Enrollment and demographic data extracted from 834 transactions.
153
+
154
+ Focus: Extract data needed for risk adjustment and Medicaid coverage tracking.
155
+
156
+ Attributes:
157
+ member_id: Unique identifier for the member
158
+ mbi: Medicare Beneficiary Identifier
159
+ medicaid_id: Medicaid/Medi-Cal ID number
160
+ dob: Date of birth (YYYY-MM-DD)
161
+ age: Calculated age
162
+ sex: Member sex (M/F)
163
+ maintenance_type: 001=Change, 021=Add, 024=Cancel, 025=Reinstate
164
+ coverage_start_date: Coverage effective date
165
+ coverage_end_date: Coverage termination date (critical for Medicaid loss detection)
166
+ has_medicare: Member has Medicare coverage
167
+ has_medicaid: Member has Medicaid coverage
168
+ dual_elgbl_cd: Dual eligibility status code ('00','01'-'08')
169
+ is_full_benefit_dual: Full Benefit Dual (uses CFA_/CFD_ prefix)
170
+ is_partial_benefit_dual: Partial Benefit Dual (uses CPA_/CPD_ prefix)
171
+ medicare_status_code: QMB, SLMB, QI, QDWI, etc.
172
+ medi_cal_aid_code: California Medi-Cal aid code
173
+ orec: Original Reason for Entitlement Code
174
+ crec: Current Reason for Entitlement Code
175
+ snp: Special Needs Plan enrollment
176
+ low_income: Low Income Subsidy (Part D)
177
+ lti: Long-Term Institutionalized
178
+ new_enrollee: New enrollee status (<= 3 months)
179
+ """
180
+ # Identifiers
181
+ member_id: Optional[str] = None
182
+ mbi: Optional[str] = None
183
+ medicaid_id: Optional[str] = None
184
+
185
+ # Demographics
186
+ dob: Optional[str] = None
187
+ age: Optional[int] = None
188
+ sex: Optional[str] = None
189
+
190
+ # Coverage tracking
191
+ maintenance_type: Optional[str] = None
192
+ coverage_start_date: Optional[str] = None
193
+ coverage_end_date: Optional[str] = None
194
+
195
+ # Medicaid/Medicare Status
196
+ has_medicare: bool = False
197
+ has_medicaid: bool = False
198
+ dual_elgbl_cd: Optional[str] = None
199
+ is_full_benefit_dual: bool = False
200
+ is_partial_benefit_dual: bool = False
201
+ medicare_status_code: Optional[str] = None
202
+ medi_cal_aid_code: Optional[str] = None
203
+
204
+ # Risk Adjustment Fields
205
+ orec: Optional[str] = None
206
+ crec: Optional[str] = None
207
+ snp: bool = False
208
+ low_income: bool = False
209
+ lti: bool = False
210
+ new_enrollee: bool = False
@@ -0,0 +1,530 @@
1
+ from typing import List, Optional, Dict
2
+ from pydantic import BaseModel
3
+ from datetime import datetime, date
4
+ from hccinfhir.datamodels import Demographics, EnrollmentData
5
+
6
+ TRANSACTION_TYPES = {
7
+ "005010X220A1": "834", # Benefit Enrollment and Maintenance
8
+ }
9
+
10
+ # California Medi-Cal Aid Codes mapping to dual eligibility status
11
+ MEDI_CAL_AID_CODES = {
12
+ # Full Benefit Dual (QMB Plus, SLMB Plus)
13
+ '4N': '02', # QMB Plus - Aged
14
+ '4P': '02', # QMB Plus - Disabled
15
+ '5B': '04', # SLMB Plus - Aged
16
+ '5D': '04', # SLMB Plus - Disabled
17
+
18
+ # Partial Benefit Dual (QMB Only, SLMB Only, QI)
19
+ '4M': '01', # QMB Only - Aged
20
+ '4O': '01', # QMB Only - Disabled
21
+ '5A': '03', # SLMB Only - Aged
22
+ '5C': '03', # SLMB Only - Disabled
23
+ '5E': '06', # QI - Aged
24
+ '5F': '06', # QI - Disabled
25
+ }
26
+
27
+ class MemberContext(BaseModel):
28
+ """Tracks member-level data across segments within 834 transaction"""
29
+ # Identifiers
30
+ member_id: Optional[str] = None
31
+ mbi: Optional[str] = None # Medicare Beneficiary Identifier
32
+ medicaid_id: Optional[str] = None
33
+
34
+ # Demographics
35
+ dob: Optional[str] = None
36
+ sex: Optional[str] = None
37
+
38
+ # Coverage Status
39
+ maintenance_type: Optional[str] = None # 001=Change, 021=Add, 024=Cancel, 025=Reinstate
40
+ coverage_start_date: Optional[str] = None
41
+ coverage_end_date: Optional[str] = None
42
+
43
+ # Medicare/Medicaid Status
44
+ has_medicare: bool = False
45
+ has_medicaid: bool = False
46
+ medicare_status_code: Optional[str] = None # QMB, SLMB, QI, etc.
47
+ medi_cal_aid_code: Optional[str] = None
48
+ dual_elgbl_cd: Optional[str] = None
49
+
50
+ # Risk Adjustment Fields
51
+ orec: Optional[str] = None
52
+ crec: Optional[str] = None
53
+ snp: bool = False
54
+ low_income: bool = False
55
+ lti: bool = False
56
+
57
+ # Helper methods for EnrollmentData - added as standalone functions
58
+ def enrollment_to_demographics(enrollment: EnrollmentData) -> Demographics:
59
+ """Convert EnrollmentData to Demographics model for risk calculation"""
60
+ return Demographics(
61
+ age=enrollment.age or 0,
62
+ sex=enrollment.sex or 'M',
63
+ dual_elgbl_cd=enrollment.dual_elgbl_cd,
64
+ orec=enrollment.orec or '',
65
+ crec=enrollment.crec or '',
66
+ new_enrollee=enrollment.new_enrollee,
67
+ snp=enrollment.snp,
68
+ low_income=enrollment.low_income,
69
+ lti=enrollment.lti
70
+ )
71
+
72
+ def is_losing_medicaid(enrollment: EnrollmentData, within_days: int = 90) -> bool:
73
+ """Check if member will lose Medicaid within specified days
74
+
75
+ Args:
76
+ enrollment: EnrollmentData object
77
+ within_days: Number of days to look ahead (default 90)
78
+
79
+ Returns:
80
+ True if Medicaid coverage ends within specified days
81
+ """
82
+ if not enrollment.coverage_end_date or not enrollment.has_medicaid:
83
+ return False
84
+
85
+ try:
86
+ end_date = datetime.strptime(enrollment.coverage_end_date, "%Y-%m-%d").date()
87
+ today = date.today()
88
+ days_until_end = (end_date - today).days
89
+
90
+ return 0 <= days_until_end <= within_days
91
+ except (ValueError, AttributeError):
92
+ return False
93
+
94
+ def is_medicaid_terminated(enrollment: EnrollmentData) -> bool:
95
+ """Check if Medicaid coverage is being terminated (maintenance type 024)"""
96
+ return enrollment.maintenance_type == '024'
97
+
98
+ def medicaid_status_summary(enrollment: EnrollmentData) -> Dict[str, any]:
99
+ """Get summary of Medicaid coverage status for monitoring
100
+
101
+ Args:
102
+ enrollment: EnrollmentData object
103
+
104
+ Returns:
105
+ Dictionary with Medicaid status, dual eligibility, and loss indicators
106
+ """
107
+ return {
108
+ 'member_id': enrollment.member_id,
109
+ 'has_medicaid': enrollment.has_medicaid,
110
+ 'has_medicare': enrollment.has_medicare,
111
+ 'dual_status': enrollment.dual_elgbl_cd,
112
+ 'is_full_benefit_dual': enrollment.is_full_benefit_dual,
113
+ 'is_partial_benefit_dual': enrollment.is_partial_benefit_dual,
114
+ 'coverage_end_date': enrollment.coverage_end_date,
115
+ 'is_termination': is_medicaid_terminated(enrollment),
116
+ 'losing_medicaid_30d': is_losing_medicaid(enrollment, 30),
117
+ 'losing_medicaid_60d': is_losing_medicaid(enrollment, 60),
118
+ 'losing_medicaid_90d': is_losing_medicaid(enrollment, 90)
119
+ }
120
+
121
+ def parse_date(date_str: str) -> Optional[str]:
122
+ """Convert 8-digit date string to ISO format YYYY-MM-DD"""
123
+ if not isinstance(date_str, str) or len(date_str) != 8:
124
+ return None
125
+ try:
126
+ year, month, day = int(date_str[:4]), int(date_str[4:6]), int(date_str[6:8])
127
+ if not (1900 <= year <= 2100 and 1 <= month <= 12 and 1 <= day <= 31):
128
+ return None
129
+ return f"{year:04d}-{month:02d}-{day:02d}"
130
+ except (ValueError, IndexError):
131
+ return None
132
+
133
+ def calculate_age(dob: str, reference_date: Optional[str] = None) -> Optional[int]:
134
+ """Calculate age from DOB in YYYY-MM-DD format"""
135
+ if not dob:
136
+ return None
137
+ try:
138
+ birth_date = datetime.strptime(dob, "%Y-%m-%d").date()
139
+ if reference_date:
140
+ ref_date = datetime.strptime(reference_date, "%Y-%m-%d").date()
141
+ else:
142
+ ref_date = date.today()
143
+
144
+ age = ref_date.year - birth_date.year
145
+ if (ref_date.month, ref_date.day) < (birth_date.month, birth_date.day):
146
+ age -= 1
147
+ return age
148
+ except (ValueError, AttributeError):
149
+ return None
150
+
151
+ def get_segment_value(segment: List[str], index: int, default: Optional[str] = None) -> Optional[str]:
152
+ """Safely get value from segment at given index"""
153
+ try:
154
+ if len(segment) > index and segment[index]:
155
+ return segment[index]
156
+ except (IndexError, TypeError):
157
+ pass
158
+ return default
159
+
160
+ def map_medicare_status_to_dual_code(status: Optional[str]) -> Optional[str]:
161
+ """Map Medicare status codes to dual eligibility codes
162
+
163
+ California Medi-Cal uses these status codes:
164
+ - QMB = Qualified Medicare Beneficiary
165
+ - QMBPLUS = QMB Plus (Full Benefit)
166
+ - SLMB = Specified Low-Income Medicare Beneficiary
167
+ - SLMBPLUS = SLMB Plus (Full Benefit)
168
+ - QI = Qualifying Individual
169
+ - QDWI = Qualified Disabled Working Individual
170
+ """
171
+ if not status:
172
+ return None
173
+
174
+ status_upper = status.upper().replace(' ', '').replace('-', '')
175
+
176
+ mapping = {
177
+ 'QMB': '01', # QMB Only (Partial)
178
+ 'QMBONLY': '01',
179
+ 'QMBPLUS': '02', # QMB Plus (Full Benefit)
180
+ 'QMB+': '02',
181
+ 'SLMB': '03', # SLMB Only (Partial)
182
+ 'SLMBONLY': '03',
183
+ 'SLMBPLUS': '04', # SLMB Plus (Full Benefit)
184
+ 'SLMB+': '04',
185
+ 'QDWI': '05',
186
+ 'QI': '06',
187
+ 'QI1': '06',
188
+ 'FBDE': '08', # Full Benefit Dual Eligible (Other)
189
+ 'OTHERFULL': '08',
190
+ }
191
+
192
+ return mapping.get(status_upper)
193
+
194
+ def map_aid_code_to_dual_status(aid_code: Optional[str]) -> Optional[str]:
195
+ """Map California Medi-Cal aid code to dual eligibility status"""
196
+ if not aid_code:
197
+ return None
198
+ return MEDI_CAL_AID_CODES.get(aid_code)
199
+
200
+ def determine_dual_status(member: MemberContext) -> str:
201
+ """Intelligently derive dual eligibility code from available data
202
+
203
+ Priority order:
204
+ 1. Explicit dual_elgbl_cd from REF segment
205
+ 2. California Medi-Cal aid code mapping
206
+ 3. Medicare status code (QMB, SLMB, etc.)
207
+ 4. Presence of both Medicare and Medicaid coverage
208
+ 5. Default to non-dual ('00')
209
+ """
210
+ # Priority 1: Explicit dual_elgbl_cd
211
+ if member.dual_elgbl_cd and member.dual_elgbl_cd in ['01','02','03','04','05','06','08']:
212
+ return member.dual_elgbl_cd
213
+
214
+ # Priority 2: California aid code mapping
215
+ if member.medi_cal_aid_code:
216
+ dual_code = map_aid_code_to_dual_status(member.medi_cal_aid_code)
217
+ if dual_code:
218
+ return dual_code
219
+
220
+ # Priority 3: Medicare status code
221
+ if member.medicare_status_code:
222
+ dual_code = map_medicare_status_to_dual_code(member.medicare_status_code)
223
+ if dual_code:
224
+ return dual_code
225
+
226
+ # Priority 4: Both Medicare and Medicaid coverage present
227
+ if member.has_medicare and (member.has_medicaid or member.medicaid_id):
228
+ # Conservative: assign '08' (Other Full Dual) to ensure dual coefficients
229
+ return '08'
230
+
231
+ # Default: Non-dual
232
+ return '00'
233
+
234
+ def classify_dual_benefit_level(dual_code: str) -> tuple[bool, bool]:
235
+ """Classify as Full Benefit Dual (FBD) or Partial Benefit Dual (PBD)
236
+
237
+ Full Benefit Dual codes: 02, 04, 08
238
+ - Uses CFA_ (Community, Full Benefit Dual, Aged) prefix
239
+ - Uses CFD_ (Community, Full Benefit Dual, Disabled) prefix
240
+
241
+ Partial Benefit Dual codes: 01, 03, 05, 06
242
+ - Uses CPA_ (Community, Partial Benefit Dual, Aged) prefix
243
+ - Uses CPD_ (Community, Partial Benefit Dual, Disabled) prefix
244
+ """
245
+ full_benefit_codes = {'02', '04', '08'}
246
+ partial_benefit_codes = {'01', '03', '05', '06'}
247
+
248
+ is_fbd = dual_code in full_benefit_codes
249
+ is_pbd = dual_code in partial_benefit_codes
250
+
251
+ return is_fbd, is_pbd
252
+
253
+ def is_new_enrollee(coverage_start_date: Optional[str], reference_date: Optional[str] = None) -> bool:
254
+ """Determine if member is new enrollee (<= 3 months since coverage start)"""
255
+ if not coverage_start_date:
256
+ return False
257
+
258
+ try:
259
+ start_date = datetime.strptime(coverage_start_date, "%Y-%m-%d").date()
260
+ if reference_date:
261
+ ref_date = datetime.strptime(reference_date, "%Y-%m-%d").date()
262
+ else:
263
+ ref_date = date.today()
264
+
265
+ # Calculate months difference
266
+ months_diff = (ref_date.year - start_date.year) * 12 + (ref_date.month - start_date.month)
267
+
268
+ return months_diff <= 3
269
+ except (ValueError, AttributeError):
270
+ return False
271
+
272
+ def parse_834_enrollment(segments: List[List[str]]) -> List[EnrollmentData]:
273
+ """Extract enrollment and demographic data from 834 transaction
274
+
275
+ California DHCS Medi-Cal 834 Structure:
276
+ Loop 2000 - Member Level
277
+ INS - Member Level Detail (subscriber/dependent, maintenance type)
278
+ REF - Member Identifiers (0F, 1L, F6, 6P, ZZ, AB, ABB)
279
+ DTP - Date Time Periods (303, 348, 349, 338)
280
+ NM1 - Member Name (IL qualifier)
281
+ DMG - Demographics (DOB, Sex) ***CRITICAL***
282
+ HD - Health Coverage ***CRITICAL FOR DUAL STATUS***
283
+ """
284
+ enrollments = []
285
+ member = MemberContext()
286
+
287
+ for i, segment in enumerate(segments):
288
+ if len(segment) < 2:
289
+ continue
290
+
291
+ seg_id = segment[0]
292
+
293
+ # ===== INS - Member Level Detail (Start of 2000 loop) =====
294
+ if seg_id == 'INS' and len(segment) >= 3:
295
+ # Save previous member before starting new one
296
+ if member.member_id or member.has_medicare or member.has_medicaid:
297
+ enrollments.append(create_enrollment_data(member))
298
+
299
+ # Start new member
300
+ member = MemberContext()
301
+
302
+ # INS03 - Maintenance Type Code
303
+ member.maintenance_type = get_segment_value(segment, 3)
304
+ # 001=Change, 021=Addition, 024=Cancellation/Term, 025=Reinstatement
305
+
306
+ # ===== REF - Reference Identifiers =====
307
+ elif seg_id == 'REF' and len(segment) >= 3:
308
+ qualifier = segment[1]
309
+ value = segment[2] if len(segment) > 2 else None
310
+
311
+ if not value:
312
+ continue
313
+
314
+ # Standard REF qualifiers
315
+ if qualifier == '0F': # Subscriber Number
316
+ if not member.member_id:
317
+ member.member_id = value
318
+ elif qualifier == 'ZZ': # Mutually Defined (often member ID or MBI)
319
+ if not member.member_id:
320
+ member.member_id = value
321
+
322
+ # Medicare Identifiers
323
+ elif qualifier == '6P': # Medicare MBI (new identifier)
324
+ member.mbi = value
325
+ member.has_medicare = True
326
+ elif qualifier == 'F6': # Medicare HICN (legacy) or MBI
327
+ if not member.mbi:
328
+ member.mbi = value
329
+ member.has_medicare = True
330
+
331
+ # Medicaid Identifiers
332
+ elif qualifier == '1D': # Medicaid/Recipient ID
333
+ member.medicaid_id = value
334
+ member.has_medicaid = True
335
+ elif qualifier == '23': # Medicaid Recipient ID (alternative)
336
+ if not member.medicaid_id:
337
+ member.medicaid_id = value
338
+ member.has_medicaid = True
339
+
340
+ # California Medi-Cal Specific
341
+ elif qualifier == 'ABB': # Medicare Status Code (QMB, SLMB, QI, etc.)
342
+ member.medicare_status_code = value
343
+ elif qualifier == 'AB': # Aid Code (California specific)
344
+ member.medi_cal_aid_code = value
345
+
346
+ # Custom dual eligibility indicators
347
+ elif qualifier == 'F5': # Dual Eligibility Code (custom)
348
+ if value in ['01','02','03','04','05','06','08']:
349
+ member.dual_elgbl_cd = value
350
+ elif qualifier == 'DX': # OREC (custom)
351
+ if value in ['0','1','2','3']:
352
+ member.orec = value
353
+ elif qualifier == 'DY': # CREC (custom)
354
+ if value in ['0','1','2','3']:
355
+ member.crec = value
356
+ elif qualifier == 'EJ': # Low Income Subsidy indicator
357
+ member.low_income = (value.upper() in ['Y', 'YES', '1', 'TRUE'])
358
+
359
+ # ===== NM1 - Member Name =====
360
+ elif seg_id == 'NM1' and len(segment) >= 4:
361
+ qualifier = segment[1]
362
+
363
+ if qualifier == 'IL': # Insured or Subscriber
364
+ # NM109 = Identification Code (Member ID)
365
+ if len(segment) > 9:
366
+ id_value = get_segment_value(segment, 9)
367
+ if id_value and not member.member_id:
368
+ member.member_id = id_value
369
+
370
+ # ===== DMG - Demographics ***CRITICAL SEGMENT*** =====
371
+ elif seg_id == 'DMG' and len(segment) >= 3:
372
+ # DMG02 = Date of Birth
373
+ dob_str = get_segment_value(segment, 2)
374
+ if dob_str:
375
+ member.dob = parse_date(dob_str)
376
+
377
+ # DMG03 = Gender Code
378
+ sex = get_segment_value(segment, 3)
379
+ if sex in ['M', 'F', '1', '2']:
380
+ member.sex = 'M' if sex in ['M', '1'] else 'F'
381
+
382
+ # ===== DTP - Date Time Periods =====
383
+ elif seg_id == 'DTP' and len(segment) >= 4:
384
+ date_qualifier = segment[1]
385
+ date_format = segment[2]
386
+ date_value = segment[3] if len(segment) > 3 else None
387
+
388
+ if not date_value or not date_format.endswith('D8'):
389
+ continue
390
+
391
+ parsed_date = parse_date(date_value[:8] if len(date_value) >= 8 else date_value)
392
+
393
+ if not parsed_date:
394
+ continue
395
+
396
+ # Date qualifiers
397
+ if date_qualifier == '348': # Benefit Begin Date
398
+ member.coverage_start_date = parsed_date
399
+ elif date_qualifier == '349': # Benefit End Date
400
+ member.coverage_end_date = parsed_date
401
+ elif date_qualifier == '338': # Medicare Part A/B Effective Date
402
+ if not member.coverage_start_date:
403
+ member.coverage_start_date = parsed_date
404
+ member.has_medicare = True
405
+
406
+ # ===== HD - Health Coverage ***CRITICAL FOR DUAL STATUS*** =====
407
+ elif seg_id == 'HD' and len(segment) >= 4:
408
+ # HD03 = Insurance Line Code
409
+ insurance_line = get_segment_value(segment, 3, '').upper()
410
+
411
+ # HD04 = Plan Coverage Description
412
+ plan_desc = get_segment_value(segment, 4, '').upper()
413
+
414
+ # HD06 = Insurance Type Code
415
+ insurance_type = get_segment_value(segment, 6, '').upper()
416
+
417
+ # Combine all fields for pattern matching
418
+ combined = f"{insurance_line} {plan_desc} {insurance_type}"
419
+
420
+ # Detect Medicare coverage
421
+ if any(keyword in combined for keyword in [
422
+ 'MEDICARE', 'MA', 'PART A', 'PART B', 'PART C', 'PART D',
423
+ 'MEDICARE ADVANTAGE', 'MA-PD'
424
+ ]):
425
+ member.has_medicare = True
426
+
427
+ # Detect Medicaid/Medi-Cal coverage
428
+ if any(keyword in combined for keyword in [
429
+ 'MEDICAID', 'MEDI-CAL', 'MEDI CAL', 'MEDIC-AID'
430
+ ]):
431
+ member.has_medicaid = True
432
+
433
+ # Detect SNP (Special Needs Plan)
434
+ if any(keyword in combined for keyword in [
435
+ 'SNP', 'SPECIAL NEEDS', 'D-SNP', 'DSNP', 'DUAL ELIGIBLE SNP'
436
+ ]):
437
+ member.snp = True
438
+ # If it's a D-SNP, they are definitely dual eligible
439
+ if 'D-SNP' in combined or 'DSNP' in combined or 'DUAL' in combined:
440
+ member.has_medicare = True
441
+ member.has_medicaid = True
442
+
443
+ # Don't forget last member
444
+ if member.member_id or member.has_medicare or member.has_medicaid:
445
+ enrollments.append(create_enrollment_data(member))
446
+
447
+ return enrollments
448
+
449
+ def create_enrollment_data(member: MemberContext) -> EnrollmentData:
450
+ """Convert MemberContext to EnrollmentData with risk adjustment fields"""
451
+
452
+ # Calculate age
453
+ age = calculate_age(member.dob) if member.dob else None
454
+
455
+ # Determine dual eligibility status
456
+ dual_code = determine_dual_status(member)
457
+
458
+ # Classify FBD vs PBD
459
+ is_fbd, is_pbd = classify_dual_benefit_level(dual_code)
460
+
461
+ # Determine new enrollee status
462
+ new_enrollee = is_new_enrollee(member.coverage_start_date)
463
+
464
+ return EnrollmentData(
465
+ # Identifiers
466
+ member_id=member.member_id,
467
+ mbi=member.mbi,
468
+ medicaid_id=member.medicaid_id,
469
+
470
+ # Demographics
471
+ dob=member.dob,
472
+ age=age,
473
+ sex=member.sex,
474
+
475
+ # Coverage tracking
476
+ maintenance_type=member.maintenance_type,
477
+ coverage_start_date=member.coverage_start_date,
478
+ coverage_end_date=member.coverage_end_date,
479
+
480
+ # Dual Eligibility
481
+ has_medicare=member.has_medicare,
482
+ has_medicaid=member.has_medicaid,
483
+ dual_elgbl_cd=dual_code,
484
+ is_full_benefit_dual=is_fbd,
485
+ is_partial_benefit_dual=is_pbd,
486
+ medicare_status_code=member.medicare_status_code,
487
+ medi_cal_aid_code=member.medi_cal_aid_code,
488
+
489
+ # Risk Adjustment
490
+ orec=member.orec,
491
+ crec=member.crec,
492
+ snp=member.snp,
493
+ low_income=member.low_income,
494
+ lti=member.lti,
495
+ new_enrollee=new_enrollee
496
+ )
497
+
498
+ def extract_enrollment_834(content: str) -> List[EnrollmentData]:
499
+ """Main entry point for 834 parsing
500
+
501
+ Args:
502
+ content: Raw X12 834 transaction file content
503
+
504
+ Returns:
505
+ List of EnrollmentData objects with demographic and dual eligibility info
506
+
507
+ Raises:
508
+ ValueError: If content is empty or invalid 834 format
509
+ """
510
+ if not content:
511
+ raise ValueError("Input X12 834 data cannot be empty")
512
+
513
+ # Split content into segments
514
+ segments = [seg.strip().split('*')
515
+ for seg in content.split('~') if seg.strip()]
516
+
517
+ if not segments:
518
+ raise ValueError("No valid segments found in 834 data")
519
+
520
+ # Validate transaction type from GS segment
521
+ transaction_type = None
522
+ for segment in segments:
523
+ if segment[0] == 'GS' and len(segment) > 8:
524
+ transaction_type = TRANSACTION_TYPES.get(segment[8])
525
+ break
526
+
527
+ if not transaction_type:
528
+ raise ValueError("Invalid or unsupported 834 format (missing GS segment or wrong version)")
529
+
530
+ return parse_834_enrollment(segments)
@@ -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,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
- current_data = ClaimData(claim_type=claim_type)
152
- in_rendering_provider_loop = False
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
- # Process NM1 segments (Provider and Patient info)
162
- if seg_id == 'ST':
163
- 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()
164
195
 
196
+ # ===== NAME/IDENTIFICATION (NM1 segments) =====
165
197
  elif seg_id == 'NM1' and len(segment) > 1:
166
- if segment[1] == 'IL': # Subscriber/Patient
167
- current_data.patient_id = get_segment_value(segment, 9)
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
- # 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) =====
180
233
  elif seg_id == 'CLM':
181
- in_rendering_provider_loop = False
182
- 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
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
- current_data.facility_type = segment[5][0] if segment[5] else None
187
- current_data.service_type = segment[5][1] if len(segment[5]) > 1 else None
188
-
189
- # Process Diagnosis Codes
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(current_data.dx_lookup)): code
251
+ str(int(pos) + len(claim.dx_lookup)): code
198
252
  for pos, code in hi_segment.items()
199
253
  }
200
- current_data.dx_lookup.update(hi_segment_realigned)
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: SV101=procedure, SV104=quantity, SV106=place_of_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
- # Get diagnosis pointers and linked diagnoses
285
+
286
+ # Get diagnosis pointers and resolve to actual codes
232
287
  dx_pointers = get_segment_value(segment, 7, '')
233
288
  linked_diagnoses = [
234
- current_data.dx_lookup[pointer]
289
+ claim.dx_lookup[pointer]
235
290
  for pointer in (dx_pointers.split(':') if dx_pointers else [])
236
- if pointer in current_data.dx_lookup
291
+ if pointer in claim.dx_lookup
237
292
  ]
238
293
  else:
239
- # SV2 Institutional Service: SV201=revenue, SV202=procedure, SV205=quantity
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 # Not applicable for institutional
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=current_data.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(current_data.dx_lookup.values()), # this is used for risk adjustment
261
- claim_type=current_data.claim_type,
262
- provider_specialty=current_data.provider_specialty,
263
- performing_provider_npi=current_data.performing_provider_npi,
264
- billing_provider_npi=current_data.billing_provider_npi,
265
- patient_id=current_data.patient_id,
266
- facility_type=current_data.facility_type,
267
- 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,
268
324
  service_date=service_date,
269
325
  place_of_service=place_of_service,
270
326
  quantity=quantity,
hccinfhir/hccinfhir.py CHANGED
@@ -189,9 +189,7 @@ class HCCInFHIR:
189
189
  """
190
190
  if not isinstance(diagnosis_codes, list):
191
191
  raise ValueError("diagnosis_codes must be a list")
192
- if not diagnosis_codes:
193
- raise ValueError("diagnosis_codes list cannot be empty")
194
-
192
+
195
193
  demographics = self._ensure_demographics(demographics)
196
194
  raf_result = self._calculate_raf_from_demographics_and_dx_codes(
197
195
  diagnosis_codes, demographics, prefix_override, maci, norm_factor, frailty_score
@@ -124,9 +124,9 @@ def calculate_raf(diagnosis_codes: List[str],
124
124
  prefix_override=prefix_override)
125
125
 
126
126
  # Calculate risk scores
127
- print(f"Coefficients: {coefficients}")
127
+ #print(f"Coefficients: {coefficients}")
128
128
  risk_score = sum(coefficients.values())
129
- print(f"Risk Score: {risk_score}")
129
+ #print(f"Risk Score: {risk_score}")
130
130
  risk_score_demographics = sum(coefficients_demographics.values())
131
131
  risk_score_chronic_only = sum(coefficients_chronic_only.values()) - risk_score_demographics
132
132
  risk_score_hcc = risk_score - risk_score_demographics
@@ -0,0 +1 @@
1
+ ISA*00* *00* *ZZ*DHCS *ZZ*HEALTHPLAN *250108*1430*^*00501*000000001*0*P*:~GS*BE*DHCS*HEALTHPLAN*20250108*1430*1*X*005010X220A1~ST*834*0001*005010X220A1~BGN*00*12345*20250108*1430****2~REF*38*MEDI-CAL~DTP*007*D8*20250108~N1*P5*CALIFORNIA DHCS*FI*953654321~N1*IN*HEALTH PLAN NAME*FI*987654321~INS*Y*18*021***FT***AC~REF*0F*MBR001~REF*6P*1A23BC456D7~REF*1D*MC123456789~REF*ABB*QMBPLUS~NM1*IL*1*GARCIA*MARIA*R***MI*MBR001~PER*IP**HP*5555551234~N3*123 MAIN ST~N4*LOS ANGELES*CA*90001~DMG*D8*19550315*F~HD*021**HLT*MEDICARE ADVANTAGE D-SNP~DTP*348*D8*20240101~HD*021**HLT*MEDI-CAL~DTP*348*D8*20240101~INS*Y*18*001***FT***AC~REF*0F*MBR002~REF*6P*2B34CD567E8~REF*1D*MC987654321~REF*AB*4M~NM1*IL*1*JOHNSON*ROBERT*L***MI*MBR002~N3*456 OAK AVE~N4*SAN DIEGO*CA*92101~DMG*D8*19600822*M~HD*001**HLT*MEDICARE PART C~DTP*348*D8*20230515~HD*001**HLT*MEDI-CAL~DTP*348*D8*20240101~INS*Y*18*024***FT***TE~REF*0F*MBR003~REF*6P*3C45DE678F9~REF*1D*MC555666777~REF*ABB*SLMBPLUS~NM1*IL*1*RODRIGUEZ*CARMEN****MI*MBR003~N3*789 PINE RD~N4*SACRAMENTO*CA*95814~DMG*D8*19481203*F~HD*024**HLT*MEDICARE PART A~DTP*348*D8*20220101~DTP*349*D8*20250228~HD*024**HLT*MEDI-CAL~DTP*348*D8*20220101~DTP*349*D8*20250228~INS*Y*18*021***FT***AC~REF*0F*MBR004~REF*1D*MC111222333~NM1*IL*1*NGUYEN*THANH*H***MI*MBR004~N3*321 ELM ST~N4*SAN FRANCISCO*CA*94102~DMG*D8*19701015*M~HD*021**HLT*MEDI-CAL~DTP*348*D8*20250101~INS*Y*18*021***FT***AC~REF*0F*MBR005~REF*6P*5E67FG890H1~NM1*IL*1*SMITH*JOHN*A***MI*MBR005~N3*654 MAPLE AVE~N4*FRESNO*CA*93650~DMG*D8*19580710*M~HD*021**HLT*MEDICARE PART C~DTP*348*D8*20250901~SE*68*0001~GE*1*1~IEA*1*000000001~
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: hccinfhir
3
- Version: 0.1.7
3
+ Version: 0.1.9
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,11 +1,12 @@
1
1
  hccinfhir/__init__.py,sha256=G_5m6jm3_BK5NdcZWoi0NEKJEsE_LjAU1RaLaL9xNPU,1043
2
- hccinfhir/datamodels.py,sha256=EHkuWMhmHBt8GfVP3lrxfSogu-qZQzeforFzp0Bn_bM,7714
2
+ hccinfhir/datamodels.py,sha256=lHDXkCpzafuzF094VoaQXl4MgeFPxPfi25QJxXj4YHk,10010
3
3
  hccinfhir/extractor.py,sha256=xL9c2VT-e2I7_c8N8j4Og42UEgVuCzyn9WFp3ntM5Ro,1822
4
- hccinfhir/extractor_837.py,sha256=Irp0ROWgZv6jru9w5sdkoTSYHDTh0v_I_xYRhWdHOjw,13037
4
+ hccinfhir/extractor_834.py,sha256=vODcD53iU5ZwQsSbBE8Gix9D-0fz-EhwkmjqRO6LML8,19541
5
+ hccinfhir/extractor_837.py,sha256=D60gUFtMk2S0NrJ0iq3ENo35yIwBmBQvF5TurJgRIa8,15327
5
6
  hccinfhir/extractor_fhir.py,sha256=wUN3vTm1oTZ-KvfcDebnpQMxAC-7YlRKv12Wrv3p85A,8490
6
7
  hccinfhir/filter.py,sha256=j_yD2g6RBXVUV9trKkWzsQ35x3fRvfKUPvEXKUefI64,2007
7
- hccinfhir/hccinfhir.py,sha256=tgNWGYvsQWOlmcnP-3yH3KfXgZtQ3IdxYfGP9SNSJb0,9879
8
- hccinfhir/model_calculate.py,sha256=vM0b4BkLafebk7A3yFxsEONRW4GkVbUl4GxNlLz5N8Q,6742
8
+ hccinfhir/hccinfhir.py,sha256=GLTqUHlD2DzU2Xiv6Y_lYXiWHNHXm96WGAoNKd2t_Wg,9786
9
+ hccinfhir/model_calculate.py,sha256=j1XMPaqW2r_pUyiUsLsDBICi56AIKMjVzDMF1EbJ24w,6744
9
10
  hccinfhir/model_coefficients.py,sha256=2Y_xCjX4__B1_xkX3pp-XTOW4KEAWo6RCVOOJ7ZZajM,5931
10
11
  hccinfhir/model_demographics.py,sha256=CR4WC8XVq-CI1nYJoVFc5-KXTw-pKoVlHkHqfnXlnj0,9121
11
12
  hccinfhir/model_dx_to_cc.py,sha256=guJny2Mb9z8YRNWCXGSIE3APbE06zwnA2NRkjAeUs60,1765
@@ -27,6 +28,7 @@ hccinfhir/data/ra_eligible_cpt_hcpcs_2026.csv,sha256=EYGN7k_rgCpJe59lL_yNInUcCkd
27
28
  hccinfhir/data/ra_hierarchies_2025.csv,sha256=HQSPNloe6mvvwMgv8ZwYAfWKkT2b2eUvm4JQy6S_mVQ,13045
28
29
  hccinfhir/data/ra_hierarchies_2026.csv,sha256=A6ZQZb0rpRWrySBB_KA5S4PGtMxWuzB2guU3aBE09v0,19596
29
30
  hccinfhir/sample_files/__init__.py,sha256=SGiSkpGrnxbvtEFMMlk82NFHOE50hFXcgKwKUSuVZUg,45
31
+ hccinfhir/sample_files/sample_834_01.txt,sha256=J2HMXfY6fAFpV36rvLQ3QymRRS2TPqf3TQY6CNS7TrE,1627
30
32
  hccinfhir/sample_files/sample_837_0.txt,sha256=eggrD259uHa05z2dfxWBpUDseSDp_AQcLyN_adpHyTw,5295
31
33
  hccinfhir/sample_files/sample_837_1.txt,sha256=E155MdemSDYoXokuTXUZ6Br_RGGedYv5t5dh-eMRmuk,1322
32
34
  hccinfhir/sample_files/sample_837_10.txt,sha256=zSJXI78vHAksA7FFQEVLvepefdpMM2_AexLyoDimV3Q,1129
@@ -44,7 +46,7 @@ hccinfhir/sample_files/sample_eob_1.json,sha256=_NGSVR2ysFpx-DcTvyga6dFCzhQ8Vi9f
44
46
  hccinfhir/sample_files/sample_eob_2.json,sha256=FcnJcx0ApOczxjJ_uxVLzCep9THfNf4xs9Yf7hxk8e4,1769
45
47
  hccinfhir/sample_files/sample_eob_200.ndjson,sha256=CxpjeQ1DCMUzZILaM68UEhfxO0p45YGhDDoCZeq8PxU,1917986
46
48
  hccinfhir/sample_files/sample_eob_3.json,sha256=4BW4wOMBEEU9RDfJR15rBEvk0KNHyuMEh3e055y87Hc,2306
47
- hccinfhir-0.1.7.dist-info/METADATA,sha256=o2Uur5C9layHtKuWQeEQ1THAwMpCLmEkmnVTvrOUWDE,24819
48
- hccinfhir-0.1.7.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
49
- hccinfhir-0.1.7.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
50
- hccinfhir-0.1.7.dist-info/RECORD,,
49
+ hccinfhir-0.1.9.dist-info/METADATA,sha256=_LWbfnpzjHKIP-T_2cRf0GlvgEY3SnJWKX_UEHkhycE,24819
50
+ hccinfhir-0.1.9.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
51
+ hccinfhir-0.1.9.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
52
+ hccinfhir-0.1.9.dist-info/RECORD,,