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 +63 -1
- hccinfhir/extractor_834.py +530 -0
- hccinfhir/extractor_837.py +114 -58
- hccinfhir/hccinfhir.py +1 -3
- hccinfhir/model_calculate.py +2 -2
- hccinfhir/sample_files/sample_834_01.txt +1 -0
- {hccinfhir-0.1.7.dist-info → hccinfhir-0.1.9.dist-info}/METADATA +1 -1
- {hccinfhir-0.1.7.dist-info → hccinfhir-0.1.9.dist-info}/RECORD +10 -8
- {hccinfhir-0.1.7.dist-info → hccinfhir-0.1.9.dist-info}/WHEEL +0 -0
- {hccinfhir-0.1.7.dist-info → hccinfhir-0.1.9.dist-info}/licenses/LICENSE +0 -0
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)
|
hccinfhir/extractor_837.py
CHANGED
|
@@ -7,17 +7,23 @@ CLAIM_TYPES = {
|
|
|
7
7
|
"005010X223A2": "837I" # Institutional
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
class
|
|
11
|
-
"""
|
|
12
|
-
claim_id: Optional[str] = None
|
|
13
|
-
patient_id: Optional[str] = None
|
|
14
|
-
performing_provider_npi: Optional[str] = None
|
|
10
|
+
class HierarchyContext(BaseModel):
|
|
11
|
+
"""Tracks the current position in the 837 hierarchy"""
|
|
15
12
|
billing_provider_npi: Optional[str] = None
|
|
16
|
-
|
|
13
|
+
subscriber_patient_id: Optional[str] = None
|
|
14
|
+
patient_patient_id: Optional[str] = None
|
|
15
|
+
current_hl_level: Optional[str] = None
|
|
16
|
+
current_hl_id: Optional[str] = None
|
|
17
|
+
|
|
18
|
+
class ClaimContext(BaseModel):
|
|
19
|
+
"""Claim-level data that resets for each CLM segment"""
|
|
20
|
+
claim_id: Optional[str] = None
|
|
21
|
+
dx_lookup: Dict[str, str] = {}
|
|
17
22
|
facility_type: Optional[str] = None
|
|
18
23
|
service_type: Optional[str] = None
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
performing_provider_npi: Optional[str] = None
|
|
25
|
+
provider_specialty: Optional[str] = None
|
|
26
|
+
last_nm1_qualifier: Optional[str] = None
|
|
21
27
|
|
|
22
28
|
def parse_date(date_str: str) -> Optional[str]:
|
|
23
29
|
"""Convert 8-digit date string to ISO format YYYY-MM-DD"""
|
|
@@ -146,11 +152,16 @@ def parse_837_claim_to_sld(segments: List[List[str]], claim_type: str) -> List[S
|
|
|
146
152
|
├── Service Line 2 (2400)
|
|
147
153
|
└── Service Line N (2400)
|
|
148
154
|
|
|
155
|
+
Properly handles multiple loops at each hierarchy level:
|
|
156
|
+
- Multiple Billing Providers (2000A)
|
|
157
|
+
- Multiple Subscribers per Billing Provider (2000B)
|
|
158
|
+
- Multiple Patients per Subscriber (2000C)
|
|
159
|
+
- Multiple Claims per Patient/Subscriber (2300)
|
|
160
|
+
- Multiple Service Lines per Claim (2400)
|
|
149
161
|
"""
|
|
150
162
|
slds = []
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
claim_control_number = None
|
|
163
|
+
hierarchy = HierarchyContext()
|
|
164
|
+
claim = ClaimContext()
|
|
154
165
|
|
|
155
166
|
for i, segment in enumerate(segments):
|
|
156
167
|
if len(segment) < 2:
|
|
@@ -158,46 +169,89 @@ def parse_837_claim_to_sld(segments: List[List[str]], claim_type: str) -> List[S
|
|
|
158
169
|
|
|
159
170
|
seg_id = segment[0]
|
|
160
171
|
|
|
161
|
-
#
|
|
162
|
-
if seg_id == '
|
|
163
|
-
|
|
172
|
+
# ===== HIERARCHY LEVEL TRACKING (HL segments) =====
|
|
173
|
+
if seg_id == 'HL' and len(segment) >= 4:
|
|
174
|
+
hl_id = segment[1]
|
|
175
|
+
parent_id = segment[2] if segment[2] else None
|
|
176
|
+
level_code = segment[3]
|
|
177
|
+
|
|
178
|
+
hierarchy.current_hl_id = hl_id
|
|
179
|
+
hierarchy.current_hl_level = level_code
|
|
180
|
+
|
|
181
|
+
if level_code == '20': # New Billing Provider
|
|
182
|
+
hierarchy.billing_provider_npi = None
|
|
183
|
+
hierarchy.subscriber_patient_id = None
|
|
184
|
+
hierarchy.patient_patient_id = None
|
|
185
|
+
claim = ClaimContext()
|
|
186
|
+
|
|
187
|
+
elif level_code == '22': # New Subscriber
|
|
188
|
+
hierarchy.subscriber_patient_id = None
|
|
189
|
+
hierarchy.patient_patient_id = None
|
|
190
|
+
claim = ClaimContext()
|
|
191
|
+
|
|
192
|
+
elif level_code == '23': # New Patient
|
|
193
|
+
hierarchy.patient_patient_id = None
|
|
194
|
+
claim = ClaimContext()
|
|
164
195
|
|
|
196
|
+
# ===== NAME/IDENTIFICATION (NM1 segments) =====
|
|
165
197
|
elif seg_id == 'NM1' and len(segment) > 1:
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
in_rendering_provider_loop = False
|
|
169
|
-
elif segment[1] == '82' and len(segment) > 8 and segment[8] == 'XX': # Rendering Provider
|
|
170
|
-
current_data.performing_provider_npi = get_segment_value(segment, 9)
|
|
171
|
-
in_rendering_provider_loop = True
|
|
172
|
-
elif segment[1] == '85' and len(segment) > 8 and segment[8] == 'XX': # Billing Provider
|
|
173
|
-
current_data.billing_provider_npi = get_segment_value(segment, 9)
|
|
174
|
-
|
|
175
|
-
# Process Provider Specialty
|
|
176
|
-
elif seg_id == 'PRV' and len(segment) > 1 and segment[1] == 'PE' and in_rendering_provider_loop:
|
|
177
|
-
current_data.provider_specialty = get_segment_value(segment, 3)
|
|
198
|
+
qualifier = segment[1]
|
|
199
|
+
claim.last_nm1_qualifier = qualifier
|
|
178
200
|
|
|
179
|
-
|
|
201
|
+
# Billing Provider (2010AA in 2000A)
|
|
202
|
+
if qualifier == '85' and len(segment) > 8 and segment[8] == 'XX':
|
|
203
|
+
hierarchy.billing_provider_npi = get_segment_value(segment, 9)
|
|
204
|
+
|
|
205
|
+
# Subscriber or Patient (2010BA in 2000B)
|
|
206
|
+
elif qualifier == 'IL':
|
|
207
|
+
patient_id = get_segment_value(segment, 9)
|
|
208
|
+
if hierarchy.current_hl_level == '22': # Subscriber level
|
|
209
|
+
hierarchy.subscriber_patient_id = patient_id
|
|
210
|
+
hierarchy.patient_patient_id = None
|
|
211
|
+
elif hierarchy.current_hl_level == '23': # Patient level
|
|
212
|
+
hierarchy.patient_patient_id = patient_id
|
|
213
|
+
else:
|
|
214
|
+
# Fallback: assume subscriber
|
|
215
|
+
hierarchy.subscriber_patient_id = patient_id
|
|
216
|
+
|
|
217
|
+
# Patient (2010CA in 2000C)
|
|
218
|
+
elif qualifier == 'QC':
|
|
219
|
+
hierarchy.patient_patient_id = get_segment_value(segment, 9)
|
|
220
|
+
|
|
221
|
+
# Performing/Rendering Provider (2310D in 2300)
|
|
222
|
+
elif qualifier == '82' and len(segment) > 8 and segment[8] == 'XX':
|
|
223
|
+
claim.performing_provider_npi = get_segment_value(segment, 9)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ===== PROVIDER SPECIALTY (PRV segment) =====
|
|
227
|
+
elif seg_id == 'PRV' and len(segment) > 3 and segment[1] == 'PE':
|
|
228
|
+
# Only apply if last NM1 was performing provider (82)
|
|
229
|
+
if claim.last_nm1_qualifier == '82':
|
|
230
|
+
claim.provider_specialty = get_segment_value(segment, 3)
|
|
231
|
+
|
|
232
|
+
# ===== CLAIM LEVEL (CLM segment - starts 2300 loop) =====
|
|
180
233
|
elif seg_id == 'CLM':
|
|
181
|
-
|
|
182
|
-
|
|
234
|
+
claim = ClaimContext()
|
|
235
|
+
claim.claim_id = segment[1] if len(segment) > 1 else None
|
|
183
236
|
|
|
184
237
|
# Parse facility and service type for institutional claims
|
|
185
238
|
if claim_type == "837I" and len(segment) > 5 and segment[5] and ':' in segment[5]:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
#
|
|
190
|
-
elif seg_id == 'HI':
|
|
239
|
+
claim.facility_type = segment[5][0] if segment[5] else None
|
|
240
|
+
claim.service_type = segment[5][1] if len(segment[5]) > 1 else None
|
|
241
|
+
|
|
242
|
+
# ===== DIAGNOSIS CODES (HI segment) =====
|
|
243
|
+
elif seg_id == 'HI':
|
|
191
244
|
# In 837I, there can be multiple HI segments in the claim
|
|
192
245
|
# Also, in 837I, diagnosis position does not matter
|
|
193
246
|
# We will use continuous numbering for diagnosis codes
|
|
194
247
|
# use the last dx_lookup position as the starting position, and update
|
|
195
248
|
hi_segment = parse_diagnosis_codes(segment)
|
|
249
|
+
# Re-index for multiple HI segments in same claim
|
|
196
250
|
hi_segment_realigned = {
|
|
197
|
-
str(int(pos) + len(
|
|
251
|
+
str(int(pos) + len(claim.dx_lookup)): code
|
|
198
252
|
for pos, code in hi_segment.items()
|
|
199
253
|
}
|
|
200
|
-
|
|
254
|
+
claim.dx_lookup.update(hi_segment_realigned)
|
|
201
255
|
|
|
202
256
|
# Process Service Lines
|
|
203
257
|
#
|
|
@@ -217,54 +271,56 @@ def parse_837_claim_to_sld(segments: List[List[str]], claim_type: str) -> List[S
|
|
|
217
271
|
# SV205 (Required) - Unit Count: Format 9999999.999 (whole numbers only - fractional quantities not recognized)
|
|
218
272
|
# NOTE: Diagnosis Code Pointer is not supported for SV2
|
|
219
273
|
#
|
|
274
|
+
# ===== SERVICE LINE (SV1/SV2 segments - 2400 loop) =====
|
|
220
275
|
elif seg_id in ['SV1', 'SV2']:
|
|
221
|
-
|
|
222
276
|
linked_diagnoses = []
|
|
223
277
|
|
|
224
278
|
if seg_id == 'SV1':
|
|
225
|
-
# SV1 Professional Service
|
|
279
|
+
# SV1 Professional Service
|
|
226
280
|
proc_info = get_segment_value(segment, 1, '').split(':')
|
|
227
281
|
procedure_code = proc_info[1] if len(proc_info) > 1 else None
|
|
228
282
|
modifiers = proc_info[2:] if len(proc_info) > 2 else []
|
|
229
283
|
quantity = parse_amount(get_segment_value(segment, 4))
|
|
230
284
|
place_of_service = get_segment_value(segment, 5)
|
|
231
|
-
|
|
285
|
+
|
|
286
|
+
# Get diagnosis pointers and resolve to actual codes
|
|
232
287
|
dx_pointers = get_segment_value(segment, 7, '')
|
|
233
288
|
linked_diagnoses = [
|
|
234
|
-
|
|
289
|
+
claim.dx_lookup[pointer]
|
|
235
290
|
for pointer in (dx_pointers.split(':') if dx_pointers else [])
|
|
236
|
-
if pointer in
|
|
291
|
+
if pointer in claim.dx_lookup
|
|
237
292
|
]
|
|
238
293
|
else:
|
|
239
|
-
# SV2 Institutional Service
|
|
240
|
-
# Revenue code in SV201
|
|
294
|
+
# SV2 Institutional Service
|
|
241
295
|
revenue_code = get_segment_value(segment, 1)
|
|
242
|
-
# Procedure code in SV202
|
|
243
296
|
proc_info = get_segment_value(segment, 2, '').split(':')
|
|
244
297
|
procedure_code = proc_info[1] if len(proc_info) > 1 else None
|
|
245
298
|
modifiers = proc_info[2:] if len(proc_info) > 2 else []
|
|
246
|
-
# Quantity in SV205
|
|
247
299
|
quantity = parse_amount(get_segment_value(segment, 5))
|
|
248
|
-
place_of_service = None
|
|
249
|
-
# linked diagnoses are not supported for SV2
|
|
250
|
-
|
|
300
|
+
place_of_service = None
|
|
251
301
|
|
|
252
|
-
# Get service line details
|
|
302
|
+
# Get service line details (NDC, dates) - lookback from current segment index
|
|
253
303
|
ndc, service_date = process_service_line(segments, i)
|
|
254
304
|
|
|
305
|
+
# Determine effective patient ID (prefer patient level, fallback to subscriber)
|
|
306
|
+
effective_patient_id = (
|
|
307
|
+
hierarchy.patient_patient_id or
|
|
308
|
+
hierarchy.subscriber_patient_id
|
|
309
|
+
)
|
|
310
|
+
|
|
255
311
|
# Create service level data
|
|
256
312
|
service_data = ServiceLevelData(
|
|
257
|
-
claim_id=
|
|
313
|
+
claim_id=claim.claim_id,
|
|
258
314
|
procedure_code=procedure_code,
|
|
259
315
|
linked_diagnosis_codes=linked_diagnoses,
|
|
260
|
-
claim_diagnosis_codes=list(
|
|
261
|
-
claim_type=
|
|
262
|
-
provider_specialty=
|
|
263
|
-
performing_provider_npi=
|
|
264
|
-
billing_provider_npi=
|
|
265
|
-
patient_id=
|
|
266
|
-
facility_type=
|
|
267
|
-
service_type=
|
|
316
|
+
claim_diagnosis_codes=list(claim.dx_lookup.values()),
|
|
317
|
+
claim_type=claim_type,
|
|
318
|
+
provider_specialty=claim.provider_specialty,
|
|
319
|
+
performing_provider_npi=claim.performing_provider_npi, # ✅ Correct field
|
|
320
|
+
billing_provider_npi=hierarchy.billing_provider_npi,
|
|
321
|
+
patient_id=effective_patient_id,
|
|
322
|
+
facility_type=claim.facility_type,
|
|
323
|
+
service_type=claim.service_type,
|
|
268
324
|
service_date=service_date,
|
|
269
325
|
place_of_service=place_of_service,
|
|
270
326
|
quantity=quantity,
|
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
|
-
|
|
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
|
hccinfhir/model_calculate.py
CHANGED
|
@@ -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,11 +1,12 @@
|
|
|
1
1
|
hccinfhir/__init__.py,sha256=G_5m6jm3_BK5NdcZWoi0NEKJEsE_LjAU1RaLaL9xNPU,1043
|
|
2
|
-
hccinfhir/datamodels.py,sha256=
|
|
2
|
+
hccinfhir/datamodels.py,sha256=lHDXkCpzafuzF094VoaQXl4MgeFPxPfi25QJxXj4YHk,10010
|
|
3
3
|
hccinfhir/extractor.py,sha256=xL9c2VT-e2I7_c8N8j4Og42UEgVuCzyn9WFp3ntM5Ro,1822
|
|
4
|
-
hccinfhir/
|
|
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=
|
|
8
|
-
hccinfhir/model_calculate.py,sha256=
|
|
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.
|
|
48
|
-
hccinfhir-0.1.
|
|
49
|
-
hccinfhir-0.1.
|
|
50
|
-
hccinfhir-0.1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|