mphapi 0.1.0__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.
- mphapi/__init__.py +5 -0
- mphapi/claim.py +332 -0
- mphapi/client.py +215 -0
- mphapi/date.py +51 -0
- mphapi/pricing.py +337 -0
- mphapi/response.py +124 -0
- mphapi-0.1.0.dist-info/METADATA +13 -0
- mphapi-0.1.0.dist-info/RECORD +9 -0
- mphapi-0.1.0.dist-info/WHEEL +4 -0
mphapi/__init__.py
ADDED
mphapi/claim.py
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
from enum import Enum, IntEnum
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import AliasGenerator, BaseModel, ConfigDict, Field
|
|
5
|
+
from pydantic.alias_generators import to_camel
|
|
6
|
+
|
|
7
|
+
from .date import Date
|
|
8
|
+
|
|
9
|
+
camel_case_model_config = ConfigDict(
|
|
10
|
+
alias_generator=AliasGenerator(
|
|
11
|
+
validation_alias=to_camel, serialization_alias=to_camel
|
|
12
|
+
),
|
|
13
|
+
populate_by_name=True,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FormType(str, Enum):
|
|
18
|
+
"""Type of form used to submit the claim. Can be HCFA or UB-04 (from CLM05_02)"""
|
|
19
|
+
|
|
20
|
+
HCFA = "HCFA"
|
|
21
|
+
UB_04 = "UB-04"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BillTypeSequence(str, Enum):
|
|
25
|
+
"""Where the claim is at in its billing lifecycle (e.g. 0: Non-Pay, 1: Admit Through
|
|
26
|
+
Discharge, 7: Replacement, etc.) (from CLM05_03)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
NON_PAY = "G"
|
|
30
|
+
ADMIT_THROUGH_DISCHARGE = "H"
|
|
31
|
+
FIRST_INTERIM = "I"
|
|
32
|
+
CONTINUING_INTERIM = "J"
|
|
33
|
+
LAST_INTERIM = "K"
|
|
34
|
+
LATE_CHARGE = "M"
|
|
35
|
+
FIRST_INTERIM_DEPRECATED = "P"
|
|
36
|
+
REPLACEMENT = "Q"
|
|
37
|
+
VOID_OR_CANCEL = "0"
|
|
38
|
+
FINAL_CLAIM = "1"
|
|
39
|
+
CWF_ADJUSTMENT = "2"
|
|
40
|
+
CMS_ADJUSTMENT = "3"
|
|
41
|
+
INTERMEDIARY_ADJUSTMENT = "4"
|
|
42
|
+
OTHER_ADJUSTMENT = "5"
|
|
43
|
+
OIG_ADJUSTMENT = "6"
|
|
44
|
+
MSP_ADJUSTMENT = "7"
|
|
45
|
+
QIO_ADJUSTMENT = "8"
|
|
46
|
+
PROVIDER_ADJUSTMENT = "9"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class SexType(IntEnum):
|
|
50
|
+
"""Biological sex of the patient for clinical purposes"""
|
|
51
|
+
|
|
52
|
+
UNKNOWN = 0
|
|
53
|
+
MALE = 1
|
|
54
|
+
FEMALE = 2
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Provider(BaseModel):
|
|
58
|
+
model_config = camel_case_model_config
|
|
59
|
+
|
|
60
|
+
npi: str
|
|
61
|
+
"""National Provider Identifier of the provider (from NM109, required)"""
|
|
62
|
+
|
|
63
|
+
provider_tax_id: Optional[str] = None
|
|
64
|
+
"""City of the provider (from N401, highly recommended)"""
|
|
65
|
+
|
|
66
|
+
provider_phones: Optional[list[str]] = None
|
|
67
|
+
"""Address line 1 of the provider (from N301, highly recommended)"""
|
|
68
|
+
|
|
69
|
+
provider_faxes: Optional[list[str]] = None
|
|
70
|
+
"""Commercial number of the provider used by some payers (from REF G2, optional)"""
|
|
71
|
+
|
|
72
|
+
provider_emails: Optional[list[str]] = None
|
|
73
|
+
"""State license number of the provider (from REF 0B, optional)"""
|
|
74
|
+
|
|
75
|
+
provider_license_number: Optional[str] = None
|
|
76
|
+
"""Last name of the provider (from NM103, highly recommended)"""
|
|
77
|
+
|
|
78
|
+
provider_commercial_number: Optional[str] = None
|
|
79
|
+
"""Email addresses of the provider (from PER, optional)"""
|
|
80
|
+
|
|
81
|
+
provider_taxonomy: Optional[str] = None
|
|
82
|
+
"""State of the provider (from N402, highly recommended)"""
|
|
83
|
+
|
|
84
|
+
provider_first_name: Optional[str] = None
|
|
85
|
+
"""Taxonomy code of the provider (from PRV03, highly recommended)"""
|
|
86
|
+
|
|
87
|
+
provider_last_name: Optional[str] = None
|
|
88
|
+
"""First name of the provider (NM104, highly recommended)"""
|
|
89
|
+
|
|
90
|
+
provider_org_name: Optional[str] = None
|
|
91
|
+
"""Organization name of the provider (from NM103, highly recommended)"""
|
|
92
|
+
|
|
93
|
+
provider_address1: Optional[str] = None
|
|
94
|
+
"""Tax ID of the provider (from REF highly recommended)"""
|
|
95
|
+
|
|
96
|
+
provider_address2: Optional[str] = None
|
|
97
|
+
"""Phone numbers of the provider (from PER, optional)"""
|
|
98
|
+
|
|
99
|
+
provider_city: Optional[str] = None
|
|
100
|
+
"""Fax numbers of the provider (from PER, optional)"""
|
|
101
|
+
|
|
102
|
+
provider_state: Optional[str] = None
|
|
103
|
+
"""Address line 2 of the provider (from N302, optional)"""
|
|
104
|
+
|
|
105
|
+
provider_zip: str
|
|
106
|
+
"""ZIP code of the provider (from N403, required)"""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ValueCode(BaseModel):
|
|
110
|
+
"""Code indicating the type of value provided (from HIxx_02)"""
|
|
111
|
+
|
|
112
|
+
model_config = camel_case_model_config
|
|
113
|
+
|
|
114
|
+
code: str
|
|
115
|
+
|
|
116
|
+
"""Amount associated with the value code (from HIxx_05)"""
|
|
117
|
+
amount: float
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class Diagnosis(BaseModel):
|
|
121
|
+
"""Principal ICD diagnosis for the patient (from HI ABK or BK)"""
|
|
122
|
+
|
|
123
|
+
model_config = camel_case_model_config
|
|
124
|
+
|
|
125
|
+
code: str
|
|
126
|
+
"""ICD code for the diagnosis"""
|
|
127
|
+
|
|
128
|
+
description: Optional[str] = None
|
|
129
|
+
"""Description of the diagnosis"""
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class Service(BaseModel):
|
|
133
|
+
model_config = camel_case_model_config
|
|
134
|
+
|
|
135
|
+
provider: Optional[Provider] = None
|
|
136
|
+
"""Additional provider information specific to this service item"""
|
|
137
|
+
|
|
138
|
+
line_number: Optional[str] = None
|
|
139
|
+
"""Unique line number for the service item (from LX01)"""
|
|
140
|
+
|
|
141
|
+
rev_code: Optional[str] = None
|
|
142
|
+
"""Revenue code (from SV2_01)"""
|
|
143
|
+
|
|
144
|
+
procedure_code: Optional[str] = None
|
|
145
|
+
"""Procedure code (from SV101_02 / SV202_02)"""
|
|
146
|
+
|
|
147
|
+
procedure_modifiers: Optional[str] = None
|
|
148
|
+
"""Procedure modifiers (from SV101_03, 4, 5, 6 / SV202_03, 4, 5, 6)"""
|
|
149
|
+
|
|
150
|
+
drug_code: Optional[str] = None
|
|
151
|
+
"""National Drug Code (from LIN03)"""
|
|
152
|
+
|
|
153
|
+
date_from: Optional[Date] = None
|
|
154
|
+
"""Begin date of service (from DTP 472)"""
|
|
155
|
+
|
|
156
|
+
date_through: Optional[Date] = None
|
|
157
|
+
"""End date of service (from DTP 472)"""
|
|
158
|
+
|
|
159
|
+
billed_amount: Optional[float] = None
|
|
160
|
+
"""Billed charge for the service (from SV102 / SV203)"""
|
|
161
|
+
|
|
162
|
+
allowed_amount: Optional[float] = None
|
|
163
|
+
"""Plan allowed amount for the service (non-EDI)"""
|
|
164
|
+
|
|
165
|
+
paid_amount: Optional[float] = None
|
|
166
|
+
"""Plan paid amount for the service (non-EDI)"""
|
|
167
|
+
|
|
168
|
+
quantity: Optional[float] = None
|
|
169
|
+
"""Quantity of the service (from SV104 / SV205)"""
|
|
170
|
+
|
|
171
|
+
units: Optional[str] = None
|
|
172
|
+
"""Units connected to the quantity given (from SV103 / SV204)"""
|
|
173
|
+
|
|
174
|
+
place_of_service: Optional[str] = None
|
|
175
|
+
"""Place of service code (from SV105)"""
|
|
176
|
+
|
|
177
|
+
diagnosis_pointers: Optional[list[int]] = None
|
|
178
|
+
"""Diagnosis pointers (from SV107)"""
|
|
179
|
+
|
|
180
|
+
ambulance_pickup_zip: Optional[str] = None
|
|
181
|
+
"""ZIP code where ambulance picked up patient. Supplied if different than claim-level value (from NM1 PW)"""
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class Claim(Provider, BaseModel):
|
|
185
|
+
model_config = camel_case_model_config
|
|
186
|
+
|
|
187
|
+
claim_id: Optional[str] = None
|
|
188
|
+
"""Unique identifier for the claim (from REF D9)"""
|
|
189
|
+
|
|
190
|
+
plan_code: Optional[str] = None
|
|
191
|
+
"""Identifies the subscriber's plan (from SBR03)"""
|
|
192
|
+
|
|
193
|
+
patient_sex: Optional[SexType] = None
|
|
194
|
+
"""Biological sex of the patient for clinical purposes (from DMG02). 0:Unknown, 1:Male,
|
|
195
|
+
2:Female
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
patient_date_of_birth: Optional[Date] = None
|
|
199
|
+
"""Patient date of birth (from DMG03)"""
|
|
200
|
+
|
|
201
|
+
patient_height_in_cm: Optional[float] = None
|
|
202
|
+
"""Patient height in centimeters (from HI value A9, MEA value HT)"""
|
|
203
|
+
|
|
204
|
+
patient_weight_in_kg: Optional[float] = None
|
|
205
|
+
"""Patient weight in kilograms (from HI value A8, PAT08, CR102 [ambulance only])"""
|
|
206
|
+
|
|
207
|
+
ambulance_pickup_zip: Optional[str] = None
|
|
208
|
+
"""Location where patient was picked up in ambulance (from HI with HIxx_01=BE and HIxx_02=A0
|
|
209
|
+
or NM1 loop with NM1 PW)
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
form_type: Optional[FormType] = None
|
|
213
|
+
"""Type of form used to submit the claim. Can be HCFA or UB-04 (from CLM05_02)"""
|
|
214
|
+
|
|
215
|
+
bill_type_or_pos: Optional[str] = None
|
|
216
|
+
"""Describes type of facility where services were rendered (from CLM05_01)"""
|
|
217
|
+
|
|
218
|
+
bill_type_sequence: Optional[BillTypeSequence] = None
|
|
219
|
+
"""Where the claim is at in its billing lifecycle (e.g. 0: Non-Pay, 1: Admit Through
|
|
220
|
+
Discharge, 7: Replacement, etc.) (from CLM05_03)
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
billed_amount: Optional[float] = None
|
|
224
|
+
"""Billed amount from provider (from CLM02)"""
|
|
225
|
+
|
|
226
|
+
allowed_amount: Optional[float] = None
|
|
227
|
+
"""Amount allowed by the plan for payment. Both member and plan responsibility (non-EDI)"""
|
|
228
|
+
|
|
229
|
+
paid_amount: Optional[float] = None
|
|
230
|
+
"""Amount paid by the plan for the claim (non-EDI)"""
|
|
231
|
+
|
|
232
|
+
date_from: Optional[Date] = None
|
|
233
|
+
"""Earliest service date among services, or statement date if not found"""
|
|
234
|
+
|
|
235
|
+
date_through: Optional[Date] = None
|
|
236
|
+
"""Latest service date among services, or statement date if not found"""
|
|
237
|
+
|
|
238
|
+
discharge_status: Optional[str] = None
|
|
239
|
+
"""Status of the patient at time of discharge (from CL103)"""
|
|
240
|
+
|
|
241
|
+
admit_diagnosis: Optional[str] = None
|
|
242
|
+
"""ICD diagnosis at the time the patient was admitted (from HI ABJ or BJ)"""
|
|
243
|
+
|
|
244
|
+
principal_diagnosis: Optional[Diagnosis] = None
|
|
245
|
+
"""Principal ICD diagnosis for the patient (from HI ABK or BK)"""
|
|
246
|
+
|
|
247
|
+
other_diagnoses: Optional[list[Diagnosis]] = None
|
|
248
|
+
"""Other ICD diagnoses that apply to the patient (from HI ABF or BF)"""
|
|
249
|
+
|
|
250
|
+
principal_procedure: Optional[str] = None
|
|
251
|
+
"""Principal ICD procedure for the patient (from HI BBR or BR)"""
|
|
252
|
+
|
|
253
|
+
other_procedures: Optional[list[str]] = None
|
|
254
|
+
"""Other ICD procedures that apply to the patient (from HI BBQ or BQ)"""
|
|
255
|
+
|
|
256
|
+
condition_codes: Optional[list[str]] = None
|
|
257
|
+
"""Special conditions that may affect payment or other processing (from HI BG)"""
|
|
258
|
+
|
|
259
|
+
value_codes: Optional[list[ValueCode]] = None
|
|
260
|
+
"""Numeric values related to the patient or claim (HI BE)"""
|
|
261
|
+
|
|
262
|
+
occurrence_codes: Optional[list[str]] = None
|
|
263
|
+
"""Date related occurrences related to the patient or claim (from HI BH)"""
|
|
264
|
+
|
|
265
|
+
drg: Optional[str] = None
|
|
266
|
+
"""Diagnosis Related Group for inpatient services (from HI DR)"""
|
|
267
|
+
|
|
268
|
+
services: list[Service] = Field(min_length=1)
|
|
269
|
+
"""One or more services provided to the patient (from LX loop)"""
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class RateSheetService(BaseModel):
|
|
273
|
+
model_config = camel_case_model_config
|
|
274
|
+
|
|
275
|
+
procedure_code: str
|
|
276
|
+
"""Procedure code (from SV101_02 / SV202_02)"""
|
|
277
|
+
|
|
278
|
+
procedure_modifiers: list[str]
|
|
279
|
+
"""Procedure modifiers (from SV101_03, 4, 5, 6 / SV202_03, 4, 5, 6)"""
|
|
280
|
+
|
|
281
|
+
billed_amount: float
|
|
282
|
+
"""Billed charge for the service (from SV102 / SV203)"""
|
|
283
|
+
|
|
284
|
+
allowed_amount: float
|
|
285
|
+
"""Plan allowed amount for the service (non-EDI)"""
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class RateSheet(BaseModel):
|
|
289
|
+
npi: str
|
|
290
|
+
"""National Provider Identifier of the provider (from NM109, required)"""
|
|
291
|
+
|
|
292
|
+
provider_first_name: str
|
|
293
|
+
"""First name of the provider (NM104, highly recommended)"""
|
|
294
|
+
|
|
295
|
+
provider_last_name: str
|
|
296
|
+
"""Last name of the provider (from NM103, highly recommended)"""
|
|
297
|
+
|
|
298
|
+
provider_org_name: str
|
|
299
|
+
"""Organization name of the provider (from NM103, highly recommended)"""
|
|
300
|
+
|
|
301
|
+
provider_address: str
|
|
302
|
+
"""Address of the provider (from N301, highly recommended)"""
|
|
303
|
+
|
|
304
|
+
provider_city: str
|
|
305
|
+
"""City of the provider (from N401, highly recommended)"""
|
|
306
|
+
|
|
307
|
+
provider_state: str
|
|
308
|
+
"""State of the provider (from N402, highly recommended)"""
|
|
309
|
+
|
|
310
|
+
provider_zip: str
|
|
311
|
+
"""ZIP code of the provider (from N403, required)"""
|
|
312
|
+
|
|
313
|
+
form_type: FormType
|
|
314
|
+
"""Type of form used to submit the claim. Can be HCFA or UB-04 (from CLM05_02)"""
|
|
315
|
+
|
|
316
|
+
bill_type_or_pos: str
|
|
317
|
+
"""Describes type of facility where services were rendered (from CLM05_01)"""
|
|
318
|
+
|
|
319
|
+
drg: str
|
|
320
|
+
"""Diagnosis Related Group for inpatient services (from HI DR)"""
|
|
321
|
+
|
|
322
|
+
billed_amount: float
|
|
323
|
+
"""Billed amount from provider (from CLM02)"""
|
|
324
|
+
|
|
325
|
+
allowed_amount: float
|
|
326
|
+
"""Amount allowed by the plan for payment. Both member and plan responsibility (non-EDI)"""
|
|
327
|
+
|
|
328
|
+
paid_amount: float
|
|
329
|
+
"""Amount paid by the plan for the claim (non-EDI)"""
|
|
330
|
+
|
|
331
|
+
services: list[RateSheetService]
|
|
332
|
+
"""One or more services provided to the patient (from LX loop)"""
|
mphapi/client.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import urllib.parse
|
|
2
|
+
from typing import Any, Mapping, Sequence
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
from pydantic import BaseModel, StrictBool, TypeAdapter
|
|
6
|
+
|
|
7
|
+
from .claim import Claim, RateSheet
|
|
8
|
+
from .pricing import Pricing
|
|
9
|
+
from .response import Response, Responses
|
|
10
|
+
|
|
11
|
+
Header = Mapping[str, str | bytes | None]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PriceConfig(BaseModel):
|
|
15
|
+
"""PriceConfig is used to configure the behavior of the pricing API"""
|
|
16
|
+
|
|
17
|
+
is_commercial: StrictBool
|
|
18
|
+
"""set to true to use commercial code crosswalks"""
|
|
19
|
+
|
|
20
|
+
disable_cost_based_reimbursement: StrictBool
|
|
21
|
+
"""by default, the API will use cost-based reimbursement for MAC priced line-items. This is the best estimate we have for this proprietary pricing"""
|
|
22
|
+
|
|
23
|
+
use_commercial_synthetic_for_not_allowed: StrictBool
|
|
24
|
+
"""set to true to use a synthetic Medicare price for line-items that are not allowed by Medicare"""
|
|
25
|
+
|
|
26
|
+
use_drg_from_grouper: StrictBool
|
|
27
|
+
"""set to true to always use the DRG from the inpatient grouper"""
|
|
28
|
+
|
|
29
|
+
use_best_drg_price: StrictBool
|
|
30
|
+
"""set to true to use the best DRG price between the price on the claim and the price from the grouper"""
|
|
31
|
+
|
|
32
|
+
override_threshold: float
|
|
33
|
+
"""set to a value greater than 0 to allow the pricer flexibility to override NCCI edits and other overridable errors and return a price"""
|
|
34
|
+
|
|
35
|
+
include_edits: StrictBool
|
|
36
|
+
"""set to true to include edit details in the response"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Client:
|
|
40
|
+
url: str
|
|
41
|
+
headers: Header
|
|
42
|
+
|
|
43
|
+
def __init__(self, apiKey: str, isTest: bool = False):
|
|
44
|
+
if isTest:
|
|
45
|
+
self.url = "https://api-test.myprice.health"
|
|
46
|
+
else:
|
|
47
|
+
self.url = "https://api.myprice.health"
|
|
48
|
+
|
|
49
|
+
self.headers = {"x-api-key": apiKey}
|
|
50
|
+
|
|
51
|
+
def _do_request(
|
|
52
|
+
self,
|
|
53
|
+
path: str,
|
|
54
|
+
json: Any | None,
|
|
55
|
+
method: str = "POST",
|
|
56
|
+
headers: Header = {},
|
|
57
|
+
) -> requests.Response:
|
|
58
|
+
return requests.request(
|
|
59
|
+
method,
|
|
60
|
+
urllib.parse.urljoin(self.url, path),
|
|
61
|
+
json=json,
|
|
62
|
+
headers={**self.headers, **headers},
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def _receive_response[
|
|
66
|
+
Model: BaseModel
|
|
67
|
+
](
|
|
68
|
+
self,
|
|
69
|
+
path: str,
|
|
70
|
+
body: BaseModel,
|
|
71
|
+
response_model: type[Model],
|
|
72
|
+
method: str = "POST",
|
|
73
|
+
headers: Header = {},
|
|
74
|
+
) -> Model:
|
|
75
|
+
"""
|
|
76
|
+
Raises:
|
|
77
|
+
ValueError
|
|
78
|
+
When response cannot be decoded.
|
|
79
|
+
mphapi.APIError
|
|
80
|
+
The error returned when the api returns an error.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
response = self._do_request(
|
|
84
|
+
path,
|
|
85
|
+
body.model_dump(mode="json", by_alias=True, exclude_none=True),
|
|
86
|
+
method,
|
|
87
|
+
headers,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
Response[response_model]
|
|
92
|
+
.model_validate_json(response.content, strict=True)
|
|
93
|
+
.result()
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def _receive_responses[
|
|
97
|
+
Model: BaseModel
|
|
98
|
+
](
|
|
99
|
+
self,
|
|
100
|
+
path: str,
|
|
101
|
+
body: Sequence[BaseModel],
|
|
102
|
+
response_model: type[Model],
|
|
103
|
+
method: str = "POST",
|
|
104
|
+
headers: Header = {},
|
|
105
|
+
) -> list[Model]:
|
|
106
|
+
"""
|
|
107
|
+
Raises:
|
|
108
|
+
ValueError
|
|
109
|
+
When response cannot be decoded.
|
|
110
|
+
mphapi.APIError
|
|
111
|
+
The error returned when the api returns an error.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
response = self._do_request(
|
|
115
|
+
path,
|
|
116
|
+
TypeAdapter(type(body)).dump_python(
|
|
117
|
+
body, mode="json", by_alias=True, exclude_none=True
|
|
118
|
+
),
|
|
119
|
+
method,
|
|
120
|
+
headers,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
Responses[response_model]
|
|
125
|
+
.model_validate_json(response.content, strict=True)
|
|
126
|
+
.results()
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def estimate_rate_sheet(self, *inputs: RateSheet) -> list[Pricing]:
|
|
130
|
+
"""
|
|
131
|
+
Raises:
|
|
132
|
+
ValueError
|
|
133
|
+
When response cannot be decoded.
|
|
134
|
+
mphapi.APIError
|
|
135
|
+
The error returned when the api returns an error.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
return self._receive_responses(
|
|
139
|
+
"/v1/medicare/estimate/rate-sheet",
|
|
140
|
+
inputs,
|
|
141
|
+
Pricing,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def estimate_claims(self, config: PriceConfig, *inputs: Claim) -> list[Pricing]:
|
|
145
|
+
"""
|
|
146
|
+
Raises:
|
|
147
|
+
ValueError
|
|
148
|
+
When response cannot be decoded.
|
|
149
|
+
mphapi.APIError
|
|
150
|
+
The error returned when the api returns an error.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
return self._receive_responses(
|
|
154
|
+
"/v1/medicare/estimate/claims",
|
|
155
|
+
inputs,
|
|
156
|
+
Pricing,
|
|
157
|
+
headers=self._get_price_headers(config),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def price(self, config: PriceConfig, input: Claim) -> Pricing:
|
|
161
|
+
"""
|
|
162
|
+
Raises:
|
|
163
|
+
ValueError
|
|
164
|
+
When response cannot be decoded.
|
|
165
|
+
mphapi.APIError
|
|
166
|
+
The error returned when the api returns an error.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
return self._receive_response(
|
|
170
|
+
"/v1/medicare/price/claim",
|
|
171
|
+
input,
|
|
172
|
+
Pricing,
|
|
173
|
+
headers=self._get_price_headers(config),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def price_batch(self, config: PriceConfig, *input: Claim) -> list[Pricing]:
|
|
177
|
+
"""
|
|
178
|
+
Raises:
|
|
179
|
+
ValueError
|
|
180
|
+
When response cannot be decoded.
|
|
181
|
+
mphapi.APIError
|
|
182
|
+
The error returned when the api returns an error.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
return self._receive_responses(
|
|
186
|
+
"/v1/medicare/price/claims",
|
|
187
|
+
input,
|
|
188
|
+
Pricing,
|
|
189
|
+
headers=self._get_price_headers(config),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def _get_price_headers(self, config: PriceConfig) -> Header:
|
|
193
|
+
headers: Header = {}
|
|
194
|
+
if config.is_commercial:
|
|
195
|
+
headers["is-commercial"] = "true"
|
|
196
|
+
|
|
197
|
+
if config.disable_cost_based_reimbursement:
|
|
198
|
+
headers["disable-cost-based-reimbursement"] = "true"
|
|
199
|
+
|
|
200
|
+
if config.use_commercial_synthetic_for_not_allowed:
|
|
201
|
+
headers["use-commercial-synthetic-for-not-allowed"] = "true"
|
|
202
|
+
|
|
203
|
+
if config.override_threshold > 0:
|
|
204
|
+
headers["override-threshold"] = str(config.override_threshold)
|
|
205
|
+
|
|
206
|
+
if config.include_edits:
|
|
207
|
+
headers["include-edits"] = "true"
|
|
208
|
+
|
|
209
|
+
if config.use_drg_from_grouper:
|
|
210
|
+
headers["use-drg-from-grouper"] = "true"
|
|
211
|
+
|
|
212
|
+
if config.use_best_drg_price:
|
|
213
|
+
headers["use-best-drg-price"] = "true"
|
|
214
|
+
|
|
215
|
+
return headers
|
mphapi/date.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from pydantic import GetCoreSchemaHandler
|
|
5
|
+
from pydantic_core import CoreSchema, core_schema
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Date:
|
|
9
|
+
"""Date is a custom type for representing dates in the format YYYYMMDD"""
|
|
10
|
+
|
|
11
|
+
year: int
|
|
12
|
+
month: int
|
|
13
|
+
day: int
|
|
14
|
+
|
|
15
|
+
def __init__(self, year: int, month: int, day: int):
|
|
16
|
+
self.year = year
|
|
17
|
+
self.month = month
|
|
18
|
+
self.day = day
|
|
19
|
+
|
|
20
|
+
def __str__(self):
|
|
21
|
+
return f"{self.year}{self.month:02d}{self.day:02d}"
|
|
22
|
+
|
|
23
|
+
# Based off of this: https://docs.pydantic.dev/2.1/usage/types/custom/#handling-third-party-types
|
|
24
|
+
@classmethod
|
|
25
|
+
def __get_pydantic_core_schema__(
|
|
26
|
+
cls, source_type: Any, handler: GetCoreSchemaHandler
|
|
27
|
+
) -> CoreSchema:
|
|
28
|
+
def to_date(value: str) -> Date:
|
|
29
|
+
time = datetime.strptime(value, "%Y%m%d")
|
|
30
|
+
|
|
31
|
+
return Date(time.year, time.month, time.day)
|
|
32
|
+
|
|
33
|
+
from_str = core_schema.chain_schema(
|
|
34
|
+
[
|
|
35
|
+
core_schema.str_schema(),
|
|
36
|
+
core_schema.no_info_plain_validator_function(to_date),
|
|
37
|
+
]
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return core_schema.json_or_python_schema(
|
|
41
|
+
json_schema=from_str,
|
|
42
|
+
python_schema=core_schema.union_schema(
|
|
43
|
+
[
|
|
44
|
+
core_schema.is_instance_schema(Date),
|
|
45
|
+
from_str,
|
|
46
|
+
]
|
|
47
|
+
),
|
|
48
|
+
serialization=core_schema.plain_serializer_function_ser_schema(
|
|
49
|
+
lambda date: str(date)
|
|
50
|
+
),
|
|
51
|
+
)
|
mphapi/pricing.py
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field, GetCoreSchemaHandler
|
|
5
|
+
from pydantic_core import core_schema
|
|
6
|
+
|
|
7
|
+
from .claim import Service, camel_case_model_config
|
|
8
|
+
from .response import ResponseError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ClaimRepricingCode(str, Enum):
|
|
12
|
+
"""claim-level repricing codes"""
|
|
13
|
+
|
|
14
|
+
MEDICARE = "MED"
|
|
15
|
+
CONTRACT_PRICING = "CON"
|
|
16
|
+
RBP_PRICING = "RBP"
|
|
17
|
+
SINGLE_CASE_AGREEMENT = "SCA"
|
|
18
|
+
NEEDS_MORE_INFO = "IFO"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LineRepricingCode(str, Enum):
|
|
22
|
+
# line-level Medicare repricing codes
|
|
23
|
+
MEDICARE = "MED"
|
|
24
|
+
SYNTHETIC_MEDICARE = "SYN"
|
|
25
|
+
COST_PERCENT = "CST"
|
|
26
|
+
MEDICARE_PERCENT = "MPT"
|
|
27
|
+
MEDICARE_NO_OUTLIER = "MNO"
|
|
28
|
+
BILLED_PERCENT = "BIL"
|
|
29
|
+
FEE_SCHEDULE = "FSC"
|
|
30
|
+
PER_DIEM = "PDM"
|
|
31
|
+
FLAT_RATE = "FLT"
|
|
32
|
+
LIMITED_TO_BILLED = "LTB"
|
|
33
|
+
|
|
34
|
+
# line-level zero dollar repricing explanations
|
|
35
|
+
NOT_ALLOWED_BY_MEDICARE = "NAM"
|
|
36
|
+
PACKAGED = "PKG"
|
|
37
|
+
NEEDS_MORE_INFO = "IFO"
|
|
38
|
+
PROCEDURE_CODE_PROBLEM = "CPB"
|
|
39
|
+
NOT_REPRICED_PER_REQUEST = "NRP"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class HospitalType(str, Enum):
|
|
43
|
+
ACUTE_CARE = "Acute Care Hospitals"
|
|
44
|
+
CRITICAL_ACCESS = "Critical Access Hospitals"
|
|
45
|
+
CHILDRENS = "Childrens"
|
|
46
|
+
PSYCHIATRIC = "Psychiatric"
|
|
47
|
+
ACUTE_CARE_DOD = "Acute Care - Department of Defense"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class InpatientPriceDetail(BaseModel):
|
|
51
|
+
"""InpatientPriceDetail contains pricing details for an inpatient claim"""
|
|
52
|
+
|
|
53
|
+
model_config = camel_case_model_config
|
|
54
|
+
|
|
55
|
+
drg: Optional[str] = None
|
|
56
|
+
"""Diagnosis Related Group (DRG) code used to price the claim"""
|
|
57
|
+
|
|
58
|
+
drg_amount: Optional[float] = None
|
|
59
|
+
"""Amount Medicare would pay for the DRG"""
|
|
60
|
+
|
|
61
|
+
passthrough_amount: Optional[float] = None
|
|
62
|
+
"""Per diem amount to cover capital-related costs, direct medical education, and other costs"""
|
|
63
|
+
|
|
64
|
+
outlier_amount: Optional[float] = None
|
|
65
|
+
"""Additional amount paid for high cost cases"""
|
|
66
|
+
|
|
67
|
+
indirect_medical_education_amount: Optional[float] = None
|
|
68
|
+
"""Additional amount paid for teaching hospitals"""
|
|
69
|
+
|
|
70
|
+
disproportionate_share_amount: Optional[float] = None
|
|
71
|
+
"""Additional amount paid for hospitals with a high number of low-income patients"""
|
|
72
|
+
|
|
73
|
+
uncompensated_care_amount: Optional[float] = None
|
|
74
|
+
"""Additional amount paid for patients who are unable to pay for their care"""
|
|
75
|
+
|
|
76
|
+
readmission_adjustment_amount: Optional[float] = None
|
|
77
|
+
"""Adjustment amount for hospitals with high readmission rates"""
|
|
78
|
+
|
|
79
|
+
value_based_purchasing_amount: Optional[float] = None
|
|
80
|
+
"""Adjustment for hospitals based on quality measures"""
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class OutpatientPriceDetail(BaseModel):
|
|
84
|
+
"""OutpatientPriceDetail contains pricing details for an outpatient claim"""
|
|
85
|
+
|
|
86
|
+
model_config = camel_case_model_config
|
|
87
|
+
|
|
88
|
+
outlier_amount: float
|
|
89
|
+
"""Additional amount paid for high cost cases"""
|
|
90
|
+
|
|
91
|
+
first_passthrough_drug_offset_amount: float
|
|
92
|
+
"""Amount built into the APC payment for certain drugs"""
|
|
93
|
+
|
|
94
|
+
second_passthrough_drug_offset_amount: float
|
|
95
|
+
"""Amount built into the APC payment for certain drugs"""
|
|
96
|
+
|
|
97
|
+
third_passthrough_drug_offset_amount: float
|
|
98
|
+
"""Amount built into the APC payment for certain drugs"""
|
|
99
|
+
|
|
100
|
+
first_device_offset_amount: float
|
|
101
|
+
"""Amount built into the APC payment for certain devices"""
|
|
102
|
+
|
|
103
|
+
second_device_offset_amount: float
|
|
104
|
+
"""Amount built into the APC payment for certain devices"""
|
|
105
|
+
|
|
106
|
+
full_or_partial_device_credit_offset_amount: float
|
|
107
|
+
"""Credit for devices that are supplied for free or at a reduced cost"""
|
|
108
|
+
|
|
109
|
+
terminated_device_procedure_offset_amount: float
|
|
110
|
+
"""Credit for devices that are not used due to a terminated procedure"""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class RuralIndicator(str, Enum):
|
|
114
|
+
RURAL = "R"
|
|
115
|
+
SUPER_RURAL = "B"
|
|
116
|
+
URBAN = ""
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def __get_pydantic_core_schema__(
|
|
120
|
+
cls,
|
|
121
|
+
_source_type: Any,
|
|
122
|
+
_handler: GetCoreSchemaHandler,
|
|
123
|
+
) -> core_schema.CoreSchema:
|
|
124
|
+
def from_int(value: int) -> RuralIndicator:
|
|
125
|
+
if value == 82:
|
|
126
|
+
return RuralIndicator.RURAL
|
|
127
|
+
elif value == 66:
|
|
128
|
+
return RuralIndicator.SUPER_RURAL
|
|
129
|
+
elif value == 32:
|
|
130
|
+
return RuralIndicator.URBAN
|
|
131
|
+
else:
|
|
132
|
+
raise ValueError(f"Unknown rural indicator value: {value}")
|
|
133
|
+
|
|
134
|
+
def to_int(instance: RuralIndicator) -> int:
|
|
135
|
+
if instance == RuralIndicator.RURAL:
|
|
136
|
+
return 82
|
|
137
|
+
elif instance == RuralIndicator.SUPER_RURAL:
|
|
138
|
+
return 66
|
|
139
|
+
elif instance == RuralIndicator.URBAN:
|
|
140
|
+
return 32
|
|
141
|
+
else:
|
|
142
|
+
raise ValueError(f"Unknown rural indicator: {instance}")
|
|
143
|
+
|
|
144
|
+
from_int_schema = core_schema.chain_schema(
|
|
145
|
+
[
|
|
146
|
+
core_schema.int_schema(),
|
|
147
|
+
core_schema.no_info_plain_validator_function(from_int),
|
|
148
|
+
]
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return core_schema.json_or_python_schema(
|
|
152
|
+
json_schema=from_int_schema,
|
|
153
|
+
python_schema=core_schema.union_schema(
|
|
154
|
+
[
|
|
155
|
+
# check if it's an instance first before doing any further work
|
|
156
|
+
core_schema.is_instance_schema(RuralIndicator),
|
|
157
|
+
from_int_schema,
|
|
158
|
+
]
|
|
159
|
+
),
|
|
160
|
+
serialization=core_schema.plain_serializer_function_ser_schema(to_int),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class ProviderDetail(BaseModel):
|
|
165
|
+
"""
|
|
166
|
+
ProviderDetail contains basic information about the provider and/or locality used for pricing.
|
|
167
|
+
Not all fields are returned with every pricing request. For example, the CMS Certification
|
|
168
|
+
Number (CCN) is only returned for facilities which have a CCN such as hospitals.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
model_config = camel_case_model_config
|
|
172
|
+
|
|
173
|
+
ccn: Optional[str] = None
|
|
174
|
+
"""CMS Certification Number for the facility"""
|
|
175
|
+
|
|
176
|
+
mac: Optional[int] = None
|
|
177
|
+
"""Medicare Administrative Contractor number"""
|
|
178
|
+
|
|
179
|
+
locality: Optional[int] = None
|
|
180
|
+
"""Geographic locality number used for pricing"""
|
|
181
|
+
|
|
182
|
+
rural_indicator: Optional[RuralIndicator] = None
|
|
183
|
+
"""Indicates whether provider is Rural (R), Super Rural (B), or Urban (blank)"""
|
|
184
|
+
|
|
185
|
+
specialty_type: Optional[str] = None
|
|
186
|
+
"""Medicare provider specialty type"""
|
|
187
|
+
|
|
188
|
+
hospital_type: Optional[HospitalType] = None
|
|
189
|
+
"""Type of hospital"""
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class ClaimEdits(BaseModel):
|
|
193
|
+
"""ClaimEdits contains errors which cause the claim to be denied, rejected, suspended, or returned to the provider."""
|
|
194
|
+
|
|
195
|
+
model_config = camel_case_model_config
|
|
196
|
+
|
|
197
|
+
claim_overall_disposition: Optional[str] = None
|
|
198
|
+
claim_rejection_disposition: Optional[str] = None
|
|
199
|
+
claim_denial_disposition: Optional[str] = None
|
|
200
|
+
claim_return_to_provider_disposition: Optional[str] = None
|
|
201
|
+
claim_suspension_disposition: Optional[str] = None
|
|
202
|
+
line_item_rejection_disposition: Optional[str] = None
|
|
203
|
+
line_item_denial_disposition: Optional[str] = None
|
|
204
|
+
claim_rejection_reasons: Optional[list[str]] = None
|
|
205
|
+
claim_denial_reasons: Optional[list[str]] = None
|
|
206
|
+
claim_return_to_provider_reasons: Optional[list[str]] = None
|
|
207
|
+
claim_suspension_reasons: Optional[list[str]] = None
|
|
208
|
+
line_item_rejection_reasons: Optional[list[str]] = None
|
|
209
|
+
line_item_denial_reasons: Optional[list[str]] = None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class Pricing(BaseModel):
|
|
213
|
+
"""Pricing contains the results of a pricing request"""
|
|
214
|
+
|
|
215
|
+
model_config = camel_case_model_config
|
|
216
|
+
|
|
217
|
+
claim_id: Optional[str] = None
|
|
218
|
+
"""The unique identifier for the claim (copied from input)"""
|
|
219
|
+
|
|
220
|
+
medicare_amount: Optional[float] = None
|
|
221
|
+
"""The amount Medicare would pay for the service"""
|
|
222
|
+
|
|
223
|
+
allowed_amount: Optional[float] = None
|
|
224
|
+
"""The allowed amount based on a contract or RBP pricing"""
|
|
225
|
+
|
|
226
|
+
allowed_calculation_error: Optional[str] = None
|
|
227
|
+
"""The reason the allowed amount was not calculated"""
|
|
228
|
+
|
|
229
|
+
medicare_repricing_code: Optional[ClaimRepricingCode] = None
|
|
230
|
+
"""Explains the methodology used to calculate Medicare (MED or IFO)"""
|
|
231
|
+
|
|
232
|
+
medicare_repricing_note: Optional[str] = None
|
|
233
|
+
"""Note explaining approach for pricing or reason for error"""
|
|
234
|
+
|
|
235
|
+
allowed_repricing_code: Optional[ClaimRepricingCode] = None
|
|
236
|
+
"""Explains the methodology used to calculate allowed amount (CON, RBP, SCA, or IFO)"""
|
|
237
|
+
|
|
238
|
+
allowed_repricing_note: Optional[str] = None
|
|
239
|
+
"""Note explaining approach for pricing or reason for error"""
|
|
240
|
+
|
|
241
|
+
medicare_std_dev: Optional[float] = None
|
|
242
|
+
"""The standard deviation of the estimated Medicare amount (estimates service only)"""
|
|
243
|
+
|
|
244
|
+
medicare_source: Optional[str] = None
|
|
245
|
+
"""Source of the Medicare amount (e.g. physician fee schedule, OPPS, etc.)"""
|
|
246
|
+
|
|
247
|
+
inpatient_price_detail: Optional[InpatientPriceDetail] = None
|
|
248
|
+
"""Details about the inpatient pricing"""
|
|
249
|
+
|
|
250
|
+
outpatient_price_detail: Optional[OutpatientPriceDetail] = None
|
|
251
|
+
"""Details about the outpatient pricing"""
|
|
252
|
+
|
|
253
|
+
provider_detail: Optional[ProviderDetail] = None
|
|
254
|
+
"""The provider details used when pricing the claim"""
|
|
255
|
+
|
|
256
|
+
edit_detail: Optional[ClaimEdits] = None
|
|
257
|
+
"""Errors which cause the claim to be denied, rejected, suspended, or returned to the provider"""
|
|
258
|
+
|
|
259
|
+
pricer_result: Optional[str] = None
|
|
260
|
+
"""Pricer return details"""
|
|
261
|
+
|
|
262
|
+
services: list[Service] = Field(min_length=1)
|
|
263
|
+
"""Pricing for each service line on the claim"""
|
|
264
|
+
|
|
265
|
+
edit_error: Optional[ResponseError] = None
|
|
266
|
+
"""An error that occurred during some step of the pricing process"""
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class LineEdits(BaseModel):
|
|
270
|
+
"""LineEdits contains errors which cause the line item to be unable to be priced."""
|
|
271
|
+
|
|
272
|
+
model_config = camel_case_model_config
|
|
273
|
+
|
|
274
|
+
denial_or_rejection_text: str
|
|
275
|
+
procedure_edits: list[str]
|
|
276
|
+
modifier1_edits: list[str]
|
|
277
|
+
modifier2_edits: list[str]
|
|
278
|
+
modifier3_edits: list[str]
|
|
279
|
+
modifier4_edits: list[str]
|
|
280
|
+
modifier5_edits: list[str]
|
|
281
|
+
data_edits: list[str]
|
|
282
|
+
revenue_edits: list[str]
|
|
283
|
+
professional_edits: list[str]
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class PricedService(BaseModel):
|
|
287
|
+
"""PricedService contains the results of a pricing request for a single service line"""
|
|
288
|
+
|
|
289
|
+
model_config = camel_case_model_config
|
|
290
|
+
|
|
291
|
+
line_number: str
|
|
292
|
+
"""Number of the service line item (copied from input)"""
|
|
293
|
+
|
|
294
|
+
provider_detail: Optional[ProviderDetail] = None
|
|
295
|
+
"""Provider Details used when pricing the service if different than the claim"""
|
|
296
|
+
|
|
297
|
+
medicare_amount: float
|
|
298
|
+
"""Amount Medicare would pay for the service"""
|
|
299
|
+
|
|
300
|
+
allowed_amount: float
|
|
301
|
+
"""Allowed amount based on a contract or RBP pricing"""
|
|
302
|
+
|
|
303
|
+
allowed_calculation_error: str
|
|
304
|
+
"""Reason the allowed amount was not calculated"""
|
|
305
|
+
|
|
306
|
+
repricing_code: LineRepricingCode
|
|
307
|
+
"""Explains the methodology used to calculate Medicare"""
|
|
308
|
+
|
|
309
|
+
repricing_note: str
|
|
310
|
+
"""Note explaining approach for pricing or reason for error"""
|
|
311
|
+
|
|
312
|
+
technical_component_amount: float
|
|
313
|
+
"""Amount Medicare would pay for the technical component"""
|
|
314
|
+
|
|
315
|
+
professional_component_amount: float
|
|
316
|
+
"""Amount Medicare would pay for the professional component"""
|
|
317
|
+
|
|
318
|
+
medicare_std_dev: float
|
|
319
|
+
"""Standard deviation of the estimated Medicare amount (estimates service only)"""
|
|
320
|
+
|
|
321
|
+
medicare_source: str
|
|
322
|
+
"""Source of the Medicare amount (e.g. physician fee schedule, OPPS, etc.)"""
|
|
323
|
+
|
|
324
|
+
pricer_result: str
|
|
325
|
+
"""Pricing service return details"""
|
|
326
|
+
|
|
327
|
+
status_indicator: str
|
|
328
|
+
"""Code which gives more detail about how Medicare pays for the service"""
|
|
329
|
+
|
|
330
|
+
payment_indicator: str
|
|
331
|
+
"""Text which explains the type of payment for Medicare"""
|
|
332
|
+
|
|
333
|
+
payment_apc: str
|
|
334
|
+
"""Ambulatory Payment Classification"""
|
|
335
|
+
|
|
336
|
+
edit_detail: Optional[LineEdits] = None
|
|
337
|
+
"""Errors which cause the line item to be unable to be priced"""
|
mphapi/response.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from pydantic import BaseModel, RootModel
|
|
2
|
+
from pydantic.dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class APIError(Exception):
|
|
6
|
+
message: str
|
|
7
|
+
|
|
8
|
+
def __init__(self, message: str):
|
|
9
|
+
super().__init__(message)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ResponseError(APIError):
|
|
14
|
+
title: str
|
|
15
|
+
detail: str
|
|
16
|
+
|
|
17
|
+
def __post_init__(self):
|
|
18
|
+
super().__init__(self.__str__())
|
|
19
|
+
|
|
20
|
+
def __str__(self) -> str:
|
|
21
|
+
return f"{self.title}: {self.detail}"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ResponseSuccess[Result: BaseModel](BaseModel):
|
|
25
|
+
result: Result
|
|
26
|
+
status: int
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ResponseFailure(BaseModel):
|
|
30
|
+
error: ResponseError
|
|
31
|
+
status: int
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class GatewayError(APIError):
|
|
36
|
+
message: str
|
|
37
|
+
code: int
|
|
38
|
+
|
|
39
|
+
def __post_init__(self):
|
|
40
|
+
super().__init__(self.message)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Response[Result: BaseModel](
|
|
44
|
+
RootModel[ResponseSuccess[Result] | ResponseFailure | GatewayError]
|
|
45
|
+
):
|
|
46
|
+
"""
|
|
47
|
+
Response contains the standardized API response data used by all My Price Health API's. It is based off of the generalized error handling recommendation found
|
|
48
|
+
in IETF RFC 7807 https://tools.ietf.org/html/rfc7807 and is a simplification of the Spring Boot error response as described at https://www.baeldung.com/rest-api-error-handling-best-practices
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
An error response might look like this:
|
|
53
|
+
{
|
|
54
|
+
"error: {
|
|
55
|
+
"title": "Incorrect username or password.",
|
|
56
|
+
"detail": "Authentication failed due to incorrect username or password.",
|
|
57
|
+
}
|
|
58
|
+
"status": 401,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
A successful response with a single result might look like this:
|
|
62
|
+
{
|
|
63
|
+
"result": {
|
|
64
|
+
"procedureCode": "ABC",
|
|
65
|
+
"billedAverage": 15.23
|
|
66
|
+
},
|
|
67
|
+
"status": 200,
|
|
68
|
+
}
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def result(
|
|
72
|
+
self,
|
|
73
|
+
) -> Result:
|
|
74
|
+
"""Returns the result if it's successful, otherwise throws
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
Result
|
|
79
|
+
The successful result.
|
|
80
|
+
|
|
81
|
+
Raises
|
|
82
|
+
------
|
|
83
|
+
ResponseError
|
|
84
|
+
The request's error response.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
if isinstance(self.root, ResponseSuccess):
|
|
88
|
+
return self.root.result
|
|
89
|
+
elif isinstance(self.root, ResponseFailure):
|
|
90
|
+
raise self.root.error
|
|
91
|
+
else:
|
|
92
|
+
raise self.root
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ResponsesSuccess[Result: BaseModel](BaseModel):
|
|
96
|
+
results: list[Result]
|
|
97
|
+
success_count: int
|
|
98
|
+
error_count: int
|
|
99
|
+
status_code: int
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Responses[Result: BaseModel](
|
|
103
|
+
RootModel[ResponsesSuccess[Result] | ResponseFailure]
|
|
104
|
+
):
|
|
105
|
+
def results(
|
|
106
|
+
self,
|
|
107
|
+
) -> list[Result]:
|
|
108
|
+
"""Returns the result if it's successful, otherwise throws
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
list[Result]
|
|
113
|
+
The results, although some may still have encountered errors.
|
|
114
|
+
|
|
115
|
+
Raises
|
|
116
|
+
------
|
|
117
|
+
ResponseError
|
|
118
|
+
The request's error response.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
if isinstance(self.root, ResponsesSuccess):
|
|
122
|
+
return self.root.results
|
|
123
|
+
else:
|
|
124
|
+
raise self.root.error
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: mphapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python interface to the MyPriceHealth API
|
|
5
|
+
Author: David Archibald
|
|
6
|
+
Author-email: davidarchibald@myprice.health
|
|
7
|
+
Requires-Python: >=3.10,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Requires-Dist: pydantic (>=2.6.4,<3.0.0)
|
|
13
|
+
Requires-Dist: requests (>=2.31.0,<3.0.0)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
mphapi/__init__.py,sha256=pY3H-ikyEKrkHm8f3L7-5_IaDf8G9vYMUVsWJRV8e04,210
|
|
2
|
+
mphapi/claim.py,sha256=LLVfSpDzf5arqFxpwRs_wgBzXptt0kVNzEpyoszTH58,10561
|
|
3
|
+
mphapi/client.py,sha256=AHEClAKS3j0wMdrjFhAmQqCJ_4Axc01DiOxZiOMzWhs,6297
|
|
4
|
+
mphapi/date.py,sha256=Vdv3xqZh610xFl1HK1e_YGK9tkdFs0giUTvCT5bbbRU,1504
|
|
5
|
+
mphapi/pricing.py,sha256=XSxJ2bBEBHd2olGXCG0LauJOi02l6p6GrqoEOX_Qn2o,11445
|
|
6
|
+
mphapi/response.py,sha256=opgHtRH7hIgz9tMSVV7966OJDAIB9O4BvOUW_KcJzqI,2958
|
|
7
|
+
mphapi-0.1.0.dist-info/METADATA,sha256=5ayo0jzI_6jMNBhBXJZEkmS0TDHoAcTP7P-MHmOCz2I,484
|
|
8
|
+
mphapi-0.1.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
|
9
|
+
mphapi-0.1.0.dist-info/RECORD,,
|