hccinfhir 0.2.3__py3-none-any.whl → 0.2.4__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
@@ -287,7 +287,8 @@ class EnrollmentData(BaseModel):
287
287
  # HCP (Health Care Plan) Info
288
288
  hcp_code: Current HCP code (HD04 first part)
289
289
  hcp_status: Current HCP status (HD04 second part)
290
- amount: Premium or cost share amount
290
+ amount_qualifier: AMT qualifier code (e.g., 'D' = premium, 'C1' = copay)
291
+ amount: Premium or cost share amount (numeric)
291
292
 
292
293
  # HCP History (multiple coverage periods)
293
294
  hcp_history: List of historical HCP coverage periods
@@ -367,7 +368,8 @@ class EnrollmentData(BaseModel):
367
368
  # HCP Info
368
369
  hcp_code: Optional[str] = None
369
370
  hcp_status: Optional[str] = None
370
- amount: Optional[str] = None
371
+ amount_qualifier: Optional[str] = None
372
+ amount: Optional[float] = None
371
373
 
372
374
  # HCP History
373
375
  hcp_history: List[HCPCoveragePeriod] = []
@@ -21,6 +21,21 @@ from hccinfhir.constants import (
21
21
  map_medicare_status_to_dual_code,
22
22
  map_aid_code_to_dual_status,
23
23
  )
24
+ from hccinfhir.utils import load_race_ethnicity
25
+
26
+ # Load race/ethnicity mapping at module level
27
+ _RACE_ETHNICITY_MAPPING: Optional[Dict[str, str]] = None
28
+
29
+
30
+ def _get_race_ethnicity_mapping() -> Dict[str, str]:
31
+ """Lazy load race/ethnicity mapping"""
32
+ global _RACE_ETHNICITY_MAPPING
33
+ if _RACE_ETHNICITY_MAPPING is None:
34
+ try:
35
+ _RACE_ETHNICITY_MAPPING = load_race_ethnicity()
36
+ except (FileNotFoundError, RuntimeError):
37
+ _RACE_ETHNICITY_MAPPING = {}
38
+ return _RACE_ETHNICITY_MAPPING
24
39
 
25
40
  # Constants
26
41
  TRANSACTION_TYPES = {"005010X220A1": "834"}
@@ -114,7 +129,8 @@ class MemberContext(BaseModel):
114
129
  # HCP Info
115
130
  hcp_code: Optional[str] = None
116
131
  hcp_status: Optional[str] = None
117
- amount: Optional[str] = None
132
+ amount_qualifier: Optional[str] = None
133
+ amount: Optional[float] = None
118
134
 
119
135
  # HCP History
120
136
  hcp_history: List[HCPContext] = []
@@ -206,6 +222,35 @@ def contains_any_keyword(text: str, keywords: set) -> bool:
206
222
  return any(kw in text_upper for kw in keywords)
207
223
 
208
224
 
225
+ def parse_race_code(raw_value: Optional[str]) -> Optional[str]:
226
+ """Parse race code from DMG05 and translate to human-readable name.
227
+
228
+ Handles formats like:
229
+ - ":RET:2135-2" -> "Hispanic or Latino"
230
+ - "2135-2" -> "Hispanic or Latino"
231
+ - "2106-3" -> "White"
232
+
233
+ Args:
234
+ raw_value: Raw race value from DMG segment
235
+
236
+ Returns:
237
+ Human-readable race/ethnicity name, or original value if not found
238
+ """
239
+ if not raw_value:
240
+ return None
241
+
242
+ # Extract code from formats like ":RET:2135-2" or "2135-2"
243
+ code = raw_value
244
+ if ':' in raw_value:
245
+ parts = raw_value.split(':')
246
+ # Take the last non-empty part which should be the code
247
+ code = next((p for p in reversed(parts) if p), raw_value)
248
+
249
+ # Look up in mapping
250
+ mapping = _get_race_ethnicity_mapping()
251
+ return mapping.get(code, raw_value)
252
+
253
+
209
254
  # ============================================================================
210
255
  # Dual Eligibility Logic
211
256
  # ============================================================================
@@ -248,7 +293,7 @@ def parse_ref_23(value: str, member: MemberContext) -> None:
248
293
  member.cin_check_digit = get_composite_part(value, 0)
249
294
  card_date = get_composite_part(value, 1)
250
295
  if card_date and len(card_date) >= 8:
251
- member.fame_card_issue_date = card_date[:8]
296
+ member.fame_card_issue_date = parse_date(card_date[:8])
252
297
 
253
298
 
254
299
  def parse_ref_3h(value: str, member: MemberContext) -> None:
@@ -277,7 +322,7 @@ def parse_ref_dx(value: str, member: MemberContext) -> None:
277
322
  member.carrier_code = strip_leading_zeros(carrier)
278
323
  policy_start = get_composite_part(value, 2)
279
324
  if policy_start and len(policy_start) >= 8:
280
- member.coverage_start_date = policy_start[:8]
325
+ member.coverage_start_date = parse_date(policy_start[:8])
281
326
 
282
327
 
283
328
  def parse_ref_17(value: str, member: MemberContext) -> None:
@@ -495,7 +540,8 @@ def _finalize_member(member: MemberContext, source: str, report_date: str) -> En
495
540
  res_zip_deliv_code=member.res_zip_deliv_code,
496
541
  orec=member.orec, crec=member.crec, snp=member.snp,
497
542
  low_income=member.low_income, lti=member.lti, new_enrollee=new_enrollee,
498
- hcp_code=member.hcp_code, hcp_status=member.hcp_status, amount=member.amount,
543
+ hcp_code=member.hcp_code, hcp_status=member.hcp_status,
544
+ amount_qualifier=member.amount_qualifier, amount=member.amount,
499
545
  hcp_history=hcp_history
500
546
  )
501
547
 
@@ -594,7 +640,7 @@ def parse_834_enrollment(segments: List[List[str]], source: str = None, report_d
594
640
  sex = get_segment_value(segment, 3)
595
641
  if sex in X12_SEX_CODE_MAPPING:
596
642
  member.sex = X12_SEX_CODE_MAPPING[sex]
597
- member.race = get_segment_value(segment, 5)
643
+ member.race = parse_race_code(get_segment_value(segment, 5))
598
644
  death_str = get_segment_value(segment, 6)
599
645
  if death_str and len(death_str) >= 8:
600
646
  member.death_date = parse_date(death_str[:8])
@@ -638,7 +684,13 @@ def parse_834_enrollment(segments: List[List[str]], source: str = None, report_d
638
684
 
639
685
  # AMT - Amount
640
686
  elif seg_id == 'AMT' and len(segment) >= 3:
641
- member.amount = get_segment_value(segment, 2)
687
+ member.amount_qualifier = get_segment_value(segment, 1)
688
+ amt_val = get_segment_value(segment, 2)
689
+ if amt_val:
690
+ try:
691
+ member.amount = float(amt_val)
692
+ except ValueError:
693
+ pass
642
694
 
643
695
  # Finalize last member
644
696
  if member.member_id or member.has_medicare or member.has_medicaid:
@@ -696,7 +748,7 @@ def extract_enrollment_834(content: str) -> List[EnrollmentData]:
696
748
  source = get_segment_value(segment, 2)
697
749
  gs_date = get_segment_value(segment, 4)
698
750
  if gs_date and len(gs_date) >= 8:
699
- report_date = f"{gs_date[:4]}-{gs_date[4:6]}-{gs_date[6:8]}"
751
+ report_date = parse_date(gs_date[:8])
700
752
  if len(segment) > 8 and segment[8] not in TRANSACTION_TYPES:
701
753
  raise ValueError("Invalid or unsupported 834 format")
702
754
  break
hccinfhir/utils.py CHANGED
@@ -247,6 +247,46 @@ def load_coefficients(file_path: str) -> Dict[Tuple[str, ModelName], float]:
247
247
  return coefficients
248
248
 
249
249
 
250
+ def load_race_ethnicity(file_path: str = "ph_race_and_ethnicity_cdc_v1.3.csv") -> Dict[str, str]:
251
+ """
252
+ Load CDC race and ethnicity codes from CSV file.
253
+ Expected format: Concept Code,Hierarchical Property,Concept Name,...
254
+
255
+ Args:
256
+ file_path: Filename or path to the CSV file
257
+
258
+ Returns:
259
+ Dictionary mapping concept code to concept name
260
+
261
+ Raises:
262
+ FileNotFoundError: If file cannot be found
263
+ RuntimeError: If file cannot be loaded or parsed
264
+ """
265
+ mapping: Dict[str, str] = {}
266
+
267
+ try:
268
+ resolved_path = resolve_data_file(file_path)
269
+ with open(resolved_path, "r", encoding="utf-8", errors="replace") as file:
270
+ content = file.read()
271
+ except FileNotFoundError as e:
272
+ raise FileNotFoundError(f"Could not load race/ethnicity mapping: {e}")
273
+ except Exception as e:
274
+ raise RuntimeError(f"Error loading race/ethnicity file '{file_path}': {e}")
275
+
276
+ for line in content.splitlines()[1:]: # Skip header
277
+ try:
278
+ parts = line.split(',')
279
+ if len(parts) >= 3:
280
+ concept_code = parts[0].strip()
281
+ concept_name = parts[2].strip()
282
+ if concept_code and concept_name:
283
+ mapping[concept_code] = concept_name
284
+ except (ValueError, IndexError):
285
+ continue # Skip malformed lines
286
+
287
+ return mapping
288
+
289
+
250
290
  def load_labels(file_path: str) -> Dict[Tuple[str, ModelName], str]:
251
291
  """
252
292
  Load HCC labels from a CSV file.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: hccinfhir
3
- Version: 0.2.3
3
+ Version: 0.2.4
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
@@ -95,6 +95,7 @@ print(f"HCCs: {result.hcc_list}")
95
95
  - **Use Case**: Extract dual eligibility status, detect Medicaid coverage loss
96
96
  - **Features**: California DHCS aid code mapping, Medicare status codes, coverage tracking
97
97
  - **Output**: Demographics with accurate dual eligibility for risk calculations
98
+ - **Architecture**: See [834 Parsing Documentation](./README_PARSING834.md) for transaction structure and parsing logic
98
99
 
99
100
  ### 3. **FHIR ExplanationOfBenefit Resources**
100
101
  - **Input**: FHIR EOB from CMS Blue Button 2.0 / BCDA API
@@ -1073,6 +1074,7 @@ Apache License 2.0. See [LICENSE](LICENSE) for details.
1073
1074
  ## 📞 Support
1074
1075
 
1075
1076
  - **Claude Code Documentation**: [CLAUDE.md](./CLAUDE.md) - Comprehensive developer guide
1077
+ - **834 Parsing Architecture**: [README_PARSING834.md](./README_PARSING834.md) - X12 834 transaction structure and parsing logic
1076
1078
  - **Issues**: [GitHub Issues](https://github.com/mimilabs/hccinfhir/issues)
1077
1079
 
1078
1080
  ## 👥 Contributors
@@ -1,9 +1,9 @@
1
1
  hccinfhir/__init__.py,sha256=3aFYtjTklZJg3wIlnMJNgfDBaDCfKXVlYsacdsZ9L4I,1113
2
2
  hccinfhir/constants.py,sha256=C4Vyjtzgyd4Jm2I2X6cTYQZLe-jAMC8boUcy-7OXQDQ,8473
3
- hccinfhir/datamodels.py,sha256=xGh9E5RVi4vONhtIZw2XiaFwVLc5UK027trY31YMUWc,15457
3
+ hccinfhir/datamodels.py,sha256=LNk94V3ez8f1J4Y8PXxNgHslVfwNQoJCl0SO4etgxs4,15593
4
4
  hccinfhir/defaults.py,sha256=aKdXPhf9bYUzpGvXM1GIXZaKxqkKInt3v9meLB9fWog,1394
5
5
  hccinfhir/extractor.py,sha256=xL9c2VT-e2I7_c8N8j4Og42UEgVuCzyn9WFp3ntM5Ro,1822
6
- hccinfhir/extractor_834.py,sha256=zH2nOUJvIJvbDLf6HJWmwCw2yAjT-6RCJyuH4kmIKIQ,27862
6
+ hccinfhir/extractor_834.py,sha256=gXbSQJOAdQiOub2LHUYX4Eb7ABKL0WC5r0-ZX1pe70k,29579
7
7
  hccinfhir/extractor_837.py,sha256=fGsvBTWIj9dsHLGGR67AdlYDSsFi5qnSVlTgwkL1f-E,15334
8
8
  hccinfhir/extractor_fhir.py,sha256=wUN3vTm1oTZ-KvfcDebnpQMxAC-7YlRKv12Wrv3p85A,8490
9
9
  hccinfhir/filter.py,sha256=j_yD2g6RBXVUV9trKkWzsQ35x3fRvfKUPvEXKUefI64,2007
@@ -15,10 +15,11 @@ hccinfhir/model_dx_to_cc.py,sha256=Yjc6xKI-jMXsbOzS_chc4NI15Bwagb7BwZZ8cKQaTbk,1
15
15
  hccinfhir/model_hierarchies.py,sha256=cboUnSHZZfOxA8QZKV4QIE-32duElssML32OqYT-65g,1542
16
16
  hccinfhir/model_interactions.py,sha256=g6jK27Xu8RQUHS3lk4sk2v6w6wqd52mdbGn0BsnR7Pk,21394
17
17
  hccinfhir/samples.py,sha256=2VSWS81cv9EnaHqK7sd6CjwG6FUI9E--5wHgD000REI,9952
18
- hccinfhir/utils.py,sha256=hQgHjuOcEQcnxemTZwqFBHWvLC5-C1Gup9cDXEYlZjE,10770
18
+ hccinfhir/utils.py,sha256=WQ2atW0CrdX7sAz_YRLeY4JD-CuH0o-WRusQ_xVVfgY,12152
19
19
  hccinfhir/data/__init__.py,sha256=SGiSkpGrnxbvtEFMMlk82NFHOE50hFXcgKwKUSuVZUg,45
20
20
  hccinfhir/data/hcc_is_chronic.csv,sha256=Bwd-RND6SdEsKP-assoBaXnjUJAuDXhSkwWlymux72Y,19701
21
21
  hccinfhir/data/hcc_is_chronic_without_esrd_model.csv,sha256=eVVI4_8mQNkiBiNO3kattfT_zfcV18XgmiltdzZEXSo,17720
22
+ hccinfhir/data/ph_race_and_ethnicity_cdc_v1.3.csv,sha256=5tw_ATN1mQWVUIahXZyIa5GOX-977PzfhNlGvm43tD8,146970
22
23
  hccinfhir/data/ra_coefficients_2025.csv,sha256=I0S2hoJlfig-D0oSFxy0b3Piv7m9AzOGo2CwR6bcQ9w,215191
23
24
  hccinfhir/data/ra_coefficients_2026.csv,sha256=0gfjGgVdIEWkBO01NaAbTLMzHCYINA0rf_zl8ojngCY,288060
24
25
  hccinfhir/data/ra_dx_to_cc_2025.csv,sha256=4N7vF6VZndkl7d3Fo0cGsbAPAZdCjAizSH8BOKsZNAo,1618924
@@ -54,7 +55,7 @@ hccinfhir/sample_files/sample_eob_1.json,sha256=_NGSVR2ysFpx-DcTvyga6dFCzhQ8Vi9f
54
55
  hccinfhir/sample_files/sample_eob_2.json,sha256=FcnJcx0ApOczxjJ_uxVLzCep9THfNf4xs9Yf7hxk8e4,1769
55
56
  hccinfhir/sample_files/sample_eob_200.ndjson,sha256=CxpjeQ1DCMUzZILaM68UEhfxO0p45YGhDDoCZeq8PxU,1917986
56
57
  hccinfhir/sample_files/sample_eob_3.json,sha256=4BW4wOMBEEU9RDfJR15rBEvk0KNHyuMEh3e055y87Hc,2306
57
- hccinfhir-0.2.3.dist-info/METADATA,sha256=YHHcOAObdo2gWJtPmP6y05-EXeXHpuE40W1pdUXlydw,37132
58
- hccinfhir-0.2.3.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
59
- hccinfhir-0.2.3.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
60
- hccinfhir-0.2.3.dist-info/RECORD,,
58
+ hccinfhir-0.2.4.dist-info/METADATA,sha256=wjhOKhD3HpfuCs69t-sBSrQn_kuNngbRaA2lLNuCTno,37381
59
+ hccinfhir-0.2.4.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
60
+ hccinfhir-0.2.4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
61
+ hccinfhir-0.2.4.dist-info/RECORD,,