mphapi 0.5.0__py3-none-any.whl → 1.11.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/claim.py +3 -17
- mphapi/client.py +181 -37
- mphapi/client_test.py +114 -0
- mphapi/credentials.py +256 -0
- mphapi/date.py +92 -18
- mphapi/env.py +24 -0
- mphapi/pricing.py +198 -6
- mphapi/py.typed +0 -0
- mphapi/snapshots/client_test/test_client/hcfa.json +75 -0
- mphapi/snapshots/client_test/test_client/inpatient.json +405 -0
- mphapi/snapshots/client_test/test_client/outpatient.json +723 -0
- mphapi/testdata/hcfa.json +30 -0
- mphapi/testdata/inpatient.json +135 -0
- mphapi/testdata/outpatient.json +265 -0
- {mphapi-0.5.0.dist-info → mphapi-1.11.0.dist-info}/METADATA +4 -2
- mphapi-1.11.0.dist-info/RECORD +20 -0
- {mphapi-0.5.0.dist-info → mphapi-1.11.0.dist-info}/WHEEL +1 -1
- mphapi-0.5.0.dist-info/RECORD +0 -10
mphapi/claim.py
CHANGED
|
@@ -60,6 +60,9 @@ class Provider(BaseModel):
|
|
|
60
60
|
npi: str
|
|
61
61
|
"""National Provider Identifier of the provider (from NM109, required)"""
|
|
62
62
|
|
|
63
|
+
ccn: Optional[str] = None
|
|
64
|
+
"""CMS Certification Number (optional)"""
|
|
65
|
+
|
|
63
66
|
provider_tax_id: Annotated[Optional[str], field_name("providerTaxID")] = None
|
|
64
67
|
"""City of the provider (from N401, highly recommended)"""
|
|
65
68
|
|
|
@@ -195,8 +198,6 @@ class Service(BaseModel):
|
|
|
195
198
|
procedure_code: Optional[str] = None
|
|
196
199
|
"""Procedure code (from SV101_02 / SV202_02)"""
|
|
197
200
|
|
|
198
|
-
hipps_code: Optional[str] = None
|
|
199
|
-
|
|
200
201
|
procedure_modifiers: Optional[list[str]] = None
|
|
201
202
|
"""Procedure modifiers (from SV101_03, 4, 5, 6 / SV202_03, 4, 5, 6)"""
|
|
202
203
|
|
|
@@ -334,12 +335,6 @@ class RateSheetService(BaseModel):
|
|
|
334
335
|
procedure_modifiers: Optional[list[str]] = None
|
|
335
336
|
"""Procedure modifiers (from SV101_03, 4, 5, 6 / SV202_03, 4, 5, 6)"""
|
|
336
337
|
|
|
337
|
-
billed_amount: Optional[float] = None
|
|
338
|
-
"""Billed charge for the service (from SV102 / SV203)"""
|
|
339
|
-
|
|
340
|
-
allowed_amount: Optional[float] = None
|
|
341
|
-
"""Plan allowed amount for the service (non-EDI)"""
|
|
342
|
-
|
|
343
338
|
|
|
344
339
|
class RateSheet(BaseModel):
|
|
345
340
|
npi: str
|
|
@@ -375,14 +370,5 @@ class RateSheet(BaseModel):
|
|
|
375
370
|
drg: Optional[str] = None
|
|
376
371
|
"""Diagnosis Related Group for inpatient services (from HI DR)"""
|
|
377
372
|
|
|
378
|
-
billed_amount: Optional[float] = None
|
|
379
|
-
"""Billed amount from provider (from CLM02)"""
|
|
380
|
-
|
|
381
|
-
allowed_amount: Optional[float] = None
|
|
382
|
-
"""Amount allowed by the plan for payment. Both member and plan responsibility (non-EDI)"""
|
|
383
|
-
|
|
384
|
-
paid_amount: Optional[float] = None
|
|
385
|
-
"""Amount paid by the plan for the claim (non-EDI)"""
|
|
386
|
-
|
|
387
373
|
services: Optional[list[RateSheetService]] = None
|
|
388
374
|
"""One or more services provided to the patient (from LX loop)"""
|
mphapi/client.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import urllib.parse
|
|
2
|
-
from typing import Any, Mapping, Sequence
|
|
2
|
+
from typing import Annotated, Any, Mapping, Optional, Sequence
|
|
3
3
|
|
|
4
4
|
import requests
|
|
5
5
|
from pydantic import BaseModel, StrictBool, TypeAdapter
|
|
6
6
|
|
|
7
7
|
from .claim import Claim, RateSheet
|
|
8
|
-
from .
|
|
8
|
+
from .credentials import Credentials, get_credentials
|
|
9
|
+
from .fields import camel_case_model_config, field_name
|
|
9
10
|
from .response import Response, Responses
|
|
10
11
|
|
|
11
12
|
Header = Mapping[str, str | bytes | None]
|
|
@@ -14,71 +15,139 @@ Header = Mapping[str, str | bytes | None]
|
|
|
14
15
|
class PriceConfig(BaseModel):
|
|
15
16
|
"""PriceConfig is used to configure the behavior of the pricing API"""
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
model_config = camel_case_model_config
|
|
19
|
+
|
|
20
|
+
contract_ruleset: Optional[str] = None
|
|
21
|
+
"""set to the name of the ruleset to use for contract pricing"""
|
|
22
|
+
|
|
23
|
+
price_zero_billed: Optional[StrictBool] = False
|
|
18
24
|
"""set to true to price claims with zero billed amounts (default is false)"""
|
|
19
25
|
|
|
20
|
-
is_commercial: StrictBool
|
|
21
|
-
"""set to true to
|
|
26
|
+
is_commercial: Optional[StrictBool] = False
|
|
27
|
+
"""set to true to crosswalk codes from commercial codes Medicare won't pay for to substitute codes they do pay for (e.g. 99201 to G0463)"""
|
|
22
28
|
|
|
23
|
-
disable_cost_based_reimbursement: StrictBool
|
|
24
|
-
"""
|
|
29
|
+
disable_cost_based_reimbursement: Optional[StrictBool] = False
|
|
30
|
+
"""set to true to disable cost-based reimbursement for line items paid as a percent of cost"""
|
|
25
31
|
|
|
26
|
-
use_commercial_synthetic_for_not_allowed: StrictBool
|
|
32
|
+
use_commercial_synthetic_for_not_allowed: Optional[StrictBool] = False
|
|
27
33
|
"""set to true to use a synthetic Medicare price for line-items that are not allowed by Medicare"""
|
|
28
34
|
|
|
29
|
-
use_drg_from_grouper:
|
|
35
|
+
use_drg_from_grouper: Annotated[
|
|
36
|
+
Optional[StrictBool], field_name("useDRGFromGrouper")
|
|
37
|
+
] = False
|
|
30
38
|
"""set to true to always use the DRG from the inpatient grouper"""
|
|
31
39
|
|
|
32
|
-
use_best_drg_price: StrictBool
|
|
40
|
+
use_best_drg_price: Annotated[StrictBool, field_name("useBestDRGPrice")]
|
|
33
41
|
"""set to true to use the best DRG price between the price on the claim and the price from the grouper"""
|
|
34
42
|
|
|
35
|
-
override_threshold: float
|
|
43
|
+
override_threshold: Optional[float] = 0
|
|
36
44
|
"""set to a value greater than 0 to allow the pricer flexibility to override NCCI edits and other overridable errors and return a price"""
|
|
37
45
|
|
|
38
|
-
include_edits: StrictBool
|
|
46
|
+
include_edits: Optional[StrictBool] = False
|
|
39
47
|
"""set to true to include edit details in the response"""
|
|
40
48
|
|
|
41
|
-
continue_on_edit_fail: StrictBool
|
|
49
|
+
continue_on_edit_fail: Optional[StrictBool] = False
|
|
42
50
|
"""set to true to continue to price the claim even if there are edit failures"""
|
|
43
51
|
|
|
44
|
-
continue_on_provider_match_fail: StrictBool
|
|
52
|
+
continue_on_provider_match_fail: Optional[StrictBool] = False
|
|
45
53
|
"""set to true to continue with a average provider for the geographic area if the provider cannot be matched"""
|
|
46
54
|
|
|
47
|
-
disable_machine_learning_estimates: StrictBool
|
|
55
|
+
disable_machine_learning_estimates: Optional[StrictBool] = False
|
|
48
56
|
"""set to true to disable machine learning estimates (applies to estimates only)"""
|
|
49
57
|
|
|
58
|
+
assume_impossible_anesthesia_units_are_minutes: Optional[StrictBool] = False
|
|
59
|
+
"""set to true to divide impossible anesthesia units by 15 (max of 96 anesthesia units per day) (default is false)"""
|
|
60
|
+
|
|
61
|
+
fallback_to_max_anesthesia_units_per_day: Optional[StrictBool] = False
|
|
62
|
+
"""set to true to fallback to the maximum anesthesia units per day (default is false which will error if there are more than 96 anesthesia units per day)"""
|
|
63
|
+
|
|
64
|
+
allow_partial_results: Optional[StrictBool] = False
|
|
65
|
+
"""set to true to return partially repriced claims. This can be useful to get pricing on non-erroring line items, but should be used with caution"""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# It may be a bit jarring to see these imports not at the top of the file. This is
|
|
69
|
+
# intentional as `.pricing` depends on `PriceConfig`.
|
|
70
|
+
from .pricing import ClaimStatus # noqa: E402
|
|
71
|
+
from .pricing import Pricing # noqa: E402
|
|
72
|
+
|
|
50
73
|
|
|
51
74
|
class Client:
|
|
52
|
-
|
|
75
|
+
api_url: str
|
|
53
76
|
headers: Header
|
|
54
77
|
|
|
55
|
-
def __init__(
|
|
56
|
-
|
|
57
|
-
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
apiKey: str,
|
|
81
|
+
isTest: bool = False,
|
|
82
|
+
api_url: str | None = None,
|
|
83
|
+
app_url: str | None = None,
|
|
84
|
+
app_api_key: str | None = None,
|
|
85
|
+
app_referer: str | None = None,
|
|
86
|
+
app_credentials: Credentials | None = None,
|
|
87
|
+
):
|
|
88
|
+
if api_url is None:
|
|
89
|
+
if isTest:
|
|
90
|
+
self.api_url = "https://api-test.myprice.health"
|
|
91
|
+
else:
|
|
92
|
+
self.api_url = "https://api.myprice.health"
|
|
93
|
+
else:
|
|
94
|
+
self.api_url = api_url
|
|
95
|
+
|
|
96
|
+
if app_url is None:
|
|
97
|
+
if isTest:
|
|
98
|
+
self.app_url = "https://app-test.myprice.health"
|
|
99
|
+
else:
|
|
100
|
+
self.app_url = "https://app.myprice.health"
|
|
58
101
|
else:
|
|
59
|
-
self.
|
|
102
|
+
self.app_url = app_url
|
|
103
|
+
|
|
104
|
+
if (app_url is None) != (app_api_key is None) or (app_api_key is None) != (
|
|
105
|
+
app_referer is None
|
|
106
|
+
):
|
|
107
|
+
raise Exception(
|
|
108
|
+
"app_url, app_api_key, and app_referer must be set in tandem"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
self.app_api_key = app_api_key
|
|
112
|
+
self.app_credentials = app_credentials
|
|
113
|
+
self.app_referer = app_referer
|
|
114
|
+
|
|
115
|
+
if (
|
|
116
|
+
self.app_credentials is None
|
|
117
|
+
and app_api_key is not None
|
|
118
|
+
and app_referer is not None
|
|
119
|
+
):
|
|
120
|
+
self.app_credentials = get_credentials(app_api_key, app_referer)
|
|
60
121
|
|
|
61
122
|
self.headers = {"x-api-key": apiKey}
|
|
62
123
|
|
|
124
|
+
def _get_id_token(self) -> str:
|
|
125
|
+
if self.app_credentials is None:
|
|
126
|
+
raise Exception("App credentials must be set to run this!")
|
|
127
|
+
|
|
128
|
+
refreshed = self.app_credentials.refresh_if_needed()
|
|
129
|
+
if refreshed is not None:
|
|
130
|
+
self.app_credentials = refreshed
|
|
131
|
+
|
|
132
|
+
return self.app_credentials.id_token
|
|
133
|
+
|
|
63
134
|
def _do_request(
|
|
64
135
|
self,
|
|
65
|
-
|
|
136
|
+
url: str,
|
|
66
137
|
json: Any | None,
|
|
67
138
|
method: str = "POST",
|
|
68
139
|
headers: Header = {},
|
|
69
140
|
) -> requests.Response:
|
|
70
141
|
return requests.request(
|
|
71
142
|
method,
|
|
72
|
-
|
|
143
|
+
url,
|
|
73
144
|
json=json,
|
|
74
145
|
headers={**self.headers, **headers},
|
|
75
146
|
)
|
|
76
147
|
|
|
77
|
-
def _receive_response[
|
|
78
|
-
Model: BaseModel
|
|
79
|
-
](
|
|
148
|
+
def _receive_response[Model: BaseModel](
|
|
80
149
|
self,
|
|
81
|
-
|
|
150
|
+
url: str,
|
|
82
151
|
body: BaseModel,
|
|
83
152
|
response_model: type[Model],
|
|
84
153
|
method: str = "POST",
|
|
@@ -93,7 +162,7 @@ class Client:
|
|
|
93
162
|
"""
|
|
94
163
|
|
|
95
164
|
response = self._do_request(
|
|
96
|
-
|
|
165
|
+
url,
|
|
97
166
|
body.model_dump(mode="json", by_alias=True, exclude_none=True),
|
|
98
167
|
method,
|
|
99
168
|
headers,
|
|
@@ -105,11 +174,44 @@ class Client:
|
|
|
105
174
|
.result()
|
|
106
175
|
)
|
|
107
176
|
|
|
108
|
-
def
|
|
109
|
-
|
|
110
|
-
|
|
177
|
+
def _receive_api_response[Model: BaseModel](
|
|
178
|
+
self,
|
|
179
|
+
url: str,
|
|
180
|
+
body: BaseModel,
|
|
181
|
+
response_model: type[Model],
|
|
182
|
+
method: str = "POST",
|
|
183
|
+
headers: Header = {},
|
|
184
|
+
) -> Model:
|
|
185
|
+
return self._receive_response(
|
|
186
|
+
urllib.parse.urljoin(self.api_url, url),
|
|
187
|
+
body,
|
|
188
|
+
response_model,
|
|
189
|
+
method,
|
|
190
|
+
headers,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def _receive_app_response[Model: BaseModel](
|
|
194
|
+
self,
|
|
195
|
+
url: str,
|
|
196
|
+
body: BaseModel,
|
|
197
|
+
response_model: type[Model],
|
|
198
|
+
method: str = "POST",
|
|
199
|
+
headers: Header = {},
|
|
200
|
+
) -> Model:
|
|
201
|
+
id_token = self._get_id_token()
|
|
202
|
+
|
|
203
|
+
# TODO: Handle refreshing revoked credentials. It's unclear what the error will be unfortunately.
|
|
204
|
+
return self._receive_response(
|
|
205
|
+
urllib.parse.urljoin(self.app_url, url),
|
|
206
|
+
body,
|
|
207
|
+
response_model,
|
|
208
|
+
method,
|
|
209
|
+
{"Authorization": f"Bearer {id_token}", **headers},
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def _receive_responses[Model: BaseModel](
|
|
111
213
|
self,
|
|
112
|
-
|
|
214
|
+
url: str,
|
|
113
215
|
body: Sequence[BaseModel],
|
|
114
216
|
response_model: type[Model],
|
|
115
217
|
method: str = "POST",
|
|
@@ -124,7 +226,7 @@ class Client:
|
|
|
124
226
|
"""
|
|
125
227
|
|
|
126
228
|
response = self._do_request(
|
|
127
|
-
|
|
229
|
+
url,
|
|
128
230
|
TypeAdapter(type(body)).dump_python(
|
|
129
231
|
body, mode="json", by_alias=True, exclude_none=True
|
|
130
232
|
),
|
|
@@ -138,6 +240,41 @@ class Client:
|
|
|
138
240
|
.results()
|
|
139
241
|
)
|
|
140
242
|
|
|
243
|
+
def _receive_api_responses[Model: BaseModel](
|
|
244
|
+
self,
|
|
245
|
+
url: str,
|
|
246
|
+
body: Sequence[BaseModel],
|
|
247
|
+
response_model: type[Model],
|
|
248
|
+
method: str = "POST",
|
|
249
|
+
headers: Header = {},
|
|
250
|
+
) -> list[Model]:
|
|
251
|
+
return self._receive_responses(
|
|
252
|
+
urllib.parse.urljoin(self.api_url, url),
|
|
253
|
+
body,
|
|
254
|
+
response_model,
|
|
255
|
+
method,
|
|
256
|
+
headers,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def _receive_app_responses[Model: BaseModel](
|
|
260
|
+
self,
|
|
261
|
+
url: str,
|
|
262
|
+
body: Sequence[BaseModel],
|
|
263
|
+
response_model: type[Model],
|
|
264
|
+
method: str = "POST",
|
|
265
|
+
headers: Header = {},
|
|
266
|
+
) -> list[Model]:
|
|
267
|
+
id_token = self._get_id_token()
|
|
268
|
+
|
|
269
|
+
# TODO: Handle refreshing revoked credentials. It's unclear what the error will be unfortunately.
|
|
270
|
+
return self._receive_responses(
|
|
271
|
+
urllib.parse.urljoin(self.app_url, url),
|
|
272
|
+
body,
|
|
273
|
+
response_model,
|
|
274
|
+
method,
|
|
275
|
+
{"Authorization": f"Bearer {id_token}", **headers},
|
|
276
|
+
)
|
|
277
|
+
|
|
141
278
|
def estimate_rate_sheet(self, *inputs: RateSheet) -> list[Pricing]:
|
|
142
279
|
"""
|
|
143
280
|
Raises:
|
|
@@ -147,7 +284,7 @@ class Client:
|
|
|
147
284
|
The error returned when the api returns an error.
|
|
148
285
|
"""
|
|
149
286
|
|
|
150
|
-
return self.
|
|
287
|
+
return self._receive_api_responses(
|
|
151
288
|
"/v1/medicare/estimate/rate-sheet",
|
|
152
289
|
inputs,
|
|
153
290
|
Pricing,
|
|
@@ -162,7 +299,7 @@ class Client:
|
|
|
162
299
|
The error returned when the api returns an error.
|
|
163
300
|
"""
|
|
164
301
|
|
|
165
|
-
return self.
|
|
302
|
+
return self._receive_api_responses(
|
|
166
303
|
"/v1/medicare/estimate/claims",
|
|
167
304
|
inputs,
|
|
168
305
|
Pricing,
|
|
@@ -179,7 +316,7 @@ class Client:
|
|
|
179
316
|
"""
|
|
180
317
|
|
|
181
318
|
return self._receive_response(
|
|
182
|
-
"/v1/medicare/price/claim",
|
|
319
|
+
urllib.parse.urljoin(self.api_url, "/v1/medicare/price/claim"),
|
|
183
320
|
input,
|
|
184
321
|
Pricing,
|
|
185
322
|
headers=self._get_price_headers(config),
|
|
@@ -194,7 +331,7 @@ class Client:
|
|
|
194
331
|
The error returned when the api returns an error.
|
|
195
332
|
"""
|
|
196
333
|
|
|
197
|
-
return self.
|
|
334
|
+
return self._receive_api_responses(
|
|
198
335
|
"/v1/medicare/price/claims",
|
|
199
336
|
input,
|
|
200
337
|
Pricing,
|
|
@@ -215,7 +352,7 @@ class Client:
|
|
|
215
352
|
if config.use_commercial_synthetic_for_not_allowed:
|
|
216
353
|
headers["use-commercial-synthetic-for-not-allowed"] = "true"
|
|
217
354
|
|
|
218
|
-
if config.override_threshold > 0:
|
|
355
|
+
if config.override_threshold is not None and config.override_threshold > 0:
|
|
219
356
|
headers["override-threshold"] = str(config.override_threshold)
|
|
220
357
|
|
|
221
358
|
if config.include_edits:
|
|
@@ -237,3 +374,10 @@ class Client:
|
|
|
237
374
|
headers["disable-machine-learning-estimates"] = "true"
|
|
238
375
|
|
|
239
376
|
return headers
|
|
377
|
+
|
|
378
|
+
def insert_claim_status(self, claim_id: str, claim_status: ClaimStatus) -> None:
|
|
379
|
+
self._receive_app_response(
|
|
380
|
+
f"/v1/claim/{claim_id}/status",
|
|
381
|
+
claim_status,
|
|
382
|
+
BaseModel,
|
|
383
|
+
)
|
mphapi/client_test.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
# This import annoys Pylance for some reason.
|
|
9
|
+
from pytest_snapshot.plugin import Snapshot # type: ignore
|
|
10
|
+
|
|
11
|
+
from .client import Claim, Client, PriceConfig
|
|
12
|
+
from .credentials import Credentials, sign_in
|
|
13
|
+
from .env import load_env
|
|
14
|
+
from .pricing import ClaimStatus, PricedService, Pricing, status_new
|
|
15
|
+
from .response import ResponseError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture(autouse=True)
|
|
19
|
+
def run_around_tests():
|
|
20
|
+
load_env()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_client(snapshot: Snapshot):
|
|
24
|
+
api_key = os.getenv("MPH_API_KEY")
|
|
25
|
+
if api_key is None:
|
|
26
|
+
raise EnvironmentError("MPH_API_KEY must be set")
|
|
27
|
+
|
|
28
|
+
api_url = os.getenv("API_URL")
|
|
29
|
+
app_url = os.getenv("APP_URL")
|
|
30
|
+
app_api_key = os.getenv("FIREBASE_API_KEY")
|
|
31
|
+
if app_api_key is None:
|
|
32
|
+
raise Exception("FIREBASE_API_KEY must be set")
|
|
33
|
+
|
|
34
|
+
app_referer = os.getenv("FIREBASE_REFERER")
|
|
35
|
+
if app_referer is None:
|
|
36
|
+
raise Exception("FIREBASE_REFERER must be set")
|
|
37
|
+
|
|
38
|
+
app_credentials = Credentials(
|
|
39
|
+
api_key=app_api_key,
|
|
40
|
+
referer=app_referer,
|
|
41
|
+
credentials_path=Path("fake-credentials-path"),
|
|
42
|
+
email="test-user@mypricehealth.com",
|
|
43
|
+
# An id token with some minimal user info.
|
|
44
|
+
# Will not work if tested against a service that expects a real id token.
|
|
45
|
+
id_token="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImFhYSI6dHJ1ZX0.eyJpc3MiOiJ0ZXN0IGlzc3VlciIsImVtYWlsIjoidGVzdC11c2VyQG15cHJpY2VoZWFsdGguY29tIiwic3ViIjoidGVzdCBzdWJqZWN0IiwiYXVkIjoidGVzdCBhdWRpZW5jZSIsImV4cCI6MCwiaWF0IjowLCJpZCI6IjEyMzQ1Njc4OTAiLCJyb2xlcyI6WyJBZG1pbiJdfQ.iCx0YI9L9qo7KCkIq1w58C0mBdiCDhikPZsM3Mx78j11Ln3WxWpAnm0IomIRfrDVU1JczW8OjSj5os7igp9fYufZwObLV5N5eDd9LmYtXs2EcO8EFHcM7HeHx6lnkT1GxX-4J_946WbIdHYnpLyoXLZF_MZNfEwsN1UBG6YdaRcwfhF9vJHt6YM7-IoOcHvdLiEWv06Y9eC0v--_R_x8GwjSrE0Z9EyZw3hMz94QZ5VgUf8e89NexeVGIoD4pUOHuYmNIx1ca_UTIOg81exhhnh4g190jUSer5582YJc5Hx7gts3DyizRmAK8Glcwhnc7WKvZDQaSjtbHzIARnUKtw",
|
|
46
|
+
refresh_token="fake-refresh-token",
|
|
47
|
+
expires_at=sys.float_info.max,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
client = Client(
|
|
51
|
+
api_key,
|
|
52
|
+
api_url=api_url,
|
|
53
|
+
app_url=app_url,
|
|
54
|
+
app_api_key=app_api_key,
|
|
55
|
+
app_referer=app_referer,
|
|
56
|
+
app_credentials=app_credentials,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
config = PriceConfig(
|
|
60
|
+
is_commercial=True,
|
|
61
|
+
disable_cost_based_reimbursement=False,
|
|
62
|
+
use_commercial_synthetic_for_not_allowed=True,
|
|
63
|
+
use_drg_from_grouper=False,
|
|
64
|
+
use_best_drg_price=True,
|
|
65
|
+
override_threshold=300,
|
|
66
|
+
include_edits=True,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
tests = ["hcfa", "inpatient", "outpatient"]
|
|
70
|
+
|
|
71
|
+
for test in tests:
|
|
72
|
+
with open(f"testdata/{test}.json", "r") as f:
|
|
73
|
+
data = json.load(f)
|
|
74
|
+
|
|
75
|
+
claim = Claim.model_validate(data)
|
|
76
|
+
pricing = client.price(config, claim)
|
|
77
|
+
|
|
78
|
+
snapshot.assert_match(pricing.model_dump_json(indent=4), f"{test}.json")
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
client.insert_claim_status(
|
|
82
|
+
"123",
|
|
83
|
+
ClaimStatus(
|
|
84
|
+
step=status_new.step,
|
|
85
|
+
status=status_new.status,
|
|
86
|
+
pricing=Pricing(services=[PricedService(line_number="6789")]),
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
except ResponseError as response_error:
|
|
90
|
+
# The claim and line item won't exist in the database
|
|
91
|
+
assert (
|
|
92
|
+
response_error.detail
|
|
93
|
+
== "expected to insert 1 line item repricing rows but inserted 0"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_signin():
|
|
100
|
+
app_api_key = os.getenv("FIREBASE_API_KEY")
|
|
101
|
+
if app_api_key is None:
|
|
102
|
+
raise Exception("FIREBASE_API_KEY must be set")
|
|
103
|
+
|
|
104
|
+
test_user = os.getenv("FIREBASE_TEST_USER")
|
|
105
|
+
if test_user is None:
|
|
106
|
+
pytest.skip("Skipping because FIREBASE_TEST_USER is not set")
|
|
107
|
+
|
|
108
|
+
test_password = os.getenv("FIREBASE_TEST_PASSWORD")
|
|
109
|
+
if test_password is None:
|
|
110
|
+
pytest.skip("Skipping because FIREBASE_TEST_PASSWORD is not set")
|
|
111
|
+
|
|
112
|
+
credentials = sign_in(app_api_key, test_user, test_password)
|
|
113
|
+
|
|
114
|
+
assert credentials.email == test_user
|