medicafe 0.240419.2__py3-none-any.whl → 0.240517.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.
Potentially problematic release.
This version of medicafe might be problematic. Click here for more details.
- MediBot/MediBot.bat +166 -38
- MediBot/MediBot.py +74 -44
- MediBot/MediBot_Crosswalk_Library.py +280 -0
- MediBot/MediBot_Preprocessor.py +155 -191
- MediBot/MediBot_Preprocessor_lib.py +357 -0
- MediBot/MediBot_UI.py +80 -30
- MediBot/MediBot_dataformat_library.py +88 -35
- MediBot/MediBot_docx_decoder.py +80 -0
- MediBot/update_medicafe.py +46 -8
- MediLink/MediLink.py +138 -34
- MediLink/MediLink_837p_encoder.py +319 -209
- MediLink/MediLink_837p_encoder_library.py +453 -242
- MediLink/MediLink_API_v2.py +174 -0
- MediLink/MediLink_APIs.py +137 -0
- MediLink/MediLink_ConfigLoader.py +44 -32
- MediLink/MediLink_DataMgmt.py +85 -33
- MediLink/MediLink_Down.py +12 -35
- MediLink/MediLink_ERA_decoder.py +4 -4
- MediLink/MediLink_Gmail.py +99 -3
- MediLink/MediLink_Mailer.py +7 -0
- MediLink/MediLink_Scheduler.py +41 -0
- MediLink/MediLink_UI.py +19 -17
- MediLink/MediLink_Up.py +297 -31
- MediLink/MediLink_batch.bat +1 -1
- MediLink/test.py +74 -0
- medicafe-0.240517.0.dist-info/METADATA +53 -0
- medicafe-0.240517.0.dist-info/RECORD +39 -0
- {medicafe-0.240419.2.dist-info → medicafe-0.240517.0.dist-info}/WHEEL +5 -5
- medicafe-0.240419.2.dist-info/METADATA +0 -19
- medicafe-0.240419.2.dist-info/RECORD +0 -32
- {medicafe-0.240419.2.dist-info → medicafe-0.240517.0.dist-info}/LICENSE +0 -0
- {medicafe-0.240419.2.dist-info → medicafe-0.240517.0.dist-info}/top_level.txt +0 -0
|
@@ -1,8 +1,32 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from datetime import datetime
|
|
3
3
|
import re #for addresses
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
|
|
5
|
+
# Add parent directory of the project to the Python path
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
project_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
9
|
+
sys.path.append(project_dir)
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from MediLink import MediLink_ConfigLoader
|
|
13
|
+
config, crosswalk = MediLink_ConfigLoader.load_configuration()
|
|
14
|
+
except ImportError:
|
|
15
|
+
from MediLink_ConfigLoader import load_configuration
|
|
16
|
+
config, crosswalk = load_configuration()
|
|
17
|
+
|
|
18
|
+
from MediBot_Preprocessor_lib import open_csv_for_editing, initialize
|
|
19
|
+
from MediBot_UI import manage_script_pause, app_control
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
- [X] (TEST) Address Formatting 30-character Limit:
|
|
24
|
+
(LOW) Address the issue where the format_street function in Medibot may produce addresses exceeding
|
|
25
|
+
the 30-character limit. Current stop-gap is removing period characters and the abbreviation "APT"
|
|
26
|
+
surrounded by spaces from all records as a temporary solution.
|
|
27
|
+
If the address still exceeds 30 characters, the function will attempt to remove spaces from right to left
|
|
28
|
+
until it reaches 30 significant digits or runs out of spaces, then truncate to 30 characters if necessary.
|
|
29
|
+
"""
|
|
6
30
|
|
|
7
31
|
# Bring in all the constants
|
|
8
32
|
initialize(config)
|
|
@@ -11,11 +35,13 @@ initialize(config)
|
|
|
11
35
|
def format_name(value):
|
|
12
36
|
if ',' in value:
|
|
13
37
|
return value
|
|
14
|
-
hyphenated_name_pattern = r'(?P<First>[\w-]+)\s+(?P<Middle>[\w-]
|
|
38
|
+
hyphenated_name_pattern = r'(?P<First>[\w-]+)\s+(?P<Middle>[\w-]?)\s+(?P<Last>[\w-]+)'
|
|
15
39
|
match = re.match(hyphenated_name_pattern, value)
|
|
16
40
|
if match:
|
|
17
41
|
first_name = match.group('First')
|
|
18
42
|
middle_name = match.group('Middle') or ''
|
|
43
|
+
if len(middle_name) > 1:
|
|
44
|
+
middle_name = middle_name[0] # take only the first character
|
|
19
45
|
last_name = match.group('Last')
|
|
20
46
|
return '{}, {} {}'.format(last_name, first_name, middle_name).strip()
|
|
21
47
|
parts = value.split()
|
|
@@ -43,70 +69,97 @@ def format_policy(value):
|
|
|
43
69
|
def format_gender(value):
|
|
44
70
|
return value[0].upper()
|
|
45
71
|
|
|
72
|
+
def enforce_significant_length(output):
|
|
73
|
+
# Replace spaces with a placeholder that counts as one significant digit
|
|
74
|
+
temp_output = output.replace('{Space}', ' ')
|
|
75
|
+
|
|
76
|
+
# Check if the number of significant digits exceeds 30
|
|
77
|
+
if len(temp_output) > 30:
|
|
78
|
+
|
|
79
|
+
# First line of defense: Replace ' APT ' with ' #' if the original length is longer than 30 characters.
|
|
80
|
+
temp_output = temp_output.replace(' APT ', ' #')
|
|
81
|
+
|
|
82
|
+
# Remove spaces in a controlled manner from right to left if still too long
|
|
83
|
+
while len(temp_output) > 30:
|
|
84
|
+
# Find the last space
|
|
85
|
+
last_space_index = temp_output.rfind(' ')
|
|
86
|
+
if last_space_index == -1:
|
|
87
|
+
break
|
|
88
|
+
# Remove the last space
|
|
89
|
+
temp_output = temp_output[:last_space_index] + temp_output[last_space_index+7:]
|
|
90
|
+
|
|
91
|
+
# If still greater than 30, truncate to 30 characters
|
|
92
|
+
if len(temp_output) > 30:
|
|
93
|
+
temp_output = temp_output[:30]
|
|
94
|
+
|
|
95
|
+
# Replace placeholder back with actual space for final return
|
|
96
|
+
return temp_output.replace(' ', '{Space}')
|
|
97
|
+
|
|
46
98
|
def format_street(value, csv_data, reverse_mapping, parsed_address_components):
|
|
47
|
-
|
|
48
|
-
|
|
99
|
+
# Temporarily disable script pause status
|
|
100
|
+
app_control.set_pause_status(False)
|
|
49
101
|
|
|
50
|
-
# Remove
|
|
102
|
+
# Remove period characters.
|
|
51
103
|
value = value.replace('.', '')
|
|
52
104
|
|
|
53
|
-
#
|
|
105
|
+
# Proceed only if there's a comma, indicating a likely full address
|
|
54
106
|
if ',' in value:
|
|
55
107
|
try:
|
|
56
|
-
|
|
108
|
+
MediLink_ConfigLoader.log("Attempting to resolve address via regex...")
|
|
109
|
+
# Retrieve common city names from configuration and prepare a regex pattern
|
|
57
110
|
common_cities = config.get('cities', [])
|
|
58
|
-
|
|
59
|
-
# Convert cities to a case-insensitive regex pattern
|
|
60
111
|
city_pattern = '|'.join(re.escape(city) for city in common_cities)
|
|
61
112
|
city_regex_pattern = r'(?P<City>{})'.format(city_pattern)
|
|
62
113
|
city_regex = re.compile(city_regex_pattern, re.IGNORECASE)
|
|
63
114
|
|
|
64
|
-
#
|
|
115
|
+
# Search for a common city in the address
|
|
65
116
|
city_match = city_regex.search(value)
|
|
66
117
|
|
|
67
118
|
if city_match:
|
|
68
|
-
|
|
119
|
+
# Extract city name and partition the value around it
|
|
120
|
+
city = city_match.group('City').upper()
|
|
69
121
|
street, _, remainder = value.partition(city)
|
|
70
|
-
|
|
71
|
-
#
|
|
122
|
+
|
|
123
|
+
# Regex pattern to find state and zip code in the remainder
|
|
72
124
|
address_pattern = r',\s*(?P<State>[A-Z]{2})\s*(?P<Zip>\d{5}(?:-\d{4})?)?'
|
|
73
|
-
|
|
74
125
|
match = re.search(address_pattern, remainder)
|
|
75
|
-
|
|
126
|
+
|
|
76
127
|
if match:
|
|
77
|
-
# Update
|
|
128
|
+
# Update parsed address components
|
|
78
129
|
parsed_address_components['City'] = city
|
|
79
130
|
parsed_address_components['State'] = match.group('State')
|
|
80
131
|
parsed_address_components['Zip Code'] = match.group('Zip')
|
|
81
|
-
|
|
82
|
-
return street.strip()
|
|
132
|
+
# Return formatted street address, enforcing significant length
|
|
133
|
+
return enforce_significant_length(street.strip())
|
|
83
134
|
else:
|
|
84
|
-
# Fallback
|
|
85
|
-
# value = street + ', ' + city + remainder
|
|
135
|
+
# Fallback regex for parsing addresses without a common city
|
|
86
136
|
address_pattern = r'(?P<Street>[\w\s]+),?\s+(?P<City>[\w\s]+),\s*(?P<State>[A-Z]{2})\s*(?P<Zip>\d{5}(-\d{4})?)'
|
|
87
137
|
match = re.match(address_pattern, value)
|
|
88
138
|
|
|
89
139
|
if match:
|
|
90
|
-
# Update
|
|
140
|
+
# Update parsed address components
|
|
91
141
|
parsed_address_components['City'] = match.group('City')
|
|
92
142
|
parsed_address_components['State'] = match.group('State')
|
|
93
143
|
parsed_address_components['Zip Code'] = match.group('Zip')
|
|
94
|
-
|
|
95
|
-
return match.group('Street').strip()
|
|
144
|
+
# Return formatted street address, enforcing significant length
|
|
145
|
+
return enforce_significant_length(match.group('Street').strip())
|
|
96
146
|
|
|
97
147
|
except Exception as e:
|
|
148
|
+
# Handle exceptions by logging and offering to correct data manually
|
|
98
149
|
print("Address format error: Unable to parse address '{}'. Error: {}".format(value, e))
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return value.replace(' ', '{Space}')
|
|
150
|
+
app_control.set_pause_status(True)
|
|
151
|
+
open_csv_for_editing(CSV_FILE_PATH)
|
|
152
|
+
manage_script_pause(csv_data, e, reverse_mapping)
|
|
153
|
+
# Return original value with spaces formatted, enforcing significant length
|
|
154
|
+
return enforce_significant_length(value.replace(' ', '{Space}'))
|
|
104
155
|
else:
|
|
105
|
-
# If no comma is present,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
156
|
+
# If no comma is present, treat the input as a simple street name
|
|
157
|
+
formatted_value = value.replace(' ', '{Space}')
|
|
158
|
+
enforced_format = enforce_significant_length(formatted_value)
|
|
159
|
+
return enforced_format
|
|
160
|
+
|
|
161
|
+
# Fallback return in case no address components are matched even though a comma was present
|
|
162
|
+
return enforce_significant_length(value.replace(' ', '{Space}'))
|
|
110
163
|
|
|
111
164
|
def format_zip(value):
|
|
112
165
|
# Ensure the value is a string, in case it's provided as an integer
|
|
@@ -138,7 +191,7 @@ def format_data(medisoft_field, value, csv_data, reverse_mapping, parsed_address
|
|
|
138
191
|
elif medisoft_field == 'Secondary Group Number':
|
|
139
192
|
formatted_value = format_policy(value)
|
|
140
193
|
else:
|
|
141
|
-
formatted_value = value
|
|
194
|
+
formatted_value = str(value) # Ensure value is always a string
|
|
142
195
|
|
|
143
196
|
formatted_value = formatted_value.replace(',', '{,}').replace(' ', '{Space}')
|
|
144
197
|
ahk_command = 'SendInput, {}{{Enter}}'.format(formatted_value)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Using docx-utils 0.1.3,
|
|
3
|
+
|
|
4
|
+
This script parses a .docx file containing a table of patient information and extracts
|
|
5
|
+
relevant data into a dictionary. Each row in the table corresponds to a new patient,
|
|
6
|
+
and the data from each cell is parsed into specific variables. The resulting dictionary
|
|
7
|
+
uses the 'Patient ID Number' as keys and lists containing 'Diagnosis Code',
|
|
8
|
+
'Left or Right Eye', and 'Femto yes or no' as values.
|
|
9
|
+
|
|
10
|
+
Functions:
|
|
11
|
+
parse_docx(filepath): Reads the .docx file and constructs the patient data dictionary.
|
|
12
|
+
parse_patient_id(text): Extracts the Patient ID Number from the text.
|
|
13
|
+
parse_diagnosis_code(text): Extracts the Diagnosis Code from the text.
|
|
14
|
+
parse_left_or_right_eye(text): Extracts the eye information (Left or Right) from the text.
|
|
15
|
+
parse_femto_yes_or_no(text): Extracts the Femto information (yes or no) from the text.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from docx import Document
|
|
19
|
+
|
|
20
|
+
def parse_docx(filepath):
|
|
21
|
+
# Open the .docx file
|
|
22
|
+
doc = Document(filepath)
|
|
23
|
+
|
|
24
|
+
# Initialize the dictionary to store data
|
|
25
|
+
patient_data = {}
|
|
26
|
+
|
|
27
|
+
# Assuming the first table contains the required data
|
|
28
|
+
table = doc.tables[0]
|
|
29
|
+
|
|
30
|
+
# Iterate over the rows in the table
|
|
31
|
+
for row in table.rows[1:]: # Skip header row if it exists
|
|
32
|
+
cells = row.cells
|
|
33
|
+
|
|
34
|
+
# Extract and parse data from each cell
|
|
35
|
+
patient_id = parse_patient_id(cells[0].text.strip())
|
|
36
|
+
diagnosis_code = parse_diagnosis_code(cells[1].text.strip())
|
|
37
|
+
left_or_right_eye = parse_left_or_right_eye(cells[2].text.strip())
|
|
38
|
+
femto_yes_or_no = parse_femto_yes_or_no(cells[3].text.strip())
|
|
39
|
+
|
|
40
|
+
# Construct the dictionary entry
|
|
41
|
+
patient_data[patient_id] = [diagnosis_code, left_or_right_eye, femto_yes_or_no]
|
|
42
|
+
|
|
43
|
+
return patient_data
|
|
44
|
+
|
|
45
|
+
def parse_patient_id(text):
|
|
46
|
+
# Implement parsing logic for Patient ID Number
|
|
47
|
+
# Example: Assume the ID is the first part of the text, separated by a space or newline
|
|
48
|
+
return text.split()[0]
|
|
49
|
+
|
|
50
|
+
def parse_diagnosis_code(text):
|
|
51
|
+
# Implement parsing logic for Diagnosis Code
|
|
52
|
+
# Example: Extract the code from a known pattern or location in the text
|
|
53
|
+
return text.split(':')[1].strip() if ':' in text else text
|
|
54
|
+
|
|
55
|
+
def parse_left_or_right_eye(text):
|
|
56
|
+
# Implement parsing logic for Left or Right Eye
|
|
57
|
+
# Example: Assume the text contains 'Left' or 'Right' and extract it
|
|
58
|
+
if 'Left' in text:
|
|
59
|
+
return 'Left'
|
|
60
|
+
elif 'Right' in text:
|
|
61
|
+
return 'Right'
|
|
62
|
+
else:
|
|
63
|
+
return 'Unknown'
|
|
64
|
+
|
|
65
|
+
def parse_femto_yes_or_no(text):
|
|
66
|
+
# Implement parsing logic for Femto yes or no
|
|
67
|
+
# Example: Check for presence of keywords 'yes' or 'no'
|
|
68
|
+
if 'yes' in text.lower():
|
|
69
|
+
return 'Yes'
|
|
70
|
+
elif 'no' in text.lower():
|
|
71
|
+
return 'No'
|
|
72
|
+
else:
|
|
73
|
+
return 'Unknown'
|
|
74
|
+
|
|
75
|
+
# Placeholder function call (replace 'path_to_docx' with the actual file path)
|
|
76
|
+
filepath = 'path_to_docx'
|
|
77
|
+
patient_data_dict = parse_docx(filepath)
|
|
78
|
+
|
|
79
|
+
# Print the resulting dictionary
|
|
80
|
+
print(patient_data_dict)
|
MediBot/update_medicafe.py
CHANGED
|
@@ -1,17 +1,55 @@
|
|
|
1
1
|
import subprocess
|
|
2
2
|
import sys
|
|
3
3
|
from tqdm import tqdm
|
|
4
|
+
import requests
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
def check_internet_connection():
|
|
8
|
+
try:
|
|
9
|
+
requests.get("http://www.google.com", timeout=5)
|
|
10
|
+
return True
|
|
11
|
+
except requests.ConnectionError:
|
|
12
|
+
return False
|
|
4
13
|
|
|
5
14
|
def upgrade_medicafe(package):
|
|
6
15
|
try:
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
# Check internet connection
|
|
17
|
+
if not check_internet_connection():
|
|
18
|
+
print("Error: No internet connection detected. Please check your internet connection and try again.")
|
|
19
|
+
sys.exit(1)
|
|
20
|
+
|
|
21
|
+
total_progress = 200 # Total progress for two runs
|
|
22
|
+
|
|
23
|
+
with tqdm(total=total_progress, desc="Upgrading %s" % package, unit="%") as progress_bar:
|
|
24
|
+
stdout_accumulator = b""
|
|
25
|
+
stderr_accumulator = b""
|
|
26
|
+
|
|
27
|
+
for _ in range(2): # Run pip install twice
|
|
28
|
+
process = subprocess.Popen([sys.executable, '-m', 'pip', 'install', '--upgrade', package, '--no-cache-dir', '--disable-pip-version-check', '--no-deps'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
29
|
+
stdout, stderr = process.communicate()
|
|
30
|
+
stdout_accumulator += stdout
|
|
31
|
+
stderr_accumulator += stderr
|
|
32
|
+
|
|
33
|
+
if process.returncode != 0:
|
|
34
|
+
# If the return code is non-zero, print error details
|
|
35
|
+
print("Error: Upgrade failed. Details:")
|
|
36
|
+
print("stdout:", stdout)
|
|
37
|
+
print("stderr:", stderr)
|
|
38
|
+
sys.exit(1)
|
|
39
|
+
|
|
40
|
+
progress_bar.update(total_progress // 2) # Update progress bar
|
|
41
|
+
|
|
42
|
+
# Add a 3-second sleep between runs
|
|
43
|
+
time.sleep(3)
|
|
44
|
+
|
|
45
|
+
progress_bar.update(total_progress // 2) # Update progress bar
|
|
46
|
+
print("stdout:", stdout_accumulator.decode("utf-8"))
|
|
47
|
+
print("stderr:", stderr_accumulator.decode("utf-8"))
|
|
48
|
+
time.sleep(1)
|
|
49
|
+
except Exception as e:
|
|
50
|
+
# Log any other exceptions
|
|
51
|
+
print("Error:", e)
|
|
52
|
+
time.sleep(3)
|
|
15
53
|
sys.exit(1)
|
|
16
54
|
|
|
17
55
|
if __name__ == "__main__":
|
MediLink/MediLink.py
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import os
|
|
2
|
-
import MediLink_ConfigLoader
|
|
3
|
-
import MediLink_837p_encoder
|
|
4
|
-
import logging
|
|
5
2
|
import MediLink_Down
|
|
6
3
|
import MediLink_Up
|
|
4
|
+
import MediLink_ConfigLoader
|
|
5
|
+
import MediLink_837p_encoder
|
|
7
6
|
|
|
8
7
|
# For UI Functions
|
|
9
8
|
import os
|
|
10
9
|
import MediLink_UI # Import UI module for handling all user interfaces
|
|
10
|
+
from tqdm import tqdm
|
|
11
|
+
|
|
12
|
+
# Add parent directory of the project to the Python path
|
|
13
|
+
import sys
|
|
14
|
+
project_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
15
|
+
sys.path.append(project_dir)
|
|
16
|
+
|
|
17
|
+
from MediBot import MediBot_Preprocessor_lib
|
|
18
|
+
load_insurance_data_from_mains = MediBot_Preprocessor_lib.load_insurance_data_from_mains
|
|
19
|
+
from MediBot import MediBot_Crosswalk_Library
|
|
11
20
|
|
|
12
21
|
"""
|
|
13
22
|
Development Tasks for Backend Enhancement in MediSoft Claims Submittal (MediLink) Script:
|
|
@@ -22,17 +31,43 @@ any new endpoint changes to Optum. May need to "de-confirm" patients, but leave
|
|
|
22
31
|
confirmed endpoints. This should be similar logic to if the user made a mistake and wants to go back and fix it.
|
|
23
32
|
These tasks involve backend enhancements such as dynamic configurations, file detection improvements, file transmission verification, automation of response file handling, and management of intermediate files and transmission failures.
|
|
24
33
|
|
|
25
|
-
TODO
|
|
26
|
-
TODO Availity has a response file that says "File was received at TIME. File was sent for processing." as a confirmation
|
|
34
|
+
TODO (Low) Availity has a response file that says "File was received at TIME. File was sent for processing." as a confirmation
|
|
27
35
|
that sits in the SendFiles folder after a submittal.
|
|
28
36
|
|
|
37
|
+
TODO (Crosswalk) When an endpoint is updated in the UI, the crosswalk should also be updated and saved for that payer ID because that payer ID
|
|
38
|
+
would basically forever need to be going to that endpoint for any patient. the suggested_endpoint should eventually be always correct.
|
|
39
|
+
|
|
29
40
|
BUG Suggested Endpoint when you say 'n' to proceed with transmission is not getting updated with the endpoint
|
|
30
41
|
that was selected previously by the user. However, when we go back to the confirmation list, we do have a persist of the assignment.
|
|
31
42
|
This can be confusing for the user.
|
|
32
|
-
"""
|
|
33
43
|
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
MediLink
|
|
45
|
+
| - import MediLink_Down
|
|
46
|
+
| - import MediLink_ERA_decoder
|
|
47
|
+
| | - from MediLink_ConfigLoader import load_configuration
|
|
48
|
+
| | | - None
|
|
49
|
+
| | - from MediLink_DataMgmt import consolidate_csvs
|
|
50
|
+
| | | - from MediLink import MediLink_ConfigLoader
|
|
51
|
+
| | - None
|
|
52
|
+
| - from MediLink_DataMgmt import operate_winscp
|
|
53
|
+
| - from MediLink import MediLink_ConfigLoader
|
|
54
|
+
| - None
|
|
55
|
+
| - import MediLink_Up
|
|
56
|
+
| - None
|
|
57
|
+
| - import MediLink_ConfigLoader
|
|
58
|
+
| - None
|
|
59
|
+
| - import MediLink_837p_encoder
|
|
60
|
+
| - import MediLink_ConfigLoader
|
|
61
|
+
| | - None
|
|
62
|
+
| - from MediLink_DataMgmt import parse_fixed_width_data, read_fixed_width_data
|
|
63
|
+
| - from MediLink import MediLink_ConfigLoader
|
|
64
|
+
| - None
|
|
65
|
+
| - import MediLink_837p_encoder_library
|
|
66
|
+
| - from MediLink import MediLink_ConfigLoader
|
|
67
|
+
| - None
|
|
68
|
+
| - import MediLink_UI
|
|
69
|
+
| - None
|
|
70
|
+
"""
|
|
36
71
|
|
|
37
72
|
def detect_and_display_file_summaries(directory_path, config, crosswalk):
|
|
38
73
|
"""
|
|
@@ -88,15 +123,42 @@ def extract_and_suggest_endpoint(file_path, config, crosswalk):
|
|
|
88
123
|
"""
|
|
89
124
|
detailed_patient_data = []
|
|
90
125
|
|
|
91
|
-
|
|
92
|
-
|
|
126
|
+
# Load insurance data from MAINS to create a mapping from insurance names to their respective IDs
|
|
127
|
+
insurance_to_id = load_insurance_data_from_mains(config)
|
|
128
|
+
MediLink_ConfigLoader.log("Insurance data loaded from MAINS. {} insurance providers found.".format(len(insurance_to_id)))
|
|
129
|
+
|
|
130
|
+
for personal_info, insurance_info, service_info in MediLink_837p_encoder.read_fixed_width_data(file_path):
|
|
131
|
+
parsed_data = MediLink_837p_encoder.parse_fixed_width_data(personal_info, insurance_info, service_info, config.get('MediLink_Config', config))
|
|
93
132
|
|
|
94
133
|
primary_insurance = parsed_data.get('INAME')
|
|
95
134
|
|
|
96
|
-
#
|
|
97
|
-
|
|
135
|
+
# Retrieve the insurance ID associated with the primary insurance
|
|
136
|
+
insurance_id = insurance_to_id.get(primary_insurance)
|
|
137
|
+
MediLink_ConfigLoader.log("Primary insurance ID retrieved for '{}': {}".format(primary_insurance, insurance_id))
|
|
138
|
+
|
|
139
|
+
# Use insurance ID to retrieve the payer ID(s) associated with the insurance
|
|
140
|
+
payer_ids = []
|
|
141
|
+
if insurance_id:
|
|
142
|
+
for payer_id, payer_data in crosswalk.get('payer_id', {}).items():
|
|
143
|
+
medisoft_ids = [str(id) for id in payer_data.get('medisoft_id', [])]
|
|
144
|
+
# MediLink_ConfigLoader.log("Payer ID: {}, Medisoft IDs: {}".format(payer_id, medisoft_ids))
|
|
145
|
+
if str(insurance_id) in medisoft_ids:
|
|
146
|
+
payer_ids.append(payer_id)
|
|
147
|
+
if payer_ids:
|
|
148
|
+
MediLink_ConfigLoader.log("Payer IDs retrieved for insurance '{}': {}".format(primary_insurance, payer_ids))
|
|
149
|
+
else:
|
|
150
|
+
MediLink_ConfigLoader.log("No payer IDs found for insurance '{}'".format(primary_insurance))
|
|
98
151
|
|
|
99
|
-
#
|
|
152
|
+
# Find the suggested endpoint from the crosswalk based on the payer IDs
|
|
153
|
+
suggested_endpoint = 'AVAILITY' # Default endpoint if no matching payer IDs found
|
|
154
|
+
if payer_ids:
|
|
155
|
+
payer_id = payer_ids[0] # Select the first payer ID
|
|
156
|
+
suggested_endpoint = crosswalk['payer_id'].get(payer_id, {}).get('endpoint', 'AVAILITY')
|
|
157
|
+
MediLink_ConfigLoader.log("Suggested endpoint for payer ID '{}': {}".format(payer_id, suggested_endpoint))
|
|
158
|
+
else:
|
|
159
|
+
MediLink_ConfigLoader.log("No suggested endpoint found for payer IDs: {}".format(payer_ids))
|
|
160
|
+
|
|
161
|
+
# Enrich detailed patient data with additional information and suggested endpoint
|
|
100
162
|
detailed_data = parsed_data.copy() # Copy parsed_data to avoid modifying the original dictionary
|
|
101
163
|
detailed_data.update({
|
|
102
164
|
'file_path': file_path,
|
|
@@ -134,20 +196,50 @@ def organize_patient_data_by_endpoint(detailed_patient_data):
|
|
|
134
196
|
def check_for_new_remittances(config):
|
|
135
197
|
print("\nChecking for new files across all endpoints...")
|
|
136
198
|
endpoints = config['MediLink_Config']['endpoints']
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
199
|
+
processed_endpoints = []
|
|
200
|
+
|
|
201
|
+
if isinstance(endpoints, dict): # BUG This check can probably be removed later.
|
|
202
|
+
for endpoint_key, endpoint_info in tqdm(endpoints.items(), desc="Processing endpoints"):
|
|
203
|
+
if 'remote_directory_down' in endpoint_info: # Check if the 'remote_directory_down' key exists
|
|
204
|
+
#print("Processing endpoint: ", endpoint_info['name'])
|
|
205
|
+
# BUG (Debug and verbosity removal) this is really for debug only. Positive statements can be muted.
|
|
206
|
+
try:
|
|
207
|
+
ERA_path = MediLink_Down.main(desired_endpoint=endpoint_key)
|
|
208
|
+
processed_endpoints.append((endpoint_info['name'], ERA_path))
|
|
209
|
+
MediLink_ConfigLoader.log("Results for {} saved to: {}".format(endpoint_info['name'], ERA_path))
|
|
210
|
+
# TODO (Low SFTP - Download side) This needs to check to see if this actually worked maybe winscplog before saying it completed successfully
|
|
211
|
+
# Check if there is commonality with the upload side so we can use the same validation function.
|
|
212
|
+
except Exception as e:
|
|
213
|
+
print("An error occurred while checking remittances for {}: {}".format(endpoint_info['name'], e))
|
|
214
|
+
else:
|
|
215
|
+
MediLink_ConfigLoader.log("Skipping endpoint '{}' as it does not have 'remote_directory_down' configured.".format(endpoint_info['name']))
|
|
216
|
+
else:
|
|
217
|
+
print("Error: Endpoint config is not a 'dictionary' as expected.")
|
|
218
|
+
# Check if all ERA paths are the same
|
|
219
|
+
unique_era_paths = set(path for _, path in processed_endpoints)
|
|
220
|
+
if len(unique_era_paths) == 1:
|
|
221
|
+
common_era_path = unique_era_paths.pop() # Get the common ERA path
|
|
222
|
+
endpoints_list = ", ".join(endpoint for endpoint, _ in processed_endpoints)
|
|
223
|
+
print("\nProcessed Endpoints: {}".format(endpoints_list))
|
|
224
|
+
print("File located at: {}\n".format(common_era_path))
|
|
225
|
+
# TODO (MediPost) These prints will eventually be logs when MediPost is made.
|
|
226
|
+
|
|
227
|
+
else:
|
|
228
|
+
if processed_endpoints:
|
|
229
|
+
print("\nProcessed Endpoints:")
|
|
230
|
+
for endpoint, path in processed_endpoints:
|
|
231
|
+
print("Endpoint: {}, ERA Path: {}".format(endpoint, path))
|
|
232
|
+
else:
|
|
233
|
+
print("No endpoints were processed.")
|
|
146
234
|
|
|
147
235
|
def user_decision_on_suggestions(detailed_patient_data, config):
|
|
148
236
|
"""
|
|
149
237
|
Presents the user with all patient summaries and suggested endpoints,
|
|
150
238
|
then asks for confirmation to proceed with all or specify adjustments manually.
|
|
239
|
+
|
|
240
|
+
BUG (Med suggested_endpoint) The display summary suggested_endpoint key isn't updating per the user's decision
|
|
241
|
+
although the user decision is persisting. Possibly consider making the current/suggested/confirmed endpoint
|
|
242
|
+
part of a class that the user can interact with via these menus. Probably better handling that way.
|
|
151
243
|
"""
|
|
152
244
|
# Display summaries of patient details and endpoints.
|
|
153
245
|
MediLink_UI.display_patient_summaries(detailed_patient_data)
|
|
@@ -174,15 +266,19 @@ def confirm_all_suggested_endpoints(detailed_patient_data):
|
|
|
174
266
|
def select_and_adjust_files(detailed_patient_data, config):
|
|
175
267
|
"""
|
|
176
268
|
Allows users to select patients and adjust their endpoints by interfacing with UI functions.
|
|
269
|
+
|
|
270
|
+
BUG (Med suggested_endpoint) After the user is done making their selection (probably via a class?),
|
|
271
|
+
Then suggested_endpoint should update to persist the user selection as priority over its original suggestion.
|
|
272
|
+
Which means the crosswalk should persist the change in the endpoint as well.
|
|
177
273
|
"""
|
|
178
274
|
# Display options for patients
|
|
179
275
|
MediLink_UI.display_patient_options(detailed_patient_data)
|
|
180
276
|
|
|
181
277
|
# Get user-selected indices for adjustment
|
|
182
278
|
selected_indices = MediLink_UI.get_selected_indices(len(detailed_patient_data))
|
|
183
|
-
|
|
184
|
-
#
|
|
185
|
-
|
|
279
|
+
|
|
280
|
+
# Get an ordered list of endpoint keys
|
|
281
|
+
endpoint_keys = list(config['MediLink_Config']['endpoints'].keys())
|
|
186
282
|
|
|
187
283
|
# Iterate over each selected index and process endpoint changes
|
|
188
284
|
for i in selected_indices:
|
|
@@ -190,20 +286,20 @@ def select_and_adjust_files(detailed_patient_data, config):
|
|
|
190
286
|
MediLink_UI.display_patient_for_adjustment(data['patient_name'], data.get('suggested_endpoint', 'N/A'))
|
|
191
287
|
|
|
192
288
|
endpoint_change = MediLink_UI.get_endpoint_decision()
|
|
193
|
-
|
|
194
289
|
if endpoint_change == 'y':
|
|
195
|
-
MediLink_UI.display_endpoint_options(
|
|
196
|
-
|
|
290
|
+
MediLink_UI.display_endpoint_options(config['MediLink_Config']['endpoints'])
|
|
291
|
+
endpoint_index = int(MediLink_UI.get_new_endpoint_choice()) - 1 # Adjusting for zero-based index
|
|
197
292
|
|
|
198
|
-
if
|
|
199
|
-
|
|
200
|
-
|
|
293
|
+
if 0 <= endpoint_index < len(endpoint_keys):
|
|
294
|
+
selected_endpoint_key = endpoint_keys[endpoint_index]
|
|
295
|
+
data['confirmed_endpoint'] = selected_endpoint_key
|
|
296
|
+
print("Endpoint changed to {0} for patient {1}.".format(config['MediLink_Config']['endpoints'][selected_endpoint_key]['name'], data['patient_name']))
|
|
297
|
+
# BUG (Med, Crosswalk & suggested_endpoint) Probably update crosswalk and suggested endpoint here???
|
|
201
298
|
else:
|
|
202
299
|
print("Invalid selection. Keeping the suggested endpoint.")
|
|
203
300
|
else:
|
|
204
301
|
data['confirmed_endpoint'] = data.get('suggested_endpoint', 'N/A')
|
|
205
302
|
|
|
206
|
-
# Return the updated data
|
|
207
303
|
return detailed_patient_data
|
|
208
304
|
|
|
209
305
|
def main_menu():
|
|
@@ -212,7 +308,14 @@ def main_menu():
|
|
|
212
308
|
including loading configurations and managing user input for menu selections.
|
|
213
309
|
"""
|
|
214
310
|
# Load configuration settings and display the initial welcome message.
|
|
215
|
-
config, crosswalk = MediLink_ConfigLoader.load_configuration()
|
|
311
|
+
config, crosswalk = MediLink_ConfigLoader.load_configuration()
|
|
312
|
+
|
|
313
|
+
# Check to make sure payer_id key is available in crosswalk, otherwise, go through that crosswalk initialization flow
|
|
314
|
+
MediBot_Crosswalk_Library.check_and_initialize_crosswalk(config)
|
|
315
|
+
|
|
316
|
+
# Check if the application is in test mode
|
|
317
|
+
if config.get("MediLink_Config", {}).get("TestMode", False):
|
|
318
|
+
print("\n--- MEDILINK TEST MODE --- \nTo enable full functionality, please update the config file \nand set 'TestMode' to 'false'.")
|
|
216
319
|
|
|
217
320
|
# Display Welcome Message
|
|
218
321
|
MediLink_UI.display_welcome()
|
|
@@ -265,7 +368,8 @@ def handle_submission(detailed_patient_data, config):
|
|
|
265
368
|
if MediLink_Up.confirm_transmission(organized_data):
|
|
266
369
|
if MediLink_Up.check_internet_connection():
|
|
267
370
|
# Submit claims if internet connectivity is confirmed.
|
|
268
|
-
MediLink_Up.submit_claims(organized_data, config)
|
|
371
|
+
_ = MediLink_Up.submit_claims(organized_data, config)
|
|
372
|
+
# TODO submit_claims will have a receipt return in the future.
|
|
269
373
|
else:
|
|
270
374
|
# Notify the user of an internet connection error.
|
|
271
375
|
print("Internet connection error. Please ensure you're connected and try again.")
|