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/credentials.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import getpass
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from pydantic import BaseModel, Field, RootModel
|
|
9
|
+
|
|
10
|
+
from .fields import camel_case_model_config
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RawCredentials(BaseModel):
|
|
14
|
+
model_config = camel_case_model_config
|
|
15
|
+
|
|
16
|
+
email: str
|
|
17
|
+
id_token: str
|
|
18
|
+
refresh_token: str
|
|
19
|
+
expires_at: float
|
|
20
|
+
|
|
21
|
+
def store(
|
|
22
|
+
self, api_key: str, referer: str, credentials_path: Path
|
|
23
|
+
) -> "Credentials":
|
|
24
|
+
stored_credentials = StoredCredentials(
|
|
25
|
+
api_key=api_key,
|
|
26
|
+
referer=referer,
|
|
27
|
+
email=self.email,
|
|
28
|
+
id_token=self.id_token,
|
|
29
|
+
refresh_token=self.refresh_token,
|
|
30
|
+
expires_at=self.expires_at,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
os.makedirs(os.path.dirname(credentials_path), exist_ok=True)
|
|
34
|
+
|
|
35
|
+
with credentials_path.open("w+") as f:
|
|
36
|
+
json.dump(
|
|
37
|
+
stored_credentials.model_dump(
|
|
38
|
+
mode="json", by_alias=True, exclude_none=True
|
|
39
|
+
),
|
|
40
|
+
f,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return Credentials(
|
|
44
|
+
referer=referer,
|
|
45
|
+
credentials_path=credentials_path,
|
|
46
|
+
api_key=api_key,
|
|
47
|
+
email=stored_credentials.email,
|
|
48
|
+
id_token=stored_credentials.id_token,
|
|
49
|
+
refresh_token=stored_credentials.refresh_token,
|
|
50
|
+
expires_at=stored_credentials.expires_at,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class StoredCredentials(RawCredentials):
|
|
55
|
+
api_key: str
|
|
56
|
+
referer: str
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Credentials(StoredCredentials):
|
|
60
|
+
credentials_path: Path = Field(..., exclude=True)
|
|
61
|
+
|
|
62
|
+
def refresh_if_needed(self) -> "Credentials | None":
|
|
63
|
+
# # There's more than 5 minutes until the credentials need refreshing. Don't bother.
|
|
64
|
+
if self.expires_at > time.time() + 5 * 60:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
new_credentials = refresh_token(self.api_key, self.referer, self.refresh_token)
|
|
68
|
+
return new_credentials.store(self.api_key, self.referer, self.credentials_path)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class GoogleException(Exception):
|
|
72
|
+
def __init__(self, message: str):
|
|
73
|
+
self.message = message
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class GoogleError(BaseModel):
|
|
77
|
+
code: int
|
|
78
|
+
message: str
|
|
79
|
+
# errors omitted as unused
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class GoogleResponseError(BaseModel):
|
|
83
|
+
model_config = camel_case_model_config
|
|
84
|
+
|
|
85
|
+
error: GoogleError
|
|
86
|
+
|
|
87
|
+
def to_exception(self) -> GoogleException:
|
|
88
|
+
return GoogleException(self.error.message)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class GoogleResponse[Result: BaseModel](RootModel[Result | GoogleResponseError]):
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class SignInResult(BaseModel):
|
|
96
|
+
"""
|
|
97
|
+
The result of signing in.
|
|
98
|
+
See https://docs.cloud.google.com/identity-platform/docs/reference/rest/v1/accounts/signInWithPassword
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
model_config = camel_case_model_config
|
|
102
|
+
|
|
103
|
+
email: str
|
|
104
|
+
"""The email of the authenticated user. Always present in the response."""
|
|
105
|
+
|
|
106
|
+
id_token: str
|
|
107
|
+
"""An Identity Platform ID token for the authenticated user."""
|
|
108
|
+
|
|
109
|
+
refresh_token: str
|
|
110
|
+
"""An Identity Platform refresh token for the authenticated user."""
|
|
111
|
+
|
|
112
|
+
expires_in: int
|
|
113
|
+
"""The number of seconds until the Identity Platform ID token expires."""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class RefreshTokenResult(BaseModel):
|
|
117
|
+
"""
|
|
118
|
+
The result of refreshing a refresh_token.
|
|
119
|
+
See https://cloud.google.com/identity-platform/docs/use-rest-api#section-refresh-token
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
# NOT camelCase
|
|
123
|
+
|
|
124
|
+
expires_in: int
|
|
125
|
+
"""The number of seconds in which the ID token expires."""
|
|
126
|
+
|
|
127
|
+
token_type: str
|
|
128
|
+
"""The type of the refresh token, always "Bearer"."""
|
|
129
|
+
|
|
130
|
+
refresh_token: str
|
|
131
|
+
"""The Identity Platform refresh token provided in the request or a new refresh token."""
|
|
132
|
+
|
|
133
|
+
id_token: str
|
|
134
|
+
"""An Identity Platform ID token."""
|
|
135
|
+
|
|
136
|
+
user_id: str
|
|
137
|
+
"""The uid corresponding to the provided ID token."""
|
|
138
|
+
|
|
139
|
+
project_id: str
|
|
140
|
+
"""Your Google Cloud project ID."""
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_credentials(api_key: str, referer: str) -> Credentials:
|
|
144
|
+
credentials_path = get_credentials_path()
|
|
145
|
+
|
|
146
|
+
credentials = get_stored_credentials(credentials_path)
|
|
147
|
+
if credentials is not None:
|
|
148
|
+
try:
|
|
149
|
+
credentials.refresh_if_needed()
|
|
150
|
+
except GoogleException as e:
|
|
151
|
+
# `INVALID_ID_TOKEN` means the user must login again.
|
|
152
|
+
if e.message != "INVALID_ID_TOKEN":
|
|
153
|
+
raise e
|
|
154
|
+
|
|
155
|
+
return credentials
|
|
156
|
+
|
|
157
|
+
email = input("Email: ")
|
|
158
|
+
login_credentials: RawCredentials | None = None
|
|
159
|
+
|
|
160
|
+
while True:
|
|
161
|
+
try:
|
|
162
|
+
password = getpass.getpass("Password: ")
|
|
163
|
+
login_credentials = sign_in(api_key, email, password)
|
|
164
|
+
break
|
|
165
|
+
except GoogleException as e:
|
|
166
|
+
if e.message == "INVALID_PASSWORD":
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
raise e
|
|
170
|
+
|
|
171
|
+
return login_credentials.store(api_key, referer, credentials_path)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def get_credentials_path() -> Path:
|
|
175
|
+
home = Path.home()
|
|
176
|
+
return home.joinpath(".mph", "credentials.json")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def get_stored_credentials(credentials_path: Path) -> Credentials | None:
|
|
180
|
+
try:
|
|
181
|
+
with credentials_path.open() as f:
|
|
182
|
+
credentials_json = json.load(f)
|
|
183
|
+
stored_credentials = StoredCredentials.model_validate(credentials_json)
|
|
184
|
+
except FileNotFoundError:
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
credentials = Credentials(
|
|
188
|
+
email=stored_credentials.email,
|
|
189
|
+
id_token=stored_credentials.id_token,
|
|
190
|
+
refresh_token=stored_credentials.refresh_token,
|
|
191
|
+
expires_at=stored_credentials.expires_at,
|
|
192
|
+
api_key=stored_credentials.api_key,
|
|
193
|
+
referer=stored_credentials.referer,
|
|
194
|
+
credentials_path=credentials_path,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return credentials
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def refresh_token(api_key: str, referer: str, refresh_token: str) -> RawCredentials:
|
|
201
|
+
response = requests.post(
|
|
202
|
+
"https://securetoken.googleapis.com/v1/token",
|
|
203
|
+
data={"grant_type": "refresh_token", "refresh_token": refresh_token},
|
|
204
|
+
params={
|
|
205
|
+
"key": api_key,
|
|
206
|
+
},
|
|
207
|
+
headers={
|
|
208
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
209
|
+
"Referer": referer,
|
|
210
|
+
},
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
result = (
|
|
214
|
+
GoogleResponse[RefreshTokenResult].model_validate_json(response.content).root
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
if isinstance(result, GoogleResponseError):
|
|
218
|
+
raise result.to_exception()
|
|
219
|
+
|
|
220
|
+
return RawCredentials(
|
|
221
|
+
email="",
|
|
222
|
+
id_token=result.id_token,
|
|
223
|
+
refresh_token=result.refresh_token,
|
|
224
|
+
expires_at=time.time() + result.expires_in,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def sign_in(api_key: str, email: str, password: str) -> RawCredentials:
|
|
229
|
+
response = requests.post(
|
|
230
|
+
"https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword",
|
|
231
|
+
json={
|
|
232
|
+
"email": email,
|
|
233
|
+
"password": password,
|
|
234
|
+
"returnSecureToken": "true",
|
|
235
|
+
},
|
|
236
|
+
params={
|
|
237
|
+
"key": api_key,
|
|
238
|
+
},
|
|
239
|
+
headers={"Referer": "http://myprice.health"},
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
result = GoogleResponse[SignInResult].model_validate_json(response.content).root
|
|
243
|
+
|
|
244
|
+
if isinstance(result, GoogleResponseError):
|
|
245
|
+
raise result.to_exception()
|
|
246
|
+
|
|
247
|
+
sign_in_result = result
|
|
248
|
+
|
|
249
|
+
credentials = RawCredentials(
|
|
250
|
+
email=sign_in_result.email,
|
|
251
|
+
id_token=sign_in_result.id_token,
|
|
252
|
+
refresh_token=sign_in_result.refresh_token,
|
|
253
|
+
expires_at=time.time() + sign_in_result.expires_in,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
return credentials
|
mphapi/date.py
CHANGED
|
@@ -1,39 +1,54 @@
|
|
|
1
|
-
from
|
|
2
|
-
from
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from datetime import datetime as dt
|
|
3
|
+
from datetime import tzinfo
|
|
4
|
+
from typing import Any, ClassVar, Self, SupportsIndex
|
|
3
5
|
|
|
4
6
|
from pydantic import GetCoreSchemaHandler
|
|
5
7
|
from pydantic_core import CoreSchema, core_schema
|
|
6
8
|
|
|
7
9
|
|
|
8
|
-
class
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
month: int
|
|
13
|
-
day: int
|
|
10
|
+
class AbstractDateTime:
|
|
11
|
+
format: ClassVar[str]
|
|
12
|
+
secondary_formats: ClassVar[list[str]] = []
|
|
13
|
+
datetime: dt
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
@classmethod
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def from_datetime(cls, datetime: dt) -> Self:
|
|
18
|
+
pass
|
|
19
19
|
|
|
20
20
|
def __str__(self):
|
|
21
|
-
return
|
|
21
|
+
return self.datetime.strftime(self.format)
|
|
22
22
|
|
|
23
23
|
# Based off of this: https://docs.pydantic.dev/2.1/usage/types/custom/#handling-third-party-types
|
|
24
24
|
@classmethod
|
|
25
25
|
def __get_pydantic_core_schema__(
|
|
26
26
|
cls, source_type: Any, handler: GetCoreSchemaHandler
|
|
27
27
|
) -> CoreSchema:
|
|
28
|
-
def
|
|
29
|
-
|
|
28
|
+
def to_datetime_cls(value: str) -> Self:
|
|
29
|
+
datetime: dt | None = None
|
|
30
|
+
|
|
31
|
+
if len(cls.secondary_formats) == 0:
|
|
32
|
+
datetime = dt.strptime(value, cls.format)
|
|
33
|
+
else:
|
|
34
|
+
formats = [cls.format, *cls.secondary_formats]
|
|
35
|
+
for format in formats:
|
|
36
|
+
try:
|
|
37
|
+
datetime = dt.strptime(value, format)
|
|
38
|
+
except ValueError:
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
if datetime is None:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
f"Could not parse date {repr(value)} with format {repr(cls.format)} or {repr(cls.secondary_formats)}"
|
|
44
|
+
)
|
|
30
45
|
|
|
31
|
-
return
|
|
46
|
+
return cls.from_datetime(datetime)
|
|
32
47
|
|
|
33
48
|
from_str = core_schema.chain_schema(
|
|
34
49
|
[
|
|
35
50
|
core_schema.str_schema(),
|
|
36
|
-
core_schema.no_info_plain_validator_function(
|
|
51
|
+
core_schema.no_info_plain_validator_function(to_datetime_cls),
|
|
37
52
|
]
|
|
38
53
|
)
|
|
39
54
|
|
|
@@ -41,7 +56,7 @@ class Date:
|
|
|
41
56
|
json_schema=from_str,
|
|
42
57
|
python_schema=core_schema.union_schema(
|
|
43
58
|
[
|
|
44
|
-
core_schema.is_instance_schema(
|
|
59
|
+
core_schema.is_instance_schema(cls),
|
|
45
60
|
from_str,
|
|
46
61
|
]
|
|
47
62
|
),
|
|
@@ -49,3 +64,62 @@ class Date:
|
|
|
49
64
|
lambda date: str(date)
|
|
50
65
|
),
|
|
51
66
|
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Date(AbstractDateTime):
|
|
70
|
+
"""Date is a custom type for representing dates in the format YYYYMMDD"""
|
|
71
|
+
|
|
72
|
+
format = "%Y%m%d"
|
|
73
|
+
|
|
74
|
+
year: int
|
|
75
|
+
month: int
|
|
76
|
+
day: int
|
|
77
|
+
|
|
78
|
+
def __init__(self, year: int, month: int, day: int):
|
|
79
|
+
self.year = year
|
|
80
|
+
self.month = month
|
|
81
|
+
self.day = day
|
|
82
|
+
self.datetime = dt(year, month, day)
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def from_datetime(cls, datetime: dt):
|
|
86
|
+
return cls(datetime.year, datetime.month, datetime.day)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class DateTime(AbstractDateTime):
|
|
90
|
+
"""DateTime is a custom type for representing dates"""
|
|
91
|
+
|
|
92
|
+
# `%f` is padded to 6 characters but should be 9 overall.
|
|
93
|
+
format = "%Y-%m-%d %H:%M:%S.000%f%z"
|
|
94
|
+
|
|
95
|
+
# Same constructor as datetime has.
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
year: SupportsIndex,
|
|
99
|
+
month: SupportsIndex,
|
|
100
|
+
day: SupportsIndex,
|
|
101
|
+
hour: SupportsIndex = 0,
|
|
102
|
+
minute: SupportsIndex = 0,
|
|
103
|
+
second: SupportsIndex = 0,
|
|
104
|
+
microsecond: SupportsIndex = 0,
|
|
105
|
+
tzinfo: tzinfo | None = None,
|
|
106
|
+
*,
|
|
107
|
+
fold: int = 0,
|
|
108
|
+
):
|
|
109
|
+
self.datetime = dt(
|
|
110
|
+
year, month, day, hour, minute, second, microsecond, tzinfo, fold=fold
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def from_datetime(cls, datetime: dt):
|
|
115
|
+
return cls(
|
|
116
|
+
datetime.year,
|
|
117
|
+
datetime.month,
|
|
118
|
+
datetime.day,
|
|
119
|
+
datetime.hour,
|
|
120
|
+
datetime.minute,
|
|
121
|
+
datetime.second,
|
|
122
|
+
datetime.microsecond,
|
|
123
|
+
datetime.tzinfo,
|
|
124
|
+
fold=datetime.fold,
|
|
125
|
+
)
|
mphapi/env.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def load_env():
|
|
8
|
+
path = Path(__file__)
|
|
9
|
+
root_dir = None
|
|
10
|
+
env_file = None
|
|
11
|
+
for parent in path.parents:
|
|
12
|
+
root_dir = parent
|
|
13
|
+
env_file = root_dir.joinpath(".env")
|
|
14
|
+
if env_file.exists():
|
|
15
|
+
break
|
|
16
|
+
|
|
17
|
+
if root_dir is None:
|
|
18
|
+
print("No .env file found", file=sys.stderr)
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
print(f"Using .env from {repr(str(root_dir))}.")
|
|
22
|
+
|
|
23
|
+
load_dotenv(root_dir.joinpath(".env"))
|
|
24
|
+
load_dotenv(root_dir.joinpath(".env.local"))
|
mphapi/pricing.py
CHANGED
|
@@ -3,6 +3,8 @@ from typing import Annotated, Optional
|
|
|
3
3
|
|
|
4
4
|
from pydantic import BaseModel, Field
|
|
5
5
|
|
|
6
|
+
from .client import PriceConfig
|
|
7
|
+
from .date import DateTime
|
|
6
8
|
from .fields import camel_case_model_config, field_name
|
|
7
9
|
from .response import ResponseError
|
|
8
10
|
|
|
@@ -72,6 +74,7 @@ class MedicareSource(str, Enum):
|
|
|
72
74
|
ManualPricing = "Manual Pricing"
|
|
73
75
|
SNF = "SNF PPS"
|
|
74
76
|
Synthetic = "Synthetic Medicare"
|
|
77
|
+
DMEFS = "DMEFS"
|
|
75
78
|
|
|
76
79
|
|
|
77
80
|
class InpatientPriceDetail(BaseModel):
|
|
@@ -143,6 +146,25 @@ class OutpatientPriceDetail(BaseModel):
|
|
|
143
146
|
"""Wage index used for geographic adjustment"""
|
|
144
147
|
|
|
145
148
|
|
|
149
|
+
class AllowedRepricingFormula(BaseModel):
|
|
150
|
+
"""The formula used to calculate the allowed amount"""
|
|
151
|
+
|
|
152
|
+
medicare_percent: Optional[float] = None
|
|
153
|
+
"""Percentage of the Medicare amount used to calculate the allowed amount"""
|
|
154
|
+
|
|
155
|
+
billed_percent: Optional[float] = None
|
|
156
|
+
"""Percentage of the billed amount used to calculate the allowed amount"""
|
|
157
|
+
|
|
158
|
+
fee_schedule: Optional[float] = None
|
|
159
|
+
"""Fee schedule amount used as the allowed amount"""
|
|
160
|
+
|
|
161
|
+
fixed_amount: Optional[float] = None
|
|
162
|
+
"""Fixed amount used as the allowed amount"""
|
|
163
|
+
|
|
164
|
+
per_diem: Optional[float] = None
|
|
165
|
+
"""Per diem rate used to calculate the allowed amount"""
|
|
166
|
+
|
|
167
|
+
|
|
146
168
|
class ProviderDetail(BaseModel):
|
|
147
169
|
"""
|
|
148
170
|
ProviderDetail contains basic information about the provider and/or locality used for pricing.
|
|
@@ -161,6 +183,12 @@ class ProviderDetail(BaseModel):
|
|
|
161
183
|
locality: Optional[int] = None
|
|
162
184
|
"""Geographic locality number used for pricing"""
|
|
163
185
|
|
|
186
|
+
geographic_cbsa: Annotated[Optional[int], field_name("geographicCBSA")] = None
|
|
187
|
+
"""Core-Based Statistical Area (CBSA) number for provider ZIP"""
|
|
188
|
+
|
|
189
|
+
state_cbsa: Annotated[Optional[int], field_name("stateCBSA")] = None
|
|
190
|
+
"""State Core-Based Statistical Area (CBSA) number"""
|
|
191
|
+
|
|
164
192
|
rural_indicator: Optional[RuralIndicator] = None
|
|
165
193
|
"""Indicates whether provider is Rural (R), Super Rural (B), or Urban (blank)"""
|
|
166
194
|
|
|
@@ -224,9 +252,6 @@ class LineEdits(BaseModel):
|
|
|
224
252
|
|
|
225
253
|
model_config = camel_case_model_config
|
|
226
254
|
|
|
227
|
-
denial_or_rejection_text: Optional[str] = None
|
|
228
|
-
"""The overall explanation for why this line item was denied or rejected by the claim editor"""
|
|
229
|
-
|
|
230
255
|
procedure_edits: Optional[list[str]] = None
|
|
231
256
|
"""Detailed description of each procedure code edit error (from outpatient editor)"""
|
|
232
257
|
|
|
@@ -251,9 +276,6 @@ class LineEdits(BaseModel):
|
|
|
251
276
|
revenue_edits: Optional[list[str]] = None
|
|
252
277
|
"""Detailed description of each revenue code edit error (from outpatient editor)"""
|
|
253
278
|
|
|
254
|
-
professional_edits: Optional[list[str]] = None
|
|
255
|
-
"""Detailed description of each professional claim edit error"""
|
|
256
|
-
|
|
257
279
|
|
|
258
280
|
class PricedService(BaseModel):
|
|
259
281
|
"""PricedService contains the results of a pricing request for a single service line"""
|
|
@@ -278,12 +300,18 @@ class PricedService(BaseModel):
|
|
|
278
300
|
medicare_repricing_note: Optional[str] = None
|
|
279
301
|
"""Note explaining approach for pricing or reason for error"""
|
|
280
302
|
|
|
303
|
+
network_code: Optional[str] = None
|
|
304
|
+
"""Code describing the network used for allowed amount pricing"""
|
|
305
|
+
|
|
281
306
|
allowed_repricing_code: Optional[LineRepricingCode] = None
|
|
282
307
|
"""Explains the methodology used to calculate allowed amount"""
|
|
283
308
|
|
|
284
309
|
allowed_repricing_note: Optional[str] = None
|
|
285
310
|
"""Note explaining approach for pricing or reason for error"""
|
|
286
311
|
|
|
312
|
+
allowed_repricing_formula: Optional[AllowedRepricingFormula] = None
|
|
313
|
+
"""Formula used to calculate the allowed amount"""
|
|
314
|
+
|
|
287
315
|
technical_component_amount: Optional[float] = None
|
|
288
316
|
"""Amount Medicare would pay for the technical component"""
|
|
289
317
|
|
|
@@ -305,6 +333,30 @@ class PricedService(BaseModel):
|
|
|
305
333
|
payment_indicator: Optional[str] = None
|
|
306
334
|
"""Text which explains the type of payment for Medicare"""
|
|
307
335
|
|
|
336
|
+
discount_formula: Optional[str] = None
|
|
337
|
+
"""The multi-procedure discount formula used to calculate the allowed amount (outpatient only)"""
|
|
338
|
+
|
|
339
|
+
line_item_denial_or_rejection_flag: Optional[str] = None
|
|
340
|
+
"""Identifies how a line item was denied or rejected and how the rejection can be overridden (outpatient only)"""
|
|
341
|
+
|
|
342
|
+
packaging_flag: Optional[str] = None
|
|
343
|
+
"""Indicates if the service is packaged and the reason for packaging (outpatient only)"""
|
|
344
|
+
|
|
345
|
+
payment_adjustment_flag: Optional[str] = None
|
|
346
|
+
"""Identifies special adjustments made to the payment (outpatient only)"""
|
|
347
|
+
|
|
348
|
+
payment_adjustment_flag2: Optional[str] = None
|
|
349
|
+
"""Identifies special adjustments made to the payment (outpatient only)"""
|
|
350
|
+
|
|
351
|
+
payment_method_flag: Optional[str] = None
|
|
352
|
+
"""The method used to calculate the allowed amount (outpatient only)"""
|
|
353
|
+
|
|
354
|
+
composite_adjustment_flag: Optional[str] = None
|
|
355
|
+
"""Assists in composite APC determination (outpatient only)"""
|
|
356
|
+
|
|
357
|
+
hcpcs_apc: Annotated[Optional[str], field_name("hcpcsAPC")] = None
|
|
358
|
+
"""Ambulatory Payment Classification code of the line item HCPCS (outpatient only)"""
|
|
359
|
+
|
|
308
360
|
payment_apc: Annotated[Optional[str], field_name("paymentAPC")] = None
|
|
309
361
|
"""Ambulatory Payment Classification"""
|
|
310
362
|
|
|
@@ -332,6 +384,9 @@ class Pricing(BaseModel):
|
|
|
332
384
|
medicare_repricing_note: Optional[str] = None
|
|
333
385
|
"""Note explaining approach for pricing or reason for error"""
|
|
334
386
|
|
|
387
|
+
network_code: Optional[str] = None
|
|
388
|
+
"""Code describing the network used for allowed amount pricing"""
|
|
389
|
+
|
|
335
390
|
allowed_repricing_code: Optional[ClaimRepricingCode] = None
|
|
336
391
|
"""Explains the methodology used to calculate allowed amount (CON, RBP, SCA, or IFO)"""
|
|
337
392
|
|
|
@@ -359,8 +414,145 @@ class Pricing(BaseModel):
|
|
|
359
414
|
pricer_result: Optional[str] = None
|
|
360
415
|
"""Pricer return details"""
|
|
361
416
|
|
|
417
|
+
price_config: Optional[PriceConfig] = None
|
|
418
|
+
"""The configuration used for pricing the claim"""
|
|
419
|
+
|
|
362
420
|
services: list[PricedService] = Field(min_length=1)
|
|
363
421
|
"""Pricing for each service line on the claim"""
|
|
364
422
|
|
|
365
423
|
edit_error: Optional[ResponseError] = None
|
|
366
424
|
"""An error that occurred during some step of the pricing process"""
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
class Step(str, Enum):
|
|
428
|
+
new = "New"
|
|
429
|
+
received = "Received"
|
|
430
|
+
pending = "Pending"
|
|
431
|
+
held = "Held"
|
|
432
|
+
error = "Error"
|
|
433
|
+
input_validated = "Input Validated"
|
|
434
|
+
provider_matched = "Provider Matched"
|
|
435
|
+
edit_complete = "Edit Complete"
|
|
436
|
+
medicare_priced = "Medicare Priced"
|
|
437
|
+
primary_allowed_priced = "Primary Allowed Priced"
|
|
438
|
+
network_allowed_priced = "Network Allowed Priced"
|
|
439
|
+
out_of_network = "Out of Network"
|
|
440
|
+
request_more_info = "Request More Info"
|
|
441
|
+
priced = "Priced"
|
|
442
|
+
returned = "Returned"
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
class Status(str, Enum):
|
|
446
|
+
pending_claim_input_validation = "Claim Input Validation"
|
|
447
|
+
pending_claim_edit_review = "Claim Edit Review"
|
|
448
|
+
pending_provider_matching = "Provider Matching"
|
|
449
|
+
pending_medicare_review = "Medicare Review"
|
|
450
|
+
pending_medicare_calculation = "Medicare Calculation"
|
|
451
|
+
pending_primary_allowed_review = "Primary Allowed Review"
|
|
452
|
+
pending_network_allowed_review = "Network Allowed Review"
|
|
453
|
+
pending_primary_allowed_determination = "Primary Allowed Determination"
|
|
454
|
+
pending_network_allowed_determination = "Network Allowed Determination"
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
class StepAndStatus(BaseModel):
|
|
458
|
+
model_config = camel_case_model_config
|
|
459
|
+
|
|
460
|
+
step: Step
|
|
461
|
+
status: Optional[Status] = None
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
class ClaimStatus(StepAndStatus):
|
|
465
|
+
model_config = camel_case_model_config
|
|
466
|
+
|
|
467
|
+
updated_by: Optional[str] = None
|
|
468
|
+
updated_at: Optional[DateTime] = None
|
|
469
|
+
pricing: Optional[Pricing] = None
|
|
470
|
+
error: Optional[ResponseError] = None
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
status_new = StepAndStatus(step=Step.new)
|
|
474
|
+
"""created by TPA. We use the transaction date as a proxy for this date"""
|
|
475
|
+
|
|
476
|
+
status_received = StepAndStatus(step=Step.received)
|
|
477
|
+
"""received and ready for processing. This is modified date of the file we get from SFTP"""
|
|
478
|
+
|
|
479
|
+
status_held = StepAndStatus(step=Step.held)
|
|
480
|
+
"""held for various reasons"""
|
|
481
|
+
|
|
482
|
+
status_error = StepAndStatus(step=Step.error)
|
|
483
|
+
"""claim encountered an error during processing"""
|
|
484
|
+
|
|
485
|
+
status_input_validated = StepAndStatus(step=Step.input_validated)
|
|
486
|
+
"""claim input has been validated"""
|
|
487
|
+
|
|
488
|
+
status_provider_matched = StepAndStatus(step=Step.provider_matched)
|
|
489
|
+
"""providers in the claim have been matched to the provider system of record"""
|
|
490
|
+
|
|
491
|
+
status_edit_complete = StepAndStatus(step=Step.edit_complete)
|
|
492
|
+
"""claim has been edited and is ready for pricing"""
|
|
493
|
+
|
|
494
|
+
status_medicare_priced = StepAndStatus(step=Step.medicare_priced)
|
|
495
|
+
"""claim has been priced according to Medicare"""
|
|
496
|
+
|
|
497
|
+
status_primary_allowed_priced = StepAndStatus(step=Step.primary_allowed_priced)
|
|
498
|
+
"""claim has been priced according to the primary allowed amount (e.g. contract, RBP, etc.)"""
|
|
499
|
+
|
|
500
|
+
status_network_allowed_priced = StepAndStatus(step=Step.network_allowed_priced)
|
|
501
|
+
"""claim has been priced according to the allowed amount of the network"""
|
|
502
|
+
|
|
503
|
+
status_out_of_network = StepAndStatus(step=Step.out_of_network)
|
|
504
|
+
"""is out of network"""
|
|
505
|
+
|
|
506
|
+
status_request_more_info = StepAndStatus(step=Step.request_more_info)
|
|
507
|
+
"""return claim to trading partner for more information to enable correct processing"""
|
|
508
|
+
|
|
509
|
+
status_priced = StepAndStatus(step=Step.priced)
|
|
510
|
+
"""done pricing"""
|
|
511
|
+
|
|
512
|
+
status_returned = StepAndStatus(step=Step.returned)
|
|
513
|
+
"""returned to TPA"""
|
|
514
|
+
|
|
515
|
+
status_pending_claim_input_validation = StepAndStatus(
|
|
516
|
+
step=Step.pending, status=Status.pending_claim_input_validation
|
|
517
|
+
)
|
|
518
|
+
"""waiting for claim input validation"""
|
|
519
|
+
|
|
520
|
+
status_pending_claim_edit_review = StepAndStatus(
|
|
521
|
+
step=Step.pending, status=Status.pending_claim_edit_review
|
|
522
|
+
)
|
|
523
|
+
"""waiting for claim edit review"""
|
|
524
|
+
|
|
525
|
+
status_pending_provider_matching = StepAndStatus(
|
|
526
|
+
step=Step.pending, status=Status.pending_provider_matching
|
|
527
|
+
)
|
|
528
|
+
"""waiting for provider matching"""
|
|
529
|
+
|
|
530
|
+
status_pending_medicare_review = StepAndStatus(
|
|
531
|
+
step=Step.pending, status=Status.pending_medicare_review
|
|
532
|
+
)
|
|
533
|
+
"""waiting for Medicare amount review"""
|
|
534
|
+
|
|
535
|
+
status_pending_medicare_calculation = StepAndStatus(
|
|
536
|
+
step=Step.pending, status=Status.pending_medicare_calculation
|
|
537
|
+
)
|
|
538
|
+
"""waiting for Medicare amount calculation"""
|
|
539
|
+
|
|
540
|
+
status_pending_primary_allowed_review = StepAndStatus(
|
|
541
|
+
step=Step.pending, status=Status.pending_primary_allowed_review
|
|
542
|
+
)
|
|
543
|
+
"""waiting for primary allowed amount review"""
|
|
544
|
+
|
|
545
|
+
status_pending_network_allowed_review = StepAndStatus(
|
|
546
|
+
step=Step.pending, status=Status.pending_network_allowed_review
|
|
547
|
+
)
|
|
548
|
+
"""waiting for network allowed amount review"""
|
|
549
|
+
|
|
550
|
+
status_pending_primary_allowed_determination = StepAndStatus(
|
|
551
|
+
step=Step.pending, status=Status.pending_primary_allowed_determination
|
|
552
|
+
)
|
|
553
|
+
"""waiting for the primary allowed amount (e.g. contract, RBP rate, etc.) to be determined"""
|
|
554
|
+
|
|
555
|
+
status_pending_network_allowed_determination = StepAndStatus(
|
|
556
|
+
step=Step.pending, status=Status.pending_network_allowed_determination
|
|
557
|
+
)
|
|
558
|
+
"""waiting for allowed amount from the network"""
|
mphapi/py.typed
ADDED
|
File without changes
|