hccinfhir 0.0.1__py3-none-any.whl → 0.0.2__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/extractor.py CHANGED
@@ -1,122 +1,51 @@
1
- from pydantic import BaseModel, ConfigDict
2
- from typing import List, Optional, Literal
3
- from datetime import date
4
-
5
- SYSTEMS = {
6
- 'dx': 'http://hl7.org/fhir/sid/icd-10-cm',
7
- 'pr': 'https://bluebutton.cms.gov/resources/codesystem/hcpcs',
8
- 'specialty': 'https://bluebutton.cms.gov/resources/variables/prvdr_spclty',
9
- 'role': 'http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBClaimCareTeamRole',
10
- 'claim_type': 'https://bluebutton.cms.gov/resources/variables/nch_clm_type_cd'
11
- }
12
-
13
- # Models
14
- class Coding(BaseModel):
15
- system: Optional[str] = None
16
- code: Optional[str] = None
17
- display: Optional[str] = None
18
-
19
- class CodeableConcept(BaseModel):
20
- coding: Optional[List[Coding]] = []
21
-
22
- class Period(BaseModel):
23
- start: Optional[date] = None
24
- end: Optional[date] = None
25
-
26
- class Diagnosis(BaseModel):
27
- sequence: Optional[int] = None
28
- diagnosisCodeableConcept: Optional[CodeableConcept] = None
29
-
30
- class CareTeamMember(BaseModel):
31
- role: Optional[CodeableConcept] = None
32
- qualification: Optional[CodeableConcept] = None
33
-
34
- class EoBItem(BaseModel):
35
- productOrService: Optional[CodeableConcept] = None
36
- diagnosisSequence: Optional[List[int]] = None
37
- servicedPeriod: Optional[Period] = None
38
-
39
- class ExplanationOfBenefit(BaseModel):
40
- model_config = ConfigDict(frozen=True)
41
- resourceType: Literal["ExplanationOfBenefit"] = "ExplanationOfBenefit"
42
- type: Optional[CodeableConcept] = None
43
- diagnosis: Optional[List[Diagnosis]] = []
44
- item: Optional[List[EoBItem]] = []
45
- careTeam: Optional[List[CareTeamMember]] = []
46
- billablePeriod: Optional[Period] = None
47
-
48
- def extract_mde(eob_data: dict) -> List[dict]:
49
- """Extract medical data elements from FHIR ExplanationOfBenefit data."""
50
- try:
51
- eob = ExplanationOfBenefit.model_validate(eob_data)
52
-
53
- # Get codes from CodeableConcept
54
- get_code = lambda concept, system: next((
55
- c.code for c in (concept.coding or [])
56
- if c and c.system == system and c.code
57
- ), None)
58
-
59
- # Get date from Period
60
- get_date = lambda period: (
61
- period.end.isoformat() if period and period.end
62
- else period.start.isoformat() if period and period.start
63
- else None
64
- )
1
+ from typing import Union, List, Literal
2
+ from .models import ServiceLevelData
3
+ from .extractor_837 import extract_sld_837
4
+ from .extractor_fhir import extract_sld_fhir
5
+
6
+ def extract_sld(
7
+ data: Union[str, dict],
8
+ format: Literal["837", "fhir"] = "fhir"
9
+ ) -> List[ServiceLevelData]:
10
+ """
11
+ Unified entry point for SLD extraction with explicit format specification
12
+
13
+ Args:
14
+ data: Input data - string for 837, dict for FHIR
15
+ format: Data format - either "837" or "fhir"
65
16
 
66
- # Create diagnosis lookup
67
- dx_lookup = {
68
- d.sequence: get_code(d.diagnosisCodeableConcept, SYSTEMS['dx'])
69
- for d in (eob.diagnosis or [])
70
- if d.sequence is not None and d.diagnosisCodeableConcept
71
- }
17
+ Returns:
18
+ List of ServiceLevelData
72
19
 
73
- # Get claim-level data
74
- claim_type = get_code(eob.type, SYSTEMS['claim_type'])
20
+ Raises:
21
+ ValueError: If format and data type don't match or format is invalid
22
+ TypeError: If data is None or wrong type
23
+ """
24
+ if data is None:
25
+ raise TypeError("Input data cannot be None")
75
26
 
76
- # Get provider specialty
77
- specialty = next((
78
- get_code(m.qualification, SYSTEMS['specialty'])
79
- for m in (eob.careTeam or [])
80
- if m.role and m.qualification
81
- and get_code(m.role, SYSTEMS['role']) in {'performing', 'rendering'}
82
- ), None)
83
-
84
- results = []
85
- for item in (eob.item or []):
86
- if not item.productOrService:
87
- continue
88
-
89
- pr_code = get_code(item.productOrService, SYSTEMS['pr'])
90
- if not pr_code:
91
- continue
92
-
93
- # Get associated data
94
- dos = get_date(item.servicedPeriod) or get_date(eob.billablePeriod)
95
- dx_codes = [dx_lookup[seq] for seq in (item.diagnosisSequence or [])
96
- if seq in dx_lookup]
97
-
98
- # Build result
99
- result = {k: v for k, v in {
100
- 'procedure_code': pr_code,
101
- 'diagnosis_codes': dx_codes,
102
- 'claim_type': claim_type,
103
- 'provider_specialty': specialty,
104
- 'service_date': dos
105
- }.items()}
106
-
107
- results.append(result)
108
-
109
- return results
110
-
111
- except Exception as e:
112
- raise ValueError(f"Error processing EOB: {str(e)}")
27
+ if format == "837":
28
+ if not isinstance(data, str) or data == "":
29
+ raise TypeError(f"837 format requires string input, got {type(data)}")
30
+ return extract_sld_837(data)
31
+ elif format == "fhir":
32
+ if not isinstance(data, dict) or data == {}:
33
+ raise TypeError(f"FHIR format requires dict input, got {type(data)}")
34
+ return extract_sld_fhir(data)
35
+ else:
36
+ raise ValueError(f'Format must be either "837" or "fhir", got {format}')
37
+
38
+
39
+ def extract_sld_list(data: Union[List[str], List[dict]], format: Literal["837", "fhir"] = "fhir") -> List[ServiceLevelData]:
40
+ """Extract SLDs from a list of FHIR EOBs"""
41
+ output = []
42
+ for item in data:
113
43
 
114
- def extract_mde_list(eob_data_list: List[dict]) -> List[dict]:
115
- """Process a list of EOB data dictionaries."""
116
- results = []
117
- for eob_data in eob_data_list:
118
44
  try:
119
- results.extend(extract_mde(eob_data))
45
+ output.extend(extract_sld(item, format))
46
+ except TypeError as e:
47
+ print(f"Warning: Skipping invalid types: {str(e)}")
120
48
  except ValueError as e:
121
- print(f"Warning: Skipping invalid EOB: {str(e)}")
122
- return results
49
+ print(f"Warning: Skipping invalid values: {str(e)}")
50
+ return output
51
+
@@ -0,0 +1,175 @@
1
+ from typing import List, Optional, Dict
2
+ from pydantic import BaseModel
3
+ from datetime import date
4
+ from .models import ServiceLevelData
5
+
6
+ CLAIM_TYPES = {
7
+ "005010X222A1": "837P", # Professional
8
+ "005010X223A2": "837I" # Institutional
9
+ }
10
+
11
+ class ClaimData(BaseModel):
12
+ """Container for claim-level data"""
13
+ claim_id: Optional[str] = None
14
+ patient_id: Optional[str] = None
15
+ performing_provider_npi: Optional[str] = None
16
+ billing_provider_npi: Optional[str] = None
17
+ provider_specialty: Optional[str] = None
18
+ facility_type: Optional[str] = None
19
+ service_type: Optional[str] = None
20
+ claim_type: str
21
+ dx_lookup: Dict[str, str] = {}
22
+
23
+ def parse_date(date_str: str) -> Optional[str]:
24
+ """Convert 8-digit date string to ISO format YYYY-MM-DD"""
25
+ if not isinstance(date_str, str) or len(date_str) != 8:
26
+ return None
27
+ try:
28
+ year, month, day = int(date_str[:4]), int(date_str[4:6]), int(date_str[6:8])
29
+ if not (1900 <= year <= 2100 and 1 <= month <= 12 and 1 <= day <= 31):
30
+ return None
31
+ return f"{year:04d}-{month:02d}-{day:02d}"
32
+ except ValueError:
33
+ return None
34
+
35
+ def parse_amount(amount_str: str) -> Optional[float]:
36
+ """Convert string to float, return None if invalid"""
37
+ try:
38
+ return float(amount_str)
39
+ except (ValueError, TypeError):
40
+ return None
41
+
42
+ def get_segment_value(segment: List[str], index: int) -> Optional[str]:
43
+ """Safely get value from segment at given index"""
44
+ return segment[index] if len(segment) > index else None
45
+
46
+ def parse_diagnosis_codes(segment: List[str]) -> Dict[str, str]:
47
+ """Extract diagnosis codes from HI segment"""
48
+ dx_lookup = {}
49
+ for pos, element in enumerate(segment[1:], 1):
50
+ if ':' not in element:
51
+ continue
52
+ qualifier, code = element.split(':')[:2]
53
+ if qualifier in ['ABK', 'ABF']: # ICD-10 qualifiers
54
+ dx_lookup[str(pos)] = code
55
+ return dx_lookup
56
+
57
+ def process_service_line(segments: List[List[str]], start_index: int) -> tuple[Optional[str], Optional[str]]:
58
+ """Extract NDC and service date from service line segments"""
59
+ ndc = None
60
+ service_date = None
61
+
62
+ for seg in segments[start_index:]:
63
+ if seg[0] in ['LX', 'CLM', 'SE']:
64
+ break
65
+ if seg[0] == 'LIN' and len(seg) > 3 and seg[2] == 'N4':
66
+ ndc = seg[3]
67
+ elif seg[0] == 'DTP' and seg[1] == '472':
68
+ service_date = parse_date(seg[3])
69
+ if ndc and service_date:
70
+ break
71
+
72
+ return ndc, service_date
73
+
74
+ def extract_sld_837(content: str) -> List[ServiceLevelData]:
75
+ """Extract service level data from 837 Professional or Institutional claims"""
76
+ if not content:
77
+ raise ValueError("Input X12 data cannot be empty")
78
+
79
+ # Split content into segments
80
+ segments = [seg.strip().split('*') for seg in content.split('~') if seg.strip()]
81
+
82
+ # Detect claim type from GS segment
83
+ claim_type = None
84
+ for segment in segments:
85
+ if segment[0] == 'GS' and len(segment) > 8:
86
+ claim_type = CLAIM_TYPES.get(segment[8])
87
+ break
88
+
89
+ if not claim_type:
90
+ raise ValueError("Invalid or unsupported 837 format")
91
+
92
+ encounters = []
93
+ current_data = ClaimData(claim_type=claim_type)
94
+ in_claim_loop = False
95
+ in_rendering_provider_loop = False
96
+
97
+ for i, segment in enumerate(segments):
98
+ if len(segment) < 2:
99
+ continue
100
+
101
+ seg_id = segment[0]
102
+
103
+ # Process NM1 segments (Provider and Patient info)
104
+ if seg_id == 'NM1':
105
+ if segment[1] == 'IL': # Subscriber/Patient
106
+ current_data.patient_id = get_segment_value(segment, 9)
107
+ in_claim_loop = False
108
+ in_rendering_provider_loop = False
109
+ elif segment[1] == '82' and len(segment) > 8 and segment[8] == 'XX': # Rendering Provider
110
+ current_data.performing_provider_npi = get_segment_value(segment, 9)
111
+ in_rendering_provider_loop = True
112
+ elif segment[1] == '85' and len(segment) > 8 and segment[8] == 'XX': # Billing Provider
113
+ current_data.billing_provider_npi = get_segment_value(segment, 9)
114
+
115
+ # Process Provider Specialty
116
+ elif seg_id == 'PRV' and segment[1] == 'PE' and in_rendering_provider_loop:
117
+ current_data.provider_specialty = get_segment_value(segment, 3)
118
+
119
+ # Process Claim Information
120
+ elif seg_id == 'CLM':
121
+ in_claim_loop = True
122
+ in_rendering_provider_loop = False
123
+ current_data.claim_id = segment[1] if len(segment) > 1 else None
124
+
125
+ # Parse facility and service type for institutional claims
126
+ if claim_type == "837I" and len(segment) > 5 and ':' in segment[5]:
127
+ current_data.facility_type = segment[5][0]
128
+ current_data.service_type = segment[5][1] if len(segment[5]) > 1 else None
129
+
130
+ # Process Diagnosis Codes
131
+ elif seg_id == 'HI' and in_claim_loop:
132
+ current_data.dx_lookup = parse_diagnosis_codes(segment)
133
+
134
+ # Process Service Lines
135
+ elif seg_id in ['SV1', 'SV2'] and in_claim_loop:
136
+ # Parse procedure info
137
+ proc_info = segment[1].split(':')
138
+ procedure_code = proc_info[1] if len(proc_info) > 1 else None
139
+ modifiers = proc_info[2:] if len(proc_info) > 2 else []
140
+
141
+ # Get diagnosis pointers and linked diagnoses
142
+ dx_pointer_pos = 7 if seg_id == 'SV1' else 11
143
+ dx_pointers = get_segment_value(segment, dx_pointer_pos)
144
+ linked_diagnoses = [
145
+ current_data.dx_lookup[pointer]
146
+ for pointer in (dx_pointers.split(',') if dx_pointers else [])
147
+ if pointer in current_data.dx_lookup
148
+ ]
149
+
150
+ # Get service line details
151
+ ndc, service_date = process_service_line(segments, i)
152
+
153
+ # Create service level data
154
+ service_data = ServiceLevelData(
155
+ claim_id=current_data.claim_id,
156
+ procedure_code=procedure_code,
157
+ linked_diagnosis_codes=linked_diagnoses,
158
+ claim_diagnosis_codes=list(current_data.dx_lookup.values()),
159
+ claim_type=current_data.claim_type,
160
+ provider_specialty=current_data.provider_specialty,
161
+ performing_provider_npi=current_data.performing_provider_npi,
162
+ billing_provider_npi=current_data.billing_provider_npi,
163
+ patient_id=current_data.patient_id,
164
+ facility_type=current_data.facility_type,
165
+ service_type=current_data.service_type,
166
+ service_date=service_date,
167
+ place_of_service=get_segment_value(segment, 6) if seg_id == 'SV1' else None,
168
+ quantity=parse_amount(get_segment_value(segment, 4)),
169
+ modifiers=modifiers,
170
+ ndc=ndc,
171
+ allowed_amount=None
172
+ )
173
+ encounters.append(service_data)
174
+
175
+ return encounters
@@ -0,0 +1,193 @@
1
+ from pydantic import BaseModel, ConfigDict, Field, AliasChoices
2
+ from typing import List, Optional, Literal, Dict
3
+ from datetime import date
4
+ from .models import ServiceLevelData
5
+
6
+ SYSTEMS = {
7
+ 'diagnosis': {
8
+ 'icd10cm': 'http://hl7.org/fhir/sid/icd-10-cm',
9
+ 'icd10': 'http://hl7.org/fhir/sid/icd-10'
10
+ },
11
+ 'procedures': {
12
+ 'hcpcs': 'https://bluebutton.cms.gov/resources/codesystem/hcpcs'
13
+ },
14
+ 'identifiers': {
15
+ 'npi': 'http://hl7.org/fhir/sid/us-npi',
16
+ 'ndc': 'http://hl7.org/fhir/sid/ndc'
17
+ },
18
+ 'context': {
19
+ 'specialty': 'https://bluebutton.cms.gov/resources/variables/prvdr_spclty',
20
+ 'role': 'http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBClaimCareTeamRole',
21
+ 'claim_type': 'https://bluebutton.cms.gov/resources/variables/nch_clm_type_cd',
22
+ 'facility': 'https://bluebutton.cms.gov/resources/variables/clm_fac_type_cd',
23
+ 'service': 'https://bluebutton.cms.gov/resources/variables/clm_srvc_clsfctn_type_cd',
24
+ 'place': 'https://bluebutton.cms.gov/resources/variables/line_place_of_srvc_cd'
25
+ }
26
+ }
27
+
28
+ class Coding(BaseModel):
29
+ system: Optional[str] = None
30
+ code: Optional[str] = None
31
+ display: Optional[str] = None
32
+
33
+ class Extension(BaseModel):
34
+ url: str
35
+ valueCoding: Optional[dict] = None
36
+
37
+ class ExtensionMixin(BaseModel):
38
+ extension: Optional[List[Extension]] = None
39
+
40
+ def get_extension_code(self, system_url: str) -> Optional[str]:
41
+ """Extract code from extensions for a specific system URL"""
42
+ if not self.extension:
43
+ return None
44
+ return next((
45
+ ext.valueCoding.get('code')
46
+ for ext in self.extension
47
+ if ext.url == system_url and ext.valueCoding
48
+ ), None)
49
+
50
+ class CodeableConcept(ExtensionMixin):
51
+ coding: Optional[List[Coding]] = None
52
+
53
+ def get_code(self, system: str) -> Optional[str]:
54
+ """Extract code for a specific coding system"""
55
+ if not self.coding:
56
+ return None
57
+ return next((c.code for c in self.coding if c and c.system == system and c.code), None)
58
+
59
+ class Period(BaseModel):
60
+ start: Optional[date] = None
61
+ end: Optional[date] = None
62
+
63
+ def get_service_date(self) -> Optional[str]:
64
+ """Return the most specific date available"""
65
+ return self.end.isoformat() if self.end else self.start.isoformat() if self.start else None
66
+
67
+ class Diagnosis(BaseModel):
68
+ sequence: int
69
+ diagnosisCodeableConcept: CodeableConcept
70
+
71
+ class CareTeamMember(BaseModel):
72
+ role: CodeableConcept
73
+ qualification: Optional[CodeableConcept] = None
74
+ provider: Optional[dict] = None
75
+
76
+ class EoBItem(BaseModel):
77
+ productOrService: Optional[CodeableConcept] = Field(None, validation_alias=AliasChoices('service', 'productOrService'))
78
+ quantity: Optional[dict] = None
79
+ diagnosisSequence: Optional[List[int]] = None
80
+ servicedPeriod: Optional[Period] = None
81
+ locationCodeableConcept: Optional[CodeableConcept] = None
82
+ modifier: Optional[List[CodeableConcept]] = None
83
+ adjudication: Optional[List[dict]] = None
84
+
85
+ class Facility(ExtensionMixin):
86
+ pass
87
+
88
+ class ExplanationOfBenefit(ExtensionMixin):
89
+ model_config = ConfigDict(frozen=True)
90
+ resourceType: Literal["ExplanationOfBenefit"] = "ExplanationOfBenefit"
91
+ id: Optional[str] = None
92
+ type: Optional[CodeableConcept] = None
93
+ diagnosis: Optional[List[Diagnosis]] = []
94
+ item: Optional[List[EoBItem]] = []
95
+ careTeam: Optional[List[CareTeamMember]] = []
96
+ billablePeriod: Optional[Period] = None
97
+ patient: Optional[dict] = None
98
+ facility: Optional[Facility] = None
99
+ contained: Optional[List[dict]] = None
100
+
101
+ def get_diagnosis_codes(self) -> Dict[int, str]:
102
+ """Extract all diagnosis codes with their sequences"""
103
+ dx_codes = {}
104
+ for dx in self.diagnosis or []:
105
+ code = (dx.diagnosisCodeableConcept.get_code(SYSTEMS['diagnosis']['icd10cm']) or
106
+ dx.diagnosisCodeableConcept.get_code(SYSTEMS['diagnosis']['icd10']))
107
+ if code:
108
+ dx_codes[dx.sequence] = code
109
+ return dx_codes
110
+
111
+ def get_rendering_provider(self) -> Optional[CareTeamMember]:
112
+ """Get the rendering provider from the care team"""
113
+ return next((
114
+ m for m in self.careTeam or []
115
+ if m.role.get_code(SYSTEMS['context']['role']) in {'performing', 'rendering'}
116
+ ), None)
117
+
118
+ def get_billing_npi(self) -> Optional[str]:
119
+ """Extract billing provider NPI from contained resources"""
120
+ return next((
121
+ i.get('value')
122
+ for c in (self.contained or [])
123
+ for i in c.get('identifier', [])
124
+ if i.get('system') == SYSTEMS['identifiers']['npi']
125
+ ), None)
126
+
127
+ def extract_sld_fhir(eob_data: dict) -> List[ServiceLevelData]:
128
+ try:
129
+ eob = ExplanationOfBenefit.model_validate(eob_data)
130
+ dx_lookup = eob.get_diagnosis_codes()
131
+ rendering_provider = eob.get_rendering_provider()
132
+
133
+ common_data = {
134
+ 'claim_id': eob.id,
135
+ 'claim_type': eob.type.get_code(SYSTEMS['context']['claim_type']) if eob.type else None,
136
+ 'provider_specialty': (rendering_provider.qualification.get_code(SYSTEMS['context']['specialty'])
137
+ if rendering_provider and rendering_provider.qualification else None),
138
+ 'performing_provider_npi': (rendering_provider.provider.get('identifier', {}).get('value')
139
+ if rendering_provider else None),
140
+ 'patient_id': eob.patient.get('reference', '').split('/')[-1] if eob.patient else None,
141
+ 'facility_type': (eob.facility.get_extension_code(SYSTEMS['context']['facility'])
142
+ if eob.facility else None),
143
+ 'service_type': (eob.type.get_extension_code(SYSTEMS['context']['service']) or
144
+ eob.type.get_code(SYSTEMS['context']['service']) if eob.type else None),
145
+ 'billing_provider_npi': eob.get_billing_npi()
146
+ }
147
+
148
+ results = []
149
+ for item in eob.item or []:
150
+ if not item.productOrService:
151
+ continue
152
+
153
+ service_data = {
154
+ **common_data,
155
+ 'procedure_code': item.productOrService.get_code(SYSTEMS['procedures']['hcpcs']),
156
+ 'ndc': (item.productOrService.get_code(SYSTEMS['identifiers']['ndc']) or
157
+ item.productOrService.get_extension_code(SYSTEMS['identifiers']['ndc'])),
158
+ 'quantity': item.quantity.get('value') if item.quantity else None,
159
+ 'linked_diagnosis_codes': [dx_lookup[seq] for seq in (item.diagnosisSequence or []) if seq in dx_lookup],
160
+ 'claim_diagnosis_codes': list(dx_lookup.values()),
161
+ 'service_date': (item.servicedPeriod.get_service_date() if item.servicedPeriod else
162
+ eob.billablePeriod.get_service_date() if eob.billablePeriod else None),
163
+ 'place_of_service': item.locationCodeableConcept.get_code(SYSTEMS['context']['place'])
164
+ if item.locationCodeableConcept else None,
165
+ 'modifiers': [m.get_code(SYSTEMS['procedures']['hcpcs'])
166
+ for m in (item.modifier or []) if m],
167
+ 'allowed_amount': next((adj.get('amount', {}).get('value')
168
+ for adj in (item.adjudication or [])
169
+ if any(c.get('code') == 'eligible'
170
+ for c in adj.get('category', {}).get('coding', []))), None)
171
+ }
172
+
173
+ if service_data['procedure_code'] or service_data['ndc']:
174
+ results.append(service_data)
175
+
176
+ if not results:
177
+ results.append({
178
+ **common_data,
179
+ 'linked_diagnosis_codes': [],
180
+ 'claim_diagnosis_codes': list(dx_lookup.values()),
181
+ 'service_date': eob.billablePeriod.get_service_date() if eob.billablePeriod else None,
182
+ 'procedure_code': None,
183
+ 'ndc': None,
184
+ 'quantity': None,
185
+ 'place_of_service': None,
186
+ 'modifiers': [],
187
+ 'allowed_amount': None
188
+ })
189
+
190
+ return [ServiceLevelData.model_validate(r) for r in results]
191
+
192
+ except ValueError as e:
193
+ raise ValueError(f"Error processing EOB: {str(e)}")
hccinfhir/filter.py ADDED
@@ -0,0 +1,43 @@
1
+ from typing import List, Set
2
+ from .models import ServiceLevelData
3
+ import importlib.resources
4
+
5
+ # use import importlib.resources to load the professional_cpt_fn file as a list of strings
6
+ professional_cpt_default_fn = 'ra_eligible_cpt_hcpcs_2023.csv'
7
+ professional_cpt_default = []
8
+ with importlib.resources.open_text('hccinfhir.data', professional_cpt_default_fn) as f:
9
+ professional_cpt_default = set(f.read().splitlines())
10
+
11
+ def apply_filter(
12
+ data: List[ServiceLevelData],
13
+ inpatient_tob: Set[str] = {'11X', '41X'},
14
+ outpatient_tob: Set[str] = {'12X', '13X', '43X', '71X', '73X', '76X', '77X', '85X'},
15
+ professional_cpt: Set[str] = professional_cpt_default
16
+ ) -> List[ServiceLevelData]:
17
+ # tob (Type of Bill) Filter is based on:
18
+ # https://www.hhs.gov/guidance/sites/default/files/hhs-guidance-documents/2012181486-wq-092916_ra_webinar_slides_5cr_092816.pdf
19
+ # https://www.hhs.gov/guidance/sites/default/files/hhs-guidance-documents/final%20industry%20memo%20medicare%20filtering%20logic%2012%2022%2015_85.pdf
20
+
21
+ # Break down the inpatient ToB into facility and service types
22
+ inpatient_facility_types = {tob[0] for tob in inpatient_tob}
23
+ inpatient_service_types = {tob[1] for tob in inpatient_tob}
24
+
25
+ # Break down the outpatient ToB into facility and service types
26
+ outpatient_facility_types = {tob[0] for tob in outpatient_tob}
27
+ outpatient_service_types = {tob[1] for tob in outpatient_tob}
28
+
29
+ # If ServiceLevelData has a facility_type and service_type, then filter the data based on the facility_type and service_type
30
+ # If not, then filter the data based on the CPT code
31
+ filtered_data = []
32
+ for item in data:
33
+ if item.facility_type and item.service_type:
34
+ if item.facility_type in inpatient_facility_types and item.service_type in inpatient_service_types:
35
+ filtered_data.append(item)
36
+ elif (item.facility_type in outpatient_facility_types and
37
+ item.service_type in outpatient_service_types and
38
+ item.procedure_code in professional_cpt):
39
+ filtered_data.append(item)
40
+ else:
41
+ if item.procedure_code in professional_cpt:
42
+ filtered_data.append(item)
43
+ return filtered_data
hccinfhir/models.py ADDED
@@ -0,0 +1,44 @@
1
+ from pydantic import BaseModel
2
+ from typing import List, Optional
3
+
4
+ class ServiceLevelData(BaseModel):
5
+ """
6
+ Represents standardized service-level data extracted from healthcare claims.
7
+
8
+ Attributes:
9
+ claim_id: Unique identifier for the claim
10
+ procedure_code: Healthcare Common Procedure Coding System (HCPCS) code
11
+ ndc: National Drug Code
12
+ linked_diagnosis_codes: ICD-10 diagnosis codes linked to this service
13
+ claim_diagnosis_codes: All diagnosis codes on the claim
14
+ claim_type: Type of claim (e.g., NCH Claim Type Code, or 837I, 837P)
15
+ provider_specialty: Provider taxonomy or specialty code
16
+ performing_provider_npi: National Provider Identifier for performing provider
17
+ billing_provider_npi: National Provider Identifier for billing provider
18
+ patient_id: Unique identifier for the patient
19
+ facility_type: Type of facility where service was rendered
20
+ service_type: Type of service provided (facility type + service type = Type of Bill)
21
+ service_date: Date service was performed (YYYY-MM-DD)
22
+ place_of_service: Place of service code
23
+ quantity: Number of units provided
24
+ quantity_unit: Unit of measure for quantity
25
+ modifiers: List of procedure code modifiers
26
+ allowed_amount: Allowed amount for the service
27
+ """
28
+ claim_id: Optional[str] = None
29
+ procedure_code: Optional[str] = None
30
+ ndc: Optional[str] = None
31
+ linked_diagnosis_codes: List[str] = []
32
+ claim_diagnosis_codes: List[str] = []
33
+ claim_type: Optional[str] = None
34
+ provider_specialty: Optional[str] = None
35
+ performing_provider_npi: Optional[str] = None
36
+ billing_provider_npi: Optional[str] = None
37
+ patient_id: Optional[str] = None
38
+ facility_type: Optional[str] = None
39
+ service_type: Optional[str] = None
40
+ service_date: Optional[str] = None
41
+ place_of_service: Optional[str] = None
42
+ quantity: Optional[float] = None
43
+ modifiers: List[str] = []
44
+ allowed_amount: Optional[float] = None