hccinfhir 0.2.0__tar.gz → 0.2.1__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.2.0 → hccinfhir-0.2.1}/PKG-INFO +1 -1
- hccinfhir-0.2.1/hccinfhir/constants.py +240 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/extractor_834.py +52 -71
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/extractor_837.py +2 -2
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/model_coefficients.py +2 -2
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/model_demographics.py +26 -29
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/model_interactions.py +7 -7
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/pyproject.toml +1 -1
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/.gitignore +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/LICENSE +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/README.md +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/__init__.py +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/data/__init__.py +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/data/hcc_is_chronic.csv +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/data/hcc_is_chronic_without_esrd_model.csv +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/data/ra_coefficients_2025.csv +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/data/ra_coefficients_2026.csv +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/data/ra_dx_to_cc_2025.csv +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/data/ra_dx_to_cc_2026.csv +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/data/ra_eligible_cpt_hcpcs_2023.csv +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/data/ra_eligible_cpt_hcpcs_2024.csv +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/data/ra_eligible_cpt_hcpcs_2025.csv +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/data/ra_eligible_cpt_hcpcs_2026.csv +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/data/ra_hierarchies_2025.csv +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/data/ra_hierarchies_2026.csv +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/datamodels.py +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/defaults.py +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/extractor.py +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/extractor_fhir.py +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/filter.py +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/hccinfhir.py +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/model_calculate.py +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/model_dx_to_cc.py +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/model_hierarchies.py +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/sample_files/__init__.py +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/sample_files/sample_834_01.txt +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/sample_files/sample_837_0.txt +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/sample_files/sample_837_1.txt +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/sample_files/sample_837_10.txt +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/sample_files/sample_837_11.txt +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/sample_files/sample_837_12.txt +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/sample_files/sample_837_2.txt +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/sample_files/sample_837_3.txt +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/sample_files/sample_837_4.txt +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/sample_files/sample_837_5.txt +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/sample_files/sample_837_6.txt +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/sample_files/sample_837_7.txt +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/sample_files/sample_837_8.txt +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/sample_files/sample_837_9.txt +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/sample_files/sample_eob_1.json +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/sample_files/sample_eob_2.json +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/sample_files/sample_eob_200.ndjson +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/sample_files/sample_eob_3.json +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/samples.py +0 -0
- {hccinfhir-0.2.0 → hccinfhir-0.2.1}/hccinfhir/utils.py +0 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CMS Risk Adjustment Domain Constants
|
|
3
|
+
|
|
4
|
+
This module contains constants used across the HCC risk adjustment system,
|
|
5
|
+
including dual eligibility codes, OREC/CREC values, and state-specific mappings.
|
|
6
|
+
|
|
7
|
+
References:
|
|
8
|
+
- CMS Rate Announcement and Call Letter
|
|
9
|
+
- Medicare Advantage Enrollment and Disenrollment Guidance
|
|
10
|
+
- X12 834 Implementation Guides
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Set, Dict
|
|
14
|
+
|
|
15
|
+
# =============================================================================
|
|
16
|
+
# DUAL ELIGIBILITY CODES
|
|
17
|
+
# =============================================================================
|
|
18
|
+
# CMS Dual Eligibility Status Codes (Medicare + Medicaid)
|
|
19
|
+
# Used in coefficient prefix selection (CNA_, CFA_, CPA_, etc.)
|
|
20
|
+
|
|
21
|
+
VALID_DUAL_CODES: Set[str] = {'00', '01', '02', '03', '04', '05', '06', '08'}
|
|
22
|
+
|
|
23
|
+
# Non-Dual Eligible
|
|
24
|
+
NON_DUAL_CODE: str = '00'
|
|
25
|
+
|
|
26
|
+
# Full Benefit Dual Eligible (receive both Medicare and full Medicaid benefits)
|
|
27
|
+
# Uses CFA_ (Community, Full Benefit Dual, Aged) or CFD_ (Disabled) prefixes
|
|
28
|
+
FULL_BENEFIT_DUAL_CODES: Set[str] = {
|
|
29
|
+
'02', # QMB Plus (Qualified Medicare Beneficiary Plus)
|
|
30
|
+
'04', # SLMB Plus (Specified Low-Income Medicare Beneficiary Plus)
|
|
31
|
+
'08', # Other Full Benefit Dual Eligible
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Partial Benefit Dual Eligible (Medicare + limited Medicaid)
|
|
35
|
+
# Uses CPA_ (Community, Partial Benefit Dual, Aged) or CPD_ (Disabled) prefixes
|
|
36
|
+
PARTIAL_BENEFIT_DUAL_CODES: Set[str] = {
|
|
37
|
+
'01', # QMB Only
|
|
38
|
+
'03', # SLMB Only
|
|
39
|
+
'05', # QDWI (Qualified Disabled and Working Individual)
|
|
40
|
+
'06', # QI (Qualifying Individual)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# =============================================================================
|
|
44
|
+
# OREC - Original Reason for Entitlement Code
|
|
45
|
+
# =============================================================================
|
|
46
|
+
# Determines if beneficiary has ESRD and affects coefficient prefix selection
|
|
47
|
+
|
|
48
|
+
VALID_OREC_VALUES: Set[str] = {'0', '1', '2', '3'}
|
|
49
|
+
|
|
50
|
+
OREC_DESCRIPTIONS: Dict[str, str] = {
|
|
51
|
+
'0': 'Old Age and Survivors Insurance (OASI)',
|
|
52
|
+
'1': 'Disability Insurance Benefits (DIB)',
|
|
53
|
+
'2': 'ESRD - End-Stage Renal Disease',
|
|
54
|
+
'3': 'DIB and ESRD',
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# OREC codes indicating ESRD status (per CMS documentation)
|
|
58
|
+
OREC_ESRD_CODES: Set[str] = {'2', '3'}
|
|
59
|
+
|
|
60
|
+
# =============================================================================
|
|
61
|
+
# CREC - Current Reason for Entitlement Code
|
|
62
|
+
# =============================================================================
|
|
63
|
+
# Current entitlement status (may differ from OREC)
|
|
64
|
+
|
|
65
|
+
VALID_CREC_VALUES: Set[str] = {'0', '1', '2', '3'}
|
|
66
|
+
|
|
67
|
+
CREC_DESCRIPTIONS: Dict[str, str] = {
|
|
68
|
+
'0': 'Old Age and Survivors Insurance (OASI)',
|
|
69
|
+
'1': 'Disability Insurance Benefits (DIB)',
|
|
70
|
+
'2': 'ESRD - End-Stage Renal Disease',
|
|
71
|
+
'3': 'DIB and ESRD',
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# CREC codes indicating ESRD status
|
|
75
|
+
CREC_ESRD_CODES: Set[str] = {'2', '3'}
|
|
76
|
+
|
|
77
|
+
# =============================================================================
|
|
78
|
+
# COEFFICIENT PREFIX GROUPS
|
|
79
|
+
# =============================================================================
|
|
80
|
+
# Used for prefix_override logic in model_demographics.py
|
|
81
|
+
|
|
82
|
+
# ESRD model prefixes
|
|
83
|
+
ESRD_PREFIXES: Set[str] = {'DI_', 'DNE_', 'GI_', 'GNE_', 'GFPA_', 'GFPN_', 'GNPA_', 'GNPN_'}
|
|
84
|
+
|
|
85
|
+
# CMS-HCC new enrollee prefixes
|
|
86
|
+
NEW_ENROLLEE_PREFIXES: Set[str] = {'NE_', 'SNPNE_', 'DNE_', 'GNE_'}
|
|
87
|
+
|
|
88
|
+
# CMS-HCC community prefixes
|
|
89
|
+
COMMUNITY_PREFIXES: Set[str] = {'CNA_', 'CND_', 'CFA_', 'CFD_', 'CPA_', 'CPD_'}
|
|
90
|
+
|
|
91
|
+
# Institutionalized prefixes
|
|
92
|
+
INSTITUTIONAL_PREFIXES: Set[str] = {'INS_', 'GI_'}
|
|
93
|
+
|
|
94
|
+
# Full Benefit Dual prefixes
|
|
95
|
+
FULL_BENEFIT_DUAL_PREFIXES: Set[str] = {'CFA_', 'CFD_', 'GFPA_', 'GFPN_'}
|
|
96
|
+
|
|
97
|
+
# Partial Benefit Dual prefixes
|
|
98
|
+
PARTIAL_BENEFIT_DUAL_PREFIXES: Set[str] = {'CPA_', 'CPD_'}
|
|
99
|
+
|
|
100
|
+
# Non-Dual prefixes
|
|
101
|
+
NON_DUAL_PREFIXES: Set[str] = {'CNA_', 'CND_', 'GNPA_', 'GNPN_'}
|
|
102
|
+
|
|
103
|
+
# =============================================================================
|
|
104
|
+
# DEMOGRAPHIC CODES
|
|
105
|
+
# =============================================================================
|
|
106
|
+
|
|
107
|
+
VALID_SEX_CODES: Set[str] = {'M', 'F'}
|
|
108
|
+
|
|
109
|
+
# X12 834 Gender Code mappings
|
|
110
|
+
X12_SEX_CODE_MAPPING: Dict[str, str] = {
|
|
111
|
+
'M': 'M',
|
|
112
|
+
'F': 'F',
|
|
113
|
+
'1': 'M', # X12 numeric code
|
|
114
|
+
'2': 'F', # X12 numeric code
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# =============================================================================
|
|
118
|
+
# X12 834 MAINTENANCE TYPE CODES
|
|
119
|
+
# =============================================================================
|
|
120
|
+
# INS03 - Maintenance Type Code
|
|
121
|
+
|
|
122
|
+
MAINTENANCE_TYPE_CHANGE: str = '001'
|
|
123
|
+
MAINTENANCE_TYPE_ADD: str = '021'
|
|
124
|
+
MAINTENANCE_TYPE_CANCEL: str = '024'
|
|
125
|
+
MAINTENANCE_TYPE_REINSTATE: str = '025'
|
|
126
|
+
|
|
127
|
+
MAINTENANCE_TYPE_DESCRIPTIONS: Dict[str, str] = {
|
|
128
|
+
'001': 'Change',
|
|
129
|
+
'021': 'Addition',
|
|
130
|
+
'024': 'Cancellation/Termination',
|
|
131
|
+
'025': 'Reinstatement',
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# =============================================================================
|
|
135
|
+
# STATE-SPECIFIC MAPPINGS
|
|
136
|
+
# =============================================================================
|
|
137
|
+
|
|
138
|
+
# -----------------------------------------------------------------------------
|
|
139
|
+
# California DHCS Medi-Cal Aid Codes
|
|
140
|
+
# -----------------------------------------------------------------------------
|
|
141
|
+
# Maps California-specific aid codes to CMS dual eligibility codes
|
|
142
|
+
# Source: California DHCS 834 Implementation Guide
|
|
143
|
+
|
|
144
|
+
MEDI_CAL_AID_CODES: Dict[str, str] = {
|
|
145
|
+
# Full Benefit Dual (QMB Plus, SLMB Plus)
|
|
146
|
+
'4N': '02', # QMB Plus - Aged
|
|
147
|
+
'4P': '02', # QMB Plus - Disabled
|
|
148
|
+
'5B': '04', # SLMB Plus - Aged
|
|
149
|
+
'5D': '04', # SLMB Plus - Disabled
|
|
150
|
+
|
|
151
|
+
# Partial Benefit Dual (QMB Only, SLMB Only, QI)
|
|
152
|
+
'4M': '01', # QMB Only - Aged
|
|
153
|
+
'4O': '01', # QMB Only - Disabled
|
|
154
|
+
'5A': '03', # SLMB Only - Aged
|
|
155
|
+
'5C': '03', # SLMB Only - Disabled
|
|
156
|
+
'5E': '06', # QI - Aged
|
|
157
|
+
'5F': '06', # QI - Disabled
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# -----------------------------------------------------------------------------
|
|
161
|
+
# Medicare Status Code Mappings
|
|
162
|
+
# -----------------------------------------------------------------------------
|
|
163
|
+
# Maps Medicare status codes (from various sources) to CMS dual eligibility codes
|
|
164
|
+
# Used in X12 834 REF*ABB segment and other payer files
|
|
165
|
+
|
|
166
|
+
MEDICARE_STATUS_CODE_MAPPING: Dict[str, str] = {
|
|
167
|
+
# QMB - Qualified Medicare Beneficiary
|
|
168
|
+
'QMB': '01', # QMB Only (Partial)
|
|
169
|
+
'QMBONLY': '01',
|
|
170
|
+
'QMBPLUS': '02', # QMB Plus (Full Benefit)
|
|
171
|
+
'QMB+': '02',
|
|
172
|
+
|
|
173
|
+
# SLMB - Specified Low-Income Medicare Beneficiary
|
|
174
|
+
'SLMB': '03', # SLMB Only (Partial)
|
|
175
|
+
'SLMBONLY': '03',
|
|
176
|
+
'SLMBPLUS': '04', # SLMB Plus (Full Benefit)
|
|
177
|
+
'SLMB+': '04',
|
|
178
|
+
|
|
179
|
+
# Other dual eligibility programs
|
|
180
|
+
'QDWI': '05', # Qualified Disabled and Working Individual
|
|
181
|
+
'QI': '06', # Qualifying Individual
|
|
182
|
+
'QI1': '06',
|
|
183
|
+
'FBDE': '08', # Full Benefit Dual Eligible (Other)
|
|
184
|
+
'OTHERFULL': '08',
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
# =============================================================================
|
|
188
|
+
# HELPER FUNCTIONS
|
|
189
|
+
# =============================================================================
|
|
190
|
+
|
|
191
|
+
def is_full_benefit_dual(dual_code: str) -> bool:
|
|
192
|
+
"""Check if dual eligibility code is Full Benefit Dual"""
|
|
193
|
+
return dual_code in FULL_BENEFIT_DUAL_CODES
|
|
194
|
+
|
|
195
|
+
def is_partial_benefit_dual(dual_code: str) -> bool:
|
|
196
|
+
"""Check if dual eligibility code is Partial Benefit Dual"""
|
|
197
|
+
return dual_code in PARTIAL_BENEFIT_DUAL_CODES
|
|
198
|
+
|
|
199
|
+
def is_esrd_by_orec(orec: str) -> bool:
|
|
200
|
+
"""Check if OREC indicates ESRD status"""
|
|
201
|
+
return orec in OREC_ESRD_CODES
|
|
202
|
+
|
|
203
|
+
def is_esrd_by_crec(crec: str) -> bool:
|
|
204
|
+
"""Check if CREC indicates ESRD status"""
|
|
205
|
+
return crec in CREC_ESRD_CODES
|
|
206
|
+
|
|
207
|
+
def normalize_medicare_status_code(status: str) -> str:
|
|
208
|
+
"""Normalize Medicare status code (uppercase, no spaces/hyphens)"""
|
|
209
|
+
if not status:
|
|
210
|
+
return ''
|
|
211
|
+
return status.upper().replace(' ', '').replace('-', '')
|
|
212
|
+
|
|
213
|
+
def map_medicare_status_to_dual_code(status: str) -> str:
|
|
214
|
+
"""Map Medicare status code to dual eligibility code
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
status: Medicare status code (e.g., 'QMB Plus', 'SLMB', 'QI')
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Dual eligibility code ('01'-'08') or '00' if not found
|
|
221
|
+
"""
|
|
222
|
+
if not status:
|
|
223
|
+
return NON_DUAL_CODE
|
|
224
|
+
|
|
225
|
+
normalized = normalize_medicare_status_code(status)
|
|
226
|
+
return MEDICARE_STATUS_CODE_MAPPING.get(normalized, NON_DUAL_CODE)
|
|
227
|
+
|
|
228
|
+
def map_aid_code_to_dual_status(aid_code: str) -> str:
|
|
229
|
+
"""Map California Medi-Cal aid code to dual eligibility code
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
aid_code: California aid code (e.g., '4N', '5B')
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Dual eligibility code ('01'-'08') or '00' if not found
|
|
236
|
+
"""
|
|
237
|
+
if not aid_code:
|
|
238
|
+
return NON_DUAL_CODE
|
|
239
|
+
|
|
240
|
+
return MEDI_CAL_AID_CODES.get(aid_code, NON_DUAL_CODE)
|
|
@@ -1,29 +1,23 @@
|
|
|
1
|
-
from typing import List, Optional, Dict
|
|
1
|
+
from typing import List, Optional, Dict, Any, Tuple
|
|
2
2
|
from pydantic import BaseModel
|
|
3
3
|
from datetime import datetime, date
|
|
4
4
|
from hccinfhir.datamodels import Demographics, EnrollmentData
|
|
5
|
+
from hccinfhir.constants import (
|
|
6
|
+
VALID_DUAL_CODES,
|
|
7
|
+
FULL_BENEFIT_DUAL_CODES,
|
|
8
|
+
PARTIAL_BENEFIT_DUAL_CODES,
|
|
9
|
+
VALID_OREC_VALUES,
|
|
10
|
+
VALID_CREC_VALUES,
|
|
11
|
+
X12_SEX_CODE_MAPPING,
|
|
12
|
+
NON_DUAL_CODE,
|
|
13
|
+
map_medicare_status_to_dual_code,
|
|
14
|
+
map_aid_code_to_dual_status,
|
|
15
|
+
)
|
|
5
16
|
|
|
6
17
|
TRANSACTION_TYPES = {
|
|
7
18
|
"005010X220A1": "834", # Benefit Enrollment and Maintenance
|
|
8
19
|
}
|
|
9
20
|
|
|
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
21
|
class MemberContext(BaseModel):
|
|
28
22
|
"""Tracks member-level data across segments within 834 transaction"""
|
|
29
23
|
# Identifiers
|
|
@@ -95,7 +89,7 @@ def is_medicaid_terminated(enrollment: EnrollmentData) -> bool:
|
|
|
95
89
|
"""Check if Medicaid coverage is being terminated (maintenance type 024)"""
|
|
96
90
|
return enrollment.maintenance_type == '024'
|
|
97
91
|
|
|
98
|
-
def medicaid_status_summary(enrollment: EnrollmentData) -> Dict[str,
|
|
92
|
+
def medicaid_status_summary(enrollment: EnrollmentData) -> Dict[str, Any]:
|
|
99
93
|
"""Get summary of Medicaid coverage status for monitoring
|
|
100
94
|
|
|
101
95
|
Args:
|
|
@@ -157,45 +151,28 @@ def get_segment_value(segment: List[str], index: int, default: Optional[str] = N
|
|
|
157
151
|
pass
|
|
158
152
|
return default
|
|
159
153
|
|
|
160
|
-
def
|
|
161
|
-
"""
|
|
154
|
+
def parse_composite_ref_value(value: str) -> str:
|
|
155
|
+
"""Parse X12 composite element format: 'qualifier;id;...'
|
|
162
156
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
157
|
+
X12 uses semicolons to separate sub-elements within a composite data element.
|
|
158
|
+
Example: REF*23*9;20061234; where 9 is the ID type qualifier
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
value: Raw REF segment value (e.g., '9;20061234;' or '20061234')
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
The last non-empty sub-element (the actual ID)
|
|
170
165
|
"""
|
|
171
|
-
if not
|
|
172
|
-
return
|
|
166
|
+
if not value:
|
|
167
|
+
return value
|
|
173
168
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
}
|
|
169
|
+
if ';' in value:
|
|
170
|
+
# Split by semicolon and filter out empty parts
|
|
171
|
+
parts = [p for p in value.split(';') if p]
|
|
172
|
+
return parts[-1] if parts else value
|
|
191
173
|
|
|
192
|
-
return
|
|
174
|
+
return value
|
|
193
175
|
|
|
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
176
|
|
|
200
177
|
def determine_dual_status(member: MemberContext) -> str:
|
|
201
178
|
"""Intelligently derive dual eligibility code from available data
|
|
@@ -208,19 +185,19 @@ def determine_dual_status(member: MemberContext) -> str:
|
|
|
208
185
|
5. Default to non-dual ('00')
|
|
209
186
|
"""
|
|
210
187
|
# Priority 1: Explicit dual_elgbl_cd
|
|
211
|
-
if member.dual_elgbl_cd and member.dual_elgbl_cd in
|
|
188
|
+
if member.dual_elgbl_cd and member.dual_elgbl_cd in VALID_DUAL_CODES:
|
|
212
189
|
return member.dual_elgbl_cd
|
|
213
190
|
|
|
214
191
|
# Priority 2: California aid code mapping
|
|
215
192
|
if member.medi_cal_aid_code:
|
|
216
193
|
dual_code = map_aid_code_to_dual_status(member.medi_cal_aid_code)
|
|
217
|
-
if dual_code:
|
|
194
|
+
if dual_code != NON_DUAL_CODE:
|
|
218
195
|
return dual_code
|
|
219
196
|
|
|
220
197
|
# Priority 3: Medicare status code
|
|
221
198
|
if member.medicare_status_code:
|
|
222
199
|
dual_code = map_medicare_status_to_dual_code(member.medicare_status_code)
|
|
223
|
-
if dual_code:
|
|
200
|
+
if dual_code != NON_DUAL_CODE:
|
|
224
201
|
return dual_code
|
|
225
202
|
|
|
226
203
|
# Priority 4: Both Medicare and Medicaid coverage present
|
|
@@ -229,9 +206,9 @@ def determine_dual_status(member: MemberContext) -> str:
|
|
|
229
206
|
return '08'
|
|
230
207
|
|
|
231
208
|
# Default: Non-dual
|
|
232
|
-
return
|
|
209
|
+
return NON_DUAL_CODE
|
|
233
210
|
|
|
234
|
-
def classify_dual_benefit_level(dual_code: str) ->
|
|
211
|
+
def classify_dual_benefit_level(dual_code: str) -> Tuple[bool, bool]:
|
|
235
212
|
"""Classify as Full Benefit Dual (FBD) or Partial Benefit Dual (PBD)
|
|
236
213
|
|
|
237
214
|
Full Benefit Dual codes: 02, 04, 08
|
|
@@ -242,11 +219,8 @@ def classify_dual_benefit_level(dual_code: str) -> tuple[bool, bool]:
|
|
|
242
219
|
- Uses CPA_ (Community, Partial Benefit Dual, Aged) prefix
|
|
243
220
|
- Uses CPD_ (Community, Partial Benefit Dual, Disabled) prefix
|
|
244
221
|
"""
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
is_fbd = dual_code in full_benefit_codes
|
|
249
|
-
is_pbd = dual_code in partial_benefit_codes
|
|
222
|
+
is_fbd = dual_code in FULL_BENEFIT_DUAL_CODES
|
|
223
|
+
is_pbd = dual_code in PARTIAL_BENEFIT_DUAL_CODES
|
|
250
224
|
|
|
251
225
|
return is_fbd, is_pbd
|
|
252
226
|
|
|
@@ -284,7 +258,7 @@ def parse_834_enrollment(segments: List[List[str]]) -> List[EnrollmentData]:
|
|
|
284
258
|
enrollments = []
|
|
285
259
|
member = MemberContext()
|
|
286
260
|
|
|
287
|
-
for
|
|
261
|
+
for segment in segments:
|
|
288
262
|
if len(segment) < 2:
|
|
289
263
|
continue
|
|
290
264
|
|
|
@@ -330,11 +304,11 @@ def parse_834_enrollment(segments: List[List[str]]) -> List[EnrollmentData]:
|
|
|
330
304
|
|
|
331
305
|
# Medicaid Identifiers
|
|
332
306
|
elif qualifier == '1D': # Medicaid/Recipient ID
|
|
333
|
-
member.medicaid_id = value
|
|
307
|
+
member.medicaid_id = parse_composite_ref_value(value)
|
|
334
308
|
member.has_medicaid = True
|
|
335
309
|
elif qualifier == '23': # Medicaid Recipient ID (alternative)
|
|
336
310
|
if not member.medicaid_id:
|
|
337
|
-
member.medicaid_id = value
|
|
311
|
+
member.medicaid_id = parse_composite_ref_value(value)
|
|
338
312
|
member.has_medicaid = True
|
|
339
313
|
|
|
340
314
|
# California Medi-Cal Specific
|
|
@@ -345,13 +319,13 @@ def parse_834_enrollment(segments: List[List[str]]) -> List[EnrollmentData]:
|
|
|
345
319
|
|
|
346
320
|
# Custom dual eligibility indicators
|
|
347
321
|
elif qualifier == 'F5': # Dual Eligibility Code (custom)
|
|
348
|
-
if value in
|
|
322
|
+
if value in VALID_DUAL_CODES:
|
|
349
323
|
member.dual_elgbl_cd = value
|
|
350
324
|
elif qualifier == 'DX': # OREC (custom)
|
|
351
|
-
if value in
|
|
325
|
+
if value in VALID_OREC_VALUES:
|
|
352
326
|
member.orec = value
|
|
353
327
|
elif qualifier == 'DY': # CREC (custom)
|
|
354
|
-
if value in
|
|
328
|
+
if value in VALID_CREC_VALUES:
|
|
355
329
|
member.crec = value
|
|
356
330
|
elif qualifier == 'EJ': # Low Income Subsidy indicator
|
|
357
331
|
member.low_income = (value.upper() in ['Y', 'YES', '1', 'TRUE'])
|
|
@@ -376,8 +350,8 @@ def parse_834_enrollment(segments: List[List[str]]) -> List[EnrollmentData]:
|
|
|
376
350
|
|
|
377
351
|
# DMG03 = Gender Code
|
|
378
352
|
sex = get_segment_value(segment, 3)
|
|
379
|
-
if sex in
|
|
380
|
-
member.sex =
|
|
353
|
+
if sex in X12_SEX_CODE_MAPPING:
|
|
354
|
+
member.sex = X12_SEX_CODE_MAPPING[sex]
|
|
381
355
|
|
|
382
356
|
# ===== DTP - Date Time Periods =====
|
|
383
357
|
elif seg_id == 'DTP' and len(segment) >= 4:
|
|
@@ -440,6 +414,13 @@ def parse_834_enrollment(segments: List[List[str]]) -> List[EnrollmentData]:
|
|
|
440
414
|
member.has_medicare = True
|
|
441
415
|
member.has_medicaid = True
|
|
442
416
|
|
|
417
|
+
# Detect LTI (Long Term Institutionalized)
|
|
418
|
+
if any(keyword in combined for keyword in [
|
|
419
|
+
'LTC', 'LONG TERM CARE', 'LONG-TERM CARE', 'NURSING HOME',
|
|
420
|
+
'SKILLED NURSING', 'SNF', 'INSTITUTIONALIZED'
|
|
421
|
+
]):
|
|
422
|
+
member.lti = True
|
|
423
|
+
|
|
443
424
|
# Don't forget last member
|
|
444
425
|
if member.member_id or member.has_medicare or member.has_medicaid:
|
|
445
426
|
enrollments.append(create_enrollment_data(member))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import List, Optional, Dict
|
|
1
|
+
from typing import List, Optional, Dict, Tuple
|
|
2
2
|
from pydantic import BaseModel
|
|
3
3
|
from hccinfhir.datamodels import ServiceLevelData
|
|
4
4
|
|
|
@@ -65,7 +65,7 @@ def parse_diagnosis_codes(segment: List[str]) -> Dict[str, str]:
|
|
|
65
65
|
dx_lookup[str(pos)] = code
|
|
66
66
|
return dx_lookup
|
|
67
67
|
|
|
68
|
-
def process_service_line(segments: List[List[str]], start_index: int) ->
|
|
68
|
+
def process_service_line(segments: List[List[str]], start_index: int) -> Tuple[Optional[str], Optional[str]]:
|
|
69
69
|
"""Extract NDC and service date from service line segments"""
|
|
70
70
|
ndc = None
|
|
71
71
|
service_date = None
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Dict, Tuple, Optional
|
|
1
|
+
from typing import Dict, Tuple, Optional, Set
|
|
2
2
|
from hccinfhir.datamodels import ModelName, Demographics, PrefixOverride
|
|
3
3
|
|
|
4
4
|
def get_coefficent_prefix(demographics: Demographics,
|
|
@@ -65,7 +65,7 @@ def get_coefficent_prefix(demographics: Demographics,
|
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
def apply_coefficients(demographics: Demographics,
|
|
68
|
-
hcc_set:
|
|
68
|
+
hcc_set: Set[str],
|
|
69
69
|
interactions: dict,
|
|
70
70
|
model_name: ModelName,
|
|
71
71
|
coefficients: Dict[Tuple[str, ModelName], float],
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
from typing import Union, Optional
|
|
2
2
|
from hccinfhir.datamodels import Demographics, PrefixOverride
|
|
3
|
+
from hccinfhir.constants import (
|
|
4
|
+
FULL_BENEFIT_DUAL_CODES,
|
|
5
|
+
PARTIAL_BENEFIT_DUAL_CODES,
|
|
6
|
+
OREC_ESRD_CODES,
|
|
7
|
+
CREC_ESRD_CODES,
|
|
8
|
+
ESRD_PREFIXES,
|
|
9
|
+
NEW_ENROLLEE_PREFIXES,
|
|
10
|
+
COMMUNITY_PREFIXES,
|
|
11
|
+
INSTITUTIONAL_PREFIXES,
|
|
12
|
+
FULL_BENEFIT_DUAL_PREFIXES,
|
|
13
|
+
PARTIAL_BENEFIT_DUAL_PREFIXES,
|
|
14
|
+
NON_DUAL_PREFIXES,
|
|
15
|
+
)
|
|
3
16
|
|
|
4
17
|
def categorize_demographics(age: Union[int, float],
|
|
5
18
|
sex: str,
|
|
@@ -75,56 +88,40 @@ def categorize_demographics(age: Union[int, float],
|
|
|
75
88
|
disabled = age < 65 and (orec is not None and orec != "0")
|
|
76
89
|
orig_disabled = (orec is not None and orec == '1') and not disabled
|
|
77
90
|
|
|
78
|
-
# Reference: https://resdac.org/cms-data/variables/medicare-medicaid-dual-eligibility-code-january
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
# Partial benefit dual codes
|
|
83
|
-
pbd_codes = {'01', '03', '05', '06'}
|
|
84
|
-
|
|
85
|
-
is_fbd = dual_elgbl_cd in fbd_codes
|
|
86
|
-
is_pbd = dual_elgbl_cd in pbd_codes
|
|
91
|
+
# Reference: https://resdac.org/cms-data/variables/medicare-medicaid-dual-eligibility-code-january
|
|
92
|
+
is_fbd = dual_elgbl_cd in FULL_BENEFIT_DUAL_CODES
|
|
93
|
+
is_pbd = dual_elgbl_cd in PARTIAL_BENEFIT_DUAL_CODES
|
|
87
94
|
|
|
88
|
-
|
|
89
|
-
|
|
95
|
+
# ESRD detection from OREC/CREC (CMS official codes: 2=ESRD, 3=DIB+ESRD)
|
|
96
|
+
esrd_orec = orec in OREC_ESRD_CODES
|
|
97
|
+
esrd_crec = crec in CREC_ESRD_CODES if crec else False
|
|
90
98
|
esrd = esrd_orec or esrd_crec
|
|
91
99
|
|
|
92
100
|
# Override demographics based on prefix_override
|
|
93
101
|
if prefix_override:
|
|
94
|
-
# ESRD model prefixes
|
|
95
|
-
esrd_prefixes = {'DI_', 'DNE_', 'GI_', 'GNE_', 'GFPA_', 'GFPN_', 'GNPA_', 'GNPN_'}
|
|
96
|
-
# CMS-HCC new enrollee prefixes
|
|
97
|
-
new_enrollee_prefixes = {'NE_', 'SNPNE_', 'DNE_', 'GNE_'}
|
|
98
|
-
# CMS-HCC community prefixes
|
|
99
|
-
community_prefixes = {'CNA_', 'CND_', 'CFA_', 'CFD_', 'CPA_', 'CPD_'}
|
|
100
|
-
# Institutionalized prefix
|
|
101
|
-
institutional_prefixes = {'INS_', 'GI_'}
|
|
102
|
-
|
|
103
|
-
# TODO: RxHCC prefixes
|
|
104
|
-
|
|
105
102
|
# Set esrd flag
|
|
106
|
-
if prefix_override in
|
|
103
|
+
if prefix_override in ESRD_PREFIXES:
|
|
107
104
|
esrd = True
|
|
108
105
|
|
|
109
106
|
# Set new_enrollee flag
|
|
110
|
-
if prefix_override in
|
|
107
|
+
if prefix_override in NEW_ENROLLEE_PREFIXES:
|
|
111
108
|
new_enrollee = True
|
|
112
|
-
elif prefix_override in
|
|
109
|
+
elif prefix_override in COMMUNITY_PREFIXES or prefix_override in INSTITUTIONAL_PREFIXES:
|
|
113
110
|
new_enrollee = False
|
|
114
111
|
|
|
115
112
|
# Set dual eligibility flags based on prefix
|
|
116
|
-
if prefix_override in
|
|
113
|
+
if prefix_override in FULL_BENEFIT_DUAL_PREFIXES:
|
|
117
114
|
is_fbd = True
|
|
118
115
|
is_pbd = False
|
|
119
|
-
elif prefix_override in
|
|
116
|
+
elif prefix_override in PARTIAL_BENEFIT_DUAL_PREFIXES:
|
|
120
117
|
is_fbd = False
|
|
121
118
|
is_pbd = True
|
|
122
|
-
elif prefix_override in
|
|
119
|
+
elif prefix_override in NON_DUAL_PREFIXES:
|
|
123
120
|
is_fbd = False
|
|
124
121
|
is_pbd = False
|
|
125
122
|
|
|
126
123
|
# Set lti flag based on prefix
|
|
127
|
-
if prefix_override in
|
|
124
|
+
if prefix_override in INSTITUTIONAL_PREFIXES:
|
|
128
125
|
lti = True
|
|
129
126
|
|
|
130
127
|
result_dict = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from hccinfhir.datamodels import Demographics, ModelName
|
|
2
|
-
from typing import Optional
|
|
2
|
+
from typing import Optional, List, Set, Dict
|
|
3
3
|
|
|
4
|
-
def has_any_hcc(hcc_list:
|
|
4
|
+
def has_any_hcc(hcc_list: List[str], hcc_set: Set[str]) -> int:
|
|
5
5
|
"""Returns 1 if any HCC in the list is present, 0 otherwise"""
|
|
6
6
|
return int(bool(set(hcc_list) & hcc_set))
|
|
7
7
|
|
|
@@ -81,7 +81,7 @@ def create_dual_interactions(demographics: Demographics) -> dict:
|
|
|
81
81
|
|
|
82
82
|
return interactions
|
|
83
83
|
|
|
84
|
-
def create_hcc_counts(hcc_set:
|
|
84
|
+
def create_hcc_counts(hcc_set: Set[str]) -> Dict:
|
|
85
85
|
"""Creates HCC count variables"""
|
|
86
86
|
counts = {}
|
|
87
87
|
hcc_count = len(hcc_set)
|
|
@@ -95,7 +95,7 @@ def create_hcc_counts(hcc_set: set[str]) -> dict:
|
|
|
95
95
|
|
|
96
96
|
return counts
|
|
97
97
|
|
|
98
|
-
def get_diagnostic_categories(model_name: ModelName, hcc_set:
|
|
98
|
+
def get_diagnostic_categories(model_name: ModelName, hcc_set: Set[str]) -> Dict:
|
|
99
99
|
"""Creates disease categories based on model version"""
|
|
100
100
|
categories = {}
|
|
101
101
|
|
|
@@ -343,9 +343,9 @@ def create_disease_interactions(model_name: ModelName,
|
|
|
343
343
|
|
|
344
344
|
return interactions
|
|
345
345
|
|
|
346
|
-
def apply_interactions(demographics: Demographics,
|
|
347
|
-
hcc_set:
|
|
348
|
-
model_name: ModelName = "CMS-HCC Model V28") ->
|
|
346
|
+
def apply_interactions(demographics: Demographics,
|
|
347
|
+
hcc_set: Set[str],
|
|
348
|
+
model_name: ModelName = "CMS-HCC Model V28") -> Dict:
|
|
349
349
|
"""
|
|
350
350
|
Calculate HCC interactions across CMS models. Handles CMS-HCC, ESRD, and RxHCC models.
|
|
351
351
|
"""
|
|
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
|