medicafe 0.250720.0__py3-none-any.whl → 0.250720.1__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.
- MediLink/MediLink_837p_cob_library.py +21 -51
- MediLink/MediLink_837p_encoder_library.py +24 -117
- MediLink/MediLink_837p_utilities.py +264 -0
- MediLink/MediLink_Deductible.py +64 -8
- MediLink/MediLink_Deductible_Validator.py +51 -7
- {medicafe-0.250720.0.dist-info → medicafe-0.250720.1.dist-info}/METADATA +1 -1
- {medicafe-0.250720.0.dist-info → medicafe-0.250720.1.dist-info}/RECORD +10 -9
- {medicafe-0.250720.0.dist-info → medicafe-0.250720.1.dist-info}/LICENSE +0 -0
- {medicafe-0.250720.0.dist-info → medicafe-0.250720.1.dist-info}/WHEEL +0 -0
- {medicafe-0.250720.0.dist-info → medicafe-0.250720.1.dist-info}/top_level.txt +0 -0
|
@@ -56,52 +56,23 @@ project_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
|
56
56
|
if project_dir not in sys.path:
|
|
57
57
|
sys.path.append(project_dir)
|
|
58
58
|
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
MediLink_ConfigLoader.log("Warning: Could not import encoder functions: {}".format(e), level="WARNING")
|
|
77
|
-
return None
|
|
78
|
-
|
|
79
|
-
# Initialize encoder functions with fallback
|
|
80
|
-
_encoder_functions = safe_import_encoder_functions()
|
|
81
|
-
|
|
82
|
-
def get_convert_date_format():
|
|
83
|
-
"""
|
|
84
|
-
Safely gets the convert_date_format function with fallback implementation.
|
|
85
|
-
|
|
86
|
-
Returns:
|
|
87
|
-
- convert_date_format function or fallback implementation
|
|
88
|
-
"""
|
|
89
|
-
if _encoder_functions and 'convert_date_format' in _encoder_functions:
|
|
90
|
-
return _encoder_functions['convert_date_format']
|
|
91
|
-
else:
|
|
92
|
-
# Fallback implementation
|
|
93
|
-
def fallback_convert_date_format(date_str):
|
|
94
|
-
"""Fallback date format conversion function"""
|
|
95
|
-
try:
|
|
96
|
-
# Parse the input date string into a datetime object
|
|
97
|
-
input_format = "%m-%d-%Y" if len(date_str) == 10 else "%m-%d-%y"
|
|
98
|
-
date_obj = datetime.strptime(date_str, input_format)
|
|
99
|
-
# Format the datetime object into the desired output format
|
|
100
|
-
return date_obj.strftime("%Y%m%d")
|
|
101
|
-
except (ValueError, TypeError):
|
|
102
|
-
# Return original string if conversion fails
|
|
103
|
-
return date_str
|
|
104
|
-
return fallback_convert_date_format
|
|
59
|
+
# Import utility functions from the utilities module
|
|
60
|
+
try:
|
|
61
|
+
from .MediLink_837p_utilities import convert_date_format
|
|
62
|
+
except ImportError as e:
|
|
63
|
+
# Fallback implementation for convert_date_format if utilities module is not available
|
|
64
|
+
MediLink_ConfigLoader.log("Warning: Could not import utilities functions: {}".format(e), level="WARNING")
|
|
65
|
+
def convert_date_format(date_str):
|
|
66
|
+
"""Fallback date format conversion function"""
|
|
67
|
+
try:
|
|
68
|
+
# Parse the input date string into a datetime object
|
|
69
|
+
input_format = "%m-%d-%Y" if len(date_str) == 10 else "%m-%d-%y"
|
|
70
|
+
date_obj = datetime.strptime(date_str, input_format)
|
|
71
|
+
# Format the datetime object into the desired output format
|
|
72
|
+
return date_obj.strftime("%Y%m%d")
|
|
73
|
+
except (ValueError, TypeError):
|
|
74
|
+
# Return original string if conversion fails
|
|
75
|
+
return date_str
|
|
105
76
|
|
|
106
77
|
def create_2320_other_subscriber_segments(patient_data, config, crosswalk):
|
|
107
78
|
"""
|
|
@@ -253,9 +224,8 @@ def create_2430_service_line_cob_segments(patient_data, config, crosswalk):
|
|
|
253
224
|
|
|
254
225
|
# DTP*573 segment for adjudication date
|
|
255
226
|
if service.get('adjudication_date'):
|
|
256
|
-
convert_date = get_convert_date_format()
|
|
257
227
|
dtp_segment = "DTP*573*D8*{}~".format(
|
|
258
|
-
|
|
228
|
+
convert_date_format(service.get('adjudication_date'))
|
|
259
229
|
)
|
|
260
230
|
segments.append(dtp_segment)
|
|
261
231
|
|
|
@@ -310,9 +280,8 @@ def create_2330C_other_subscriber_name_segments(patient_data, config, crosswalk)
|
|
|
310
280
|
|
|
311
281
|
# Optional DMG segment for date of birth/gender
|
|
312
282
|
if patient_data.get('subscriber_dob'):
|
|
313
|
-
convert_date = get_convert_date_format()
|
|
314
283
|
dmg_segment = "DMG*D8*{}*{}~".format(
|
|
315
|
-
|
|
284
|
+
convert_date_format(patient_data.get('subscriber_dob')),
|
|
316
285
|
patient_data.get('subscriber_gender', '')
|
|
317
286
|
)
|
|
318
287
|
segments.append(dmg_segment)
|
|
@@ -636,7 +605,8 @@ def get_enhanced_insurance_options(config):
|
|
|
636
605
|
'MA': 'Medicare Advantage',
|
|
637
606
|
'MC': 'Medicare Part C'
|
|
638
607
|
}
|
|
639
|
-
enhanced_options =
|
|
608
|
+
enhanced_options = base_options.copy()
|
|
609
|
+
enhanced_options.update(medicare_options)
|
|
640
610
|
return enhanced_options
|
|
641
611
|
|
|
642
612
|
# Main COB processing function
|
|
@@ -10,27 +10,22 @@ if project_dir not in sys.path:
|
|
|
10
10
|
from MediBot import MediBot_Preprocessor_lib
|
|
11
11
|
load_insurance_data_from_mains = MediBot_Preprocessor_lib.load_insurance_data_from_mains
|
|
12
12
|
from MediBot import MediBot_Crosswalk_Library
|
|
13
|
-
from MediLink_API_v3 import fetch_payer_name_from_api
|
|
14
|
-
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return dt.strftime('%Y%m%d')
|
|
30
|
-
elif format_type == 'isa':
|
|
31
|
-
return dt.strftime('%y%m%d')
|
|
32
|
-
elif format_type == 'time':
|
|
33
|
-
return dt.strftime('%H%M')
|
|
13
|
+
from .MediLink_API_v3 import fetch_payer_name_from_api
|
|
14
|
+
|
|
15
|
+
# Import utility functions from utilities module
|
|
16
|
+
from .MediLink_837p_utilities import (
|
|
17
|
+
convert_date_format,
|
|
18
|
+
format_datetime,
|
|
19
|
+
get_user_confirmation,
|
|
20
|
+
prompt_user_for_payer_id,
|
|
21
|
+
format_claim_number,
|
|
22
|
+
generate_segment_counts,
|
|
23
|
+
handle_validation_errors,
|
|
24
|
+
get_output_directory,
|
|
25
|
+
winscp_validate_output_directory
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
34
29
|
|
|
35
30
|
# Constructs the ST segment for transaction set.
|
|
36
31
|
def create_st_segment(transaction_set_control_number):
|
|
@@ -152,7 +147,11 @@ def create_1000A_submitter_name_segment(patient_data, config, endpoint):
|
|
|
152
147
|
# Submitter contact details
|
|
153
148
|
contact_name = config.get('submitter_name', 'NONE')
|
|
154
149
|
contact_telephone_number = config.get('submitter_tel', 'NONE')
|
|
155
|
-
|
|
150
|
+
|
|
151
|
+
# Get submitter first name to determine entity type qualifier
|
|
152
|
+
submitter_first_name = config.get('submitter_first_name', '')
|
|
153
|
+
# Determine entity_type_qualifier: '1' for individual (with first name), '2' for organization
|
|
154
|
+
entity_type_qualifier = '1' if submitter_first_name else '2' # Make sure that this is correct. Original default was 2.
|
|
156
155
|
|
|
157
156
|
# Construct NM1 segment for the submitter
|
|
158
157
|
nm1_segment = "NM1*41*{}*{}*****{}*{}~".format(entity_type_qualifier, submitter_name, submitter_id_qualifier, submitter_id) # BUG - need to check submitter_name because this is written as fixed ****** which implies a single entry and not a first and last name. This is weird.
|
|
@@ -237,15 +236,7 @@ def create_2010BB_payer_information_segment(parsed_data):
|
|
|
237
236
|
# Build NM1 segment using provided payer name and payer ID
|
|
238
237
|
return build_nm1_segment(payer_name, payer_id)
|
|
239
238
|
|
|
240
|
-
|
|
241
|
-
while True:
|
|
242
|
-
response = input(prompt_message).strip().lower()
|
|
243
|
-
if response in ['yes', 'y']:
|
|
244
|
-
return True
|
|
245
|
-
elif response in ['no', 'n']:
|
|
246
|
-
return False
|
|
247
|
-
else:
|
|
248
|
-
print("Please respond with 'yes' or 'no'.")
|
|
239
|
+
|
|
249
240
|
|
|
250
241
|
def resolve_payer_name(payer_id, config, primary_endpoint, insurance_name, parsed_data, crosswalk, client):
|
|
251
242
|
# Check if the payer_id is in the crosswalk with a name already attached to it.
|
|
@@ -405,18 +396,7 @@ def handle_missing_payer_id(insurance_name, config, crosswalk, client):
|
|
|
405
396
|
MediLink_ConfigLoader.log("User did not confirm the standard insurance name. Manual intervention is required.", config, level="CRITICAL")
|
|
406
397
|
return None
|
|
407
398
|
|
|
408
|
-
|
|
409
|
-
"""
|
|
410
|
-
Prompts the user to input the payer ID manually and ensures that a valid alphanumeric ID is provided.
|
|
411
|
-
"""
|
|
412
|
-
while True:
|
|
413
|
-
print("Manual intervention required: No payer ID found for insurance name '{}'.".format(insurance_name))
|
|
414
|
-
payer_id = input("Please enter the payer ID manually: ").strip()
|
|
415
|
-
|
|
416
|
-
if payer_id.isalnum():
|
|
417
|
-
return payer_id
|
|
418
|
-
else:
|
|
419
|
-
print("Error: Payer ID must be alphanumeric. Please try again.")
|
|
399
|
+
|
|
420
400
|
|
|
421
401
|
def build_nm1_segment(payer_name, payer_id):
|
|
422
402
|
# Step 1: Build NM1 segment using payer name and ID
|
|
@@ -696,15 +676,7 @@ def create_nm1_rendering_provider_segment(config, is_rendering_provider_differen
|
|
|
696
676
|
else:
|
|
697
677
|
return []
|
|
698
678
|
|
|
699
|
-
|
|
700
|
-
# Remove any non-alphanumeric characters from chart number and date
|
|
701
|
-
chart_number_alphanumeric = ''.join(filter(str.isalnum, chart_number))
|
|
702
|
-
date_of_service_alphanumeric = ''.join(filter(str.isalnum, date_of_service))
|
|
703
|
-
|
|
704
|
-
# Combine the alphanumeric components without spaces
|
|
705
|
-
formatted_claim_number = chart_number_alphanumeric + date_of_service_alphanumeric
|
|
706
|
-
|
|
707
|
-
return formatted_claim_number
|
|
679
|
+
|
|
708
680
|
|
|
709
681
|
# Constructs the CLM and related segments based on parsed data and configuration.
|
|
710
682
|
def create_clm_and_related_segments(parsed_data, config, crosswalk):
|
|
@@ -940,70 +912,5 @@ def create_interchange_trailer(config, num_transactions, isa13, num_functional_g
|
|
|
940
912
|
|
|
941
913
|
return ge_segment, iea_segment
|
|
942
914
|
|
|
943
|
-
# Generates segment counts for the formatted 837P transaction and updates SE segment.
|
|
944
|
-
def generate_segment_counts(compiled_segments, transaction_set_control_number):
|
|
945
|
-
# Count the number of segments, not including the placeholder SE segment
|
|
946
|
-
segment_count = compiled_segments.count('~') # + 1 Including SE segment itself, but seems to be giving errors.
|
|
947
|
-
|
|
948
|
-
# Ensure transaction set control number is correctly formatted as a string
|
|
949
|
-
formatted_control_number = str(transaction_set_control_number).zfill(4) # Pad to ensure minimum 4 characters
|
|
950
|
-
|
|
951
|
-
# Construct the SE segment with the actual segment count and the formatted transaction set control_number
|
|
952
|
-
se_segment = "SE*{0}*{1}~".format(segment_count, formatted_control_number)
|
|
953
915
|
|
|
954
|
-
# Assuming the placeholder SE segment was the last segment added before compiling
|
|
955
|
-
# This time, we directly replace the placeholder with the correct SE segment
|
|
956
|
-
formatted_837p = compiled_segments.rsplit('SE**', 1)[0] + se_segment
|
|
957
|
-
|
|
958
|
-
return formatted_837p
|
|
959
916
|
|
|
960
|
-
def handle_validation_errors(transaction_set_control_number, validation_errors, config):
|
|
961
|
-
for error in validation_errors:
|
|
962
|
-
MediLink_ConfigLoader.log("Validation error for transaction set {}: {}".format(transaction_set_control_number, error), config, level="WARNING")
|
|
963
|
-
|
|
964
|
-
print("Validation errors encountered for transaction set {}. Errors: {}".format(transaction_set_control_number, validation_errors))
|
|
965
|
-
user_input = input("Skip this patient and continue without incrementing transaction set number? (yes/no): ")
|
|
966
|
-
if user_input.lower() == 'yes':
|
|
967
|
-
print("Skipping patient...")
|
|
968
|
-
MediLink_ConfigLoader.log("Skipped processing of transaction set {} due to user decision.".format(transaction_set_control_number), config, level="INFO")
|
|
969
|
-
return True # Skip the current patient
|
|
970
|
-
else:
|
|
971
|
-
print("Processing halted due to validation errors.")
|
|
972
|
-
MediLink_ConfigLoader.log("HALT: Processing halted at transaction set {} due to unresolved validation errors.".format(transaction_set_control_number), config, level="ERROR")
|
|
973
|
-
sys.exit() # Optionally halt further processing
|
|
974
|
-
|
|
975
|
-
def winscp_validate_output_directory(output_directory):
|
|
976
|
-
"""
|
|
977
|
-
Validates the output directory path to ensure it has no spaces.
|
|
978
|
-
If spaces are found, prompts the user to input a new path.
|
|
979
|
-
If the directory doesn't exist, creates it.
|
|
980
|
-
"""
|
|
981
|
-
while ' ' in output_directory:
|
|
982
|
-
print("\nWARNING: The output directory path contains spaces, which can cause issues with upload operations.")
|
|
983
|
-
print(" Current proposed path: {}".format(output_directory))
|
|
984
|
-
new_path = input("Please enter a new path for the output directory: ")
|
|
985
|
-
output_directory = new_path.strip() # Remove leading/trailing spaces
|
|
986
|
-
|
|
987
|
-
# Check if the directory exists, if not, create it
|
|
988
|
-
if not os.path.exists(output_directory):
|
|
989
|
-
os.makedirs(output_directory)
|
|
990
|
-
print("INFO: Created output directory: {}".format(output_directory))
|
|
991
|
-
|
|
992
|
-
return output_directory
|
|
993
|
-
|
|
994
|
-
def get_output_directory(config):
|
|
995
|
-
# Retrieve desired default output file path from config
|
|
996
|
-
output_directory = config.get('outputFilePath', '').strip()
|
|
997
|
-
# BUG (Low SFTP) Add WinSCP validation because of the mishandling of spaces in paths. (This shouldn't need to exist.)
|
|
998
|
-
if not output_directory:
|
|
999
|
-
print("Output file path is not specified in the configuration.")
|
|
1000
|
-
output_directory = input("Please enter a valid output directory path: ").strip()
|
|
1001
|
-
|
|
1002
|
-
# Validate the directory path (checks for spaces and existence)
|
|
1003
|
-
output_directory = winscp_validate_output_directory(output_directory)
|
|
1004
|
-
|
|
1005
|
-
if not os.path.isdir(output_directory):
|
|
1006
|
-
print("Output directory does not exist or is not accessible. Please check the configuration.")
|
|
1007
|
-
return None
|
|
1008
|
-
|
|
1009
|
-
return output_directory
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# MediLink_837p_utilities.py
|
|
2
|
+
"""
|
|
3
|
+
837P Encoder Utility Functions
|
|
4
|
+
|
|
5
|
+
This module contains utility functions extracted from MediLink_837p_encoder_library.py
|
|
6
|
+
to reduce the size and complexity of the main encoder library while avoiding circular imports.
|
|
7
|
+
|
|
8
|
+
Functions included:
|
|
9
|
+
- Date/time formatting utilities
|
|
10
|
+
- User interaction utilities
|
|
11
|
+
- File/path handling utilities
|
|
12
|
+
- Processing utilities
|
|
13
|
+
- Validation utilities
|
|
14
|
+
|
|
15
|
+
Import Strategy:
|
|
16
|
+
This module only imports base Python modules and MediLink_ConfigLoader to avoid
|
|
17
|
+
circular dependencies. Other modules import from this utilities module.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
import sys
|
|
22
|
+
import os
|
|
23
|
+
import re
|
|
24
|
+
|
|
25
|
+
# Import MediLink_ConfigLoader for logging functionality
|
|
26
|
+
try:
|
|
27
|
+
from MediLink import MediLink_ConfigLoader
|
|
28
|
+
except ImportError:
|
|
29
|
+
import MediLink_ConfigLoader
|
|
30
|
+
|
|
31
|
+
# =============================================================================
|
|
32
|
+
# DATE/TIME UTILITIES
|
|
33
|
+
# =============================================================================
|
|
34
|
+
|
|
35
|
+
def convert_date_format(date_str):
|
|
36
|
+
"""
|
|
37
|
+
Converts date format from one format to another.
|
|
38
|
+
|
|
39
|
+
Parameters:
|
|
40
|
+
- date_str: Date string in MM-DD-YYYY or MM-DD-YY format
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
- Date string in YYYYMMDD format
|
|
44
|
+
"""
|
|
45
|
+
# Parse the input date string into a datetime object using the input format
|
|
46
|
+
# Determine the input date format based on the length of the input string
|
|
47
|
+
input_format = "%m-%d-%Y" if len(date_str) == 10 else "%m-%d-%y"
|
|
48
|
+
date_obj = datetime.strptime(date_str, input_format)
|
|
49
|
+
# Format the datetime object into the desired output format and return
|
|
50
|
+
return date_obj.strftime("%Y%m%d")
|
|
51
|
+
|
|
52
|
+
def format_datetime(dt=None, format_type='date'):
|
|
53
|
+
"""
|
|
54
|
+
Formats date and time according to the specified format.
|
|
55
|
+
|
|
56
|
+
Parameters:
|
|
57
|
+
- dt: datetime object (defaults to current datetime if None)
|
|
58
|
+
- format_type: 'date', 'isa', or 'time'
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
- Formatted date/time string
|
|
62
|
+
"""
|
|
63
|
+
if dt is None:
|
|
64
|
+
dt = datetime.now()
|
|
65
|
+
if format_type == 'date':
|
|
66
|
+
return dt.strftime('%Y%m%d')
|
|
67
|
+
elif format_type == 'isa':
|
|
68
|
+
return dt.strftime('%y%m%d')
|
|
69
|
+
elif format_type == 'time':
|
|
70
|
+
return dt.strftime('%H%M')
|
|
71
|
+
|
|
72
|
+
# =============================================================================
|
|
73
|
+
# USER INTERACTION UTILITIES
|
|
74
|
+
# =============================================================================
|
|
75
|
+
|
|
76
|
+
def get_user_confirmation(prompt_message):
|
|
77
|
+
"""
|
|
78
|
+
Prompts user for yes/no confirmation with validation.
|
|
79
|
+
|
|
80
|
+
Parameters:
|
|
81
|
+
- prompt_message: Message to display to user
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
- Boolean: True for yes, False for no
|
|
85
|
+
"""
|
|
86
|
+
while True:
|
|
87
|
+
response = input(prompt_message).strip().lower()
|
|
88
|
+
if response in ['yes', 'y']:
|
|
89
|
+
return True
|
|
90
|
+
elif response in ['no', 'n']:
|
|
91
|
+
return False
|
|
92
|
+
else:
|
|
93
|
+
print("Please respond with 'yes' or 'no'.")
|
|
94
|
+
|
|
95
|
+
def prompt_user_for_payer_id(insurance_name):
|
|
96
|
+
"""
|
|
97
|
+
Prompts the user to input the payer ID manually and ensures that a valid alphanumeric ID is provided.
|
|
98
|
+
|
|
99
|
+
Parameters:
|
|
100
|
+
- insurance_name: Name of the insurance for context
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
- Valid alphanumeric payer ID
|
|
104
|
+
"""
|
|
105
|
+
while True:
|
|
106
|
+
print("Manual intervention required: No payer ID found for insurance name '{}'.".format(insurance_name))
|
|
107
|
+
payer_id = input("Please enter the payer ID manually: ").strip()
|
|
108
|
+
|
|
109
|
+
if payer_id.isalnum():
|
|
110
|
+
return payer_id
|
|
111
|
+
else:
|
|
112
|
+
print("Error: Payer ID must be alphanumeric. Please try again.")
|
|
113
|
+
|
|
114
|
+
# =============================================================================
|
|
115
|
+
# FILE/PATH UTILITIES
|
|
116
|
+
# =============================================================================
|
|
117
|
+
|
|
118
|
+
def format_claim_number(chart_number, date_of_service):
|
|
119
|
+
"""
|
|
120
|
+
Formats claim number by combining chart number and date of service.
|
|
121
|
+
|
|
122
|
+
Parameters:
|
|
123
|
+
- chart_number: Patient chart number
|
|
124
|
+
- date_of_service: Date of service
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
- Formatted claim number (alphanumeric only)
|
|
128
|
+
"""
|
|
129
|
+
# Remove any non-alphanumeric characters from chart number and date
|
|
130
|
+
chart_number_alphanumeric = ''.join(filter(str.isalnum, chart_number))
|
|
131
|
+
date_of_service_alphanumeric = ''.join(filter(str.isalnum, date_of_service))
|
|
132
|
+
|
|
133
|
+
# Combine the alphanumeric components without spaces
|
|
134
|
+
formatted_claim_number = chart_number_alphanumeric + date_of_service_alphanumeric
|
|
135
|
+
|
|
136
|
+
return formatted_claim_number
|
|
137
|
+
|
|
138
|
+
def winscp_validate_output_directory(output_directory):
|
|
139
|
+
"""
|
|
140
|
+
Validates the output directory path to ensure it has no spaces.
|
|
141
|
+
If spaces are found, prompts the user to input a new path.
|
|
142
|
+
If the directory doesn't exist, creates it.
|
|
143
|
+
|
|
144
|
+
Parameters:
|
|
145
|
+
- output_directory: Directory path to validate
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
- Validated directory path
|
|
149
|
+
"""
|
|
150
|
+
while ' ' in output_directory:
|
|
151
|
+
print("\nWARNING: The output directory path contains spaces, which can cause issues with upload operations.")
|
|
152
|
+
print(" Current proposed path: {}".format(output_directory))
|
|
153
|
+
new_path = input("Please enter a new path for the output directory: ")
|
|
154
|
+
output_directory = new_path.strip() # Remove leading/trailing spaces
|
|
155
|
+
|
|
156
|
+
# Check if the directory exists, if not, create it
|
|
157
|
+
if not os.path.exists(output_directory):
|
|
158
|
+
os.makedirs(output_directory)
|
|
159
|
+
print("INFO: Created output directory: {}".format(output_directory))
|
|
160
|
+
|
|
161
|
+
return output_directory
|
|
162
|
+
|
|
163
|
+
def get_output_directory(config):
|
|
164
|
+
"""
|
|
165
|
+
Retrieves and validates output directory from configuration.
|
|
166
|
+
|
|
167
|
+
Parameters:
|
|
168
|
+
- config: Configuration dictionary
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
- Valid output directory path or None if invalid
|
|
172
|
+
"""
|
|
173
|
+
# Retrieve desired default output file path from config
|
|
174
|
+
output_directory = config.get('outputFilePath', '').strip()
|
|
175
|
+
# BUG (Low SFTP) Add WinSCP validation because of the mishandling of spaces in paths. (This shouldn't need to exist.)
|
|
176
|
+
if not output_directory:
|
|
177
|
+
print("Output file path is not specified in the configuration.")
|
|
178
|
+
output_directory = input("Please enter a valid output directory path: ").strip()
|
|
179
|
+
|
|
180
|
+
# Validate the directory path (checks for spaces and existence)
|
|
181
|
+
output_directory = winscp_validate_output_directory(output_directory)
|
|
182
|
+
|
|
183
|
+
if not os.path.isdir(output_directory):
|
|
184
|
+
print("Output directory does not exist or is not accessible. Please check the configuration.")
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
return output_directory
|
|
188
|
+
|
|
189
|
+
# =============================================================================
|
|
190
|
+
# PROCESSING UTILITIES
|
|
191
|
+
# =============================================================================
|
|
192
|
+
|
|
193
|
+
def generate_segment_counts(compiled_segments, transaction_set_control_number):
|
|
194
|
+
"""
|
|
195
|
+
Generates segment counts for the formatted 837P transaction and updates SE segment.
|
|
196
|
+
|
|
197
|
+
Parameters:
|
|
198
|
+
- compiled_segments: String containing compiled 837P segments
|
|
199
|
+
- transaction_set_control_number: Transaction set control number
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
- Formatted 837P string with correct SE segment
|
|
203
|
+
"""
|
|
204
|
+
# Count the number of segments, not including the placeholder SE segment
|
|
205
|
+
segment_count = compiled_segments.count('~') # + 1 Including SE segment itself, but seems to be giving errors.
|
|
206
|
+
|
|
207
|
+
# Ensure transaction set control number is correctly formatted as a string
|
|
208
|
+
formatted_control_number = str(transaction_set_control_number).zfill(4) # Pad to ensure minimum 4 characters
|
|
209
|
+
|
|
210
|
+
# Construct the SE segment with the actual segment count and the formatted transaction set control_number
|
|
211
|
+
se_segment = "SE*{0}*{1}~".format(segment_count, formatted_control_number)
|
|
212
|
+
|
|
213
|
+
# Assuming the placeholder SE segment was the last segment added before compiling
|
|
214
|
+
# This time, we directly replace the placeholder with the correct SE segment
|
|
215
|
+
formatted_837p = compiled_segments.rsplit('SE**', 1)[0] + se_segment
|
|
216
|
+
|
|
217
|
+
return formatted_837p
|
|
218
|
+
|
|
219
|
+
# =============================================================================
|
|
220
|
+
# VALIDATION UTILITIES
|
|
221
|
+
# =============================================================================
|
|
222
|
+
|
|
223
|
+
def handle_validation_errors(transaction_set_control_number, validation_errors, config):
|
|
224
|
+
"""
|
|
225
|
+
Handles validation errors with user interaction for decision making.
|
|
226
|
+
|
|
227
|
+
Parameters:
|
|
228
|
+
- transaction_set_control_number: Current transaction set control number
|
|
229
|
+
- validation_errors: List of validation errors
|
|
230
|
+
- config: Configuration for logging
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
- Boolean: True to skip patient, False to halt processing
|
|
234
|
+
"""
|
|
235
|
+
for error in validation_errors:
|
|
236
|
+
MediLink_ConfigLoader.log("Validation error for transaction set {}: {}".format(transaction_set_control_number, error), config, level="WARNING")
|
|
237
|
+
|
|
238
|
+
print("Validation errors encountered for transaction set {}. Errors: {}".format(transaction_set_control_number, validation_errors))
|
|
239
|
+
user_input = input("Skip this patient and continue without incrementing transaction set number? (yes/no): ")
|
|
240
|
+
if user_input.lower() == 'yes':
|
|
241
|
+
print("Skipping patient...")
|
|
242
|
+
MediLink_ConfigLoader.log("Skipped processing of transaction set {} due to user decision.".format(transaction_set_control_number), config, level="INFO")
|
|
243
|
+
return True # Skip the current patient
|
|
244
|
+
else:
|
|
245
|
+
print("Processing halted due to validation errors.")
|
|
246
|
+
MediLink_ConfigLoader.log("HALT: Processing halted at transaction set {} due to unresolved validation errors.".format(transaction_set_control_number), config, level="ERROR")
|
|
247
|
+
sys.exit() # Optionally halt further processing
|
|
248
|
+
|
|
249
|
+
# =============================================================================
|
|
250
|
+
# UTILITY FUNCTION REGISTRY
|
|
251
|
+
# =============================================================================
|
|
252
|
+
|
|
253
|
+
# Export all utility functions for easy importing
|
|
254
|
+
__all__ = [
|
|
255
|
+
'convert_date_format',
|
|
256
|
+
'format_datetime',
|
|
257
|
+
'get_user_confirmation',
|
|
258
|
+
'prompt_user_for_payer_id',
|
|
259
|
+
'format_claim_number',
|
|
260
|
+
'winscp_validate_output_directory',
|
|
261
|
+
'get_output_directory',
|
|
262
|
+
'generate_segment_counts',
|
|
263
|
+
'handle_validation_errors'
|
|
264
|
+
]
|
MediLink/MediLink_Deductible.py
CHANGED
|
@@ -229,14 +229,32 @@ def get_eligibility_info(client, payer_id, provider_last_name, date_of_birth, me
|
|
|
229
229
|
)
|
|
230
230
|
print("\nValidation report generated: {}".format(validation_file_path))
|
|
231
231
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
232
|
+
# Log any Super Connector API errors
|
|
233
|
+
if super_connector_eligibility and "rawGraphQLResponse" in super_connector_eligibility:
|
|
234
|
+
raw_response = super_connector_eligibility.get('rawGraphQLResponse', {})
|
|
235
|
+
errors = raw_response.get('errors', [])
|
|
236
|
+
if errors:
|
|
237
|
+
print("Super Connector API returned {} error(s):".format(len(errors)))
|
|
238
|
+
for i, error in enumerate(errors):
|
|
239
|
+
error_code = error.get('code', 'UNKNOWN')
|
|
240
|
+
error_desc = error.get('description', 'No description')
|
|
241
|
+
print(" Error {}: {} - {}".format(i+1, error_code, error_desc))
|
|
242
|
+
|
|
243
|
+
# Check for data in error extensions (some APIs return data here)
|
|
244
|
+
extensions = error.get('extensions', {})
|
|
245
|
+
if extensions and 'details' in extensions:
|
|
246
|
+
details = extensions.get('details', [])
|
|
247
|
+
if details:
|
|
248
|
+
print(" Found {} detail records in error extensions".format(len(details)))
|
|
249
|
+
# Log first detail record for debugging
|
|
250
|
+
if details:
|
|
251
|
+
first_detail = details[0]
|
|
252
|
+
print(" First detail: {}".format(first_detail))
|
|
253
|
+
|
|
254
|
+
# Check status code
|
|
255
|
+
status_code = super_connector_eligibility.get('statuscode')
|
|
256
|
+
if status_code and status_code != '200':
|
|
257
|
+
print("Super Connector API status code: {} (non-200 indicates errors)".format(status_code))
|
|
240
258
|
|
|
241
259
|
# Open validation report in Notepad
|
|
242
260
|
os.system('notepad.exe "{}"'.format(validation_file_path))
|
|
@@ -291,6 +309,22 @@ def extract_super_connector_patient_info(eligibility_data):
|
|
|
291
309
|
'firstName': member_info.get("firstName", ""),
|
|
292
310
|
'middleName': member_info.get("middleName", "")
|
|
293
311
|
}
|
|
312
|
+
|
|
313
|
+
# Check for data in error extensions (some APIs return data here despite errors)
|
|
314
|
+
errors = raw_response.get('errors', [])
|
|
315
|
+
for error in errors:
|
|
316
|
+
extensions = error.get('extensions', {})
|
|
317
|
+
if extensions and 'details' in extensions:
|
|
318
|
+
details = extensions.get('details', [])
|
|
319
|
+
if details:
|
|
320
|
+
# Use the first detail record that has patient info
|
|
321
|
+
for detail in details:
|
|
322
|
+
if detail.get('lastName') or detail.get('firstName'):
|
|
323
|
+
return {
|
|
324
|
+
'lastName': detail.get("lastName", ""),
|
|
325
|
+
'firstName': detail.get("firstName", ""),
|
|
326
|
+
'middleName': detail.get("middleName", "")
|
|
327
|
+
}
|
|
294
328
|
|
|
295
329
|
# Fallback to top-level fields
|
|
296
330
|
return {
|
|
@@ -446,6 +480,28 @@ def extract_super_connector_insurance_info(eligibility_data):
|
|
|
446
480
|
'memberId': insurance_info.get("memberId", ""),
|
|
447
481
|
'payerId': insurance_info.get("payerId", "")
|
|
448
482
|
}
|
|
483
|
+
|
|
484
|
+
# Check for data in error extensions (some APIs return data here despite errors)
|
|
485
|
+
errors = raw_response.get('errors', [])
|
|
486
|
+
for error in errors:
|
|
487
|
+
extensions = error.get('extensions', {})
|
|
488
|
+
if extensions and 'details' in extensions:
|
|
489
|
+
details = extensions.get('details', [])
|
|
490
|
+
if details:
|
|
491
|
+
# Use the first detail record that has insurance info
|
|
492
|
+
for detail in details:
|
|
493
|
+
if detail.get('memberId') or detail.get('payerId'):
|
|
494
|
+
# Try to determine insurance type from available data
|
|
495
|
+
insurance_type = detail.get('planType', '')
|
|
496
|
+
if not insurance_type:
|
|
497
|
+
insurance_type = detail.get('productType', '')
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
'insuranceType': insurance_type,
|
|
501
|
+
'insuranceTypeCode': detail.get("productServiceCode", ""),
|
|
502
|
+
'memberId': detail.get("memberId", ""),
|
|
503
|
+
'payerId': detail.get("payerId", "")
|
|
504
|
+
}
|
|
449
505
|
|
|
450
506
|
# Fallback to top-level fields
|
|
451
507
|
insurance_type = eligibility_data.get("planTypeDescription", "")
|
|
@@ -266,13 +266,57 @@ def check_data_quality_issues(super_connector_data):
|
|
|
266
266
|
errors = raw_response.get('errors', [])
|
|
267
267
|
if errors:
|
|
268
268
|
for error in errors:
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
269
|
+
error_code = error.get('code', 'UNKNOWN')
|
|
270
|
+
error_desc = error.get('description', 'No description')
|
|
271
|
+
|
|
272
|
+
# Check if this is an informational error with data
|
|
273
|
+
if error_code == 'INFORMATIONAL':
|
|
274
|
+
extensions = error.get('extensions', {})
|
|
275
|
+
if extensions and 'details' in extensions:
|
|
276
|
+
details = extensions.get('details', [])
|
|
277
|
+
if details:
|
|
278
|
+
issues.append({
|
|
279
|
+
"type": "Informational Error with Data",
|
|
280
|
+
"field": "rawGraphQLResponse.errors",
|
|
281
|
+
"value": error_code,
|
|
282
|
+
"issue": "API returned informational error but provided data in extensions: {}".format(error_desc),
|
|
283
|
+
"recommendation": "Data available in error extensions - system will attempt to extract"
|
|
284
|
+
})
|
|
285
|
+
else:
|
|
286
|
+
issues.append({
|
|
287
|
+
"type": "API Error",
|
|
288
|
+
"field": "rawGraphQLResponse.errors",
|
|
289
|
+
"value": error_code,
|
|
290
|
+
"issue": "Super Connector API returned error: {}".format(error_desc),
|
|
291
|
+
"recommendation": "Review API implementation and error handling"
|
|
292
|
+
})
|
|
293
|
+
else:
|
|
294
|
+
issues.append({
|
|
295
|
+
"type": "API Error",
|
|
296
|
+
"field": "rawGraphQLResponse.errors",
|
|
297
|
+
"value": error_code,
|
|
298
|
+
"issue": "Super Connector API returned error: {}".format(error_desc),
|
|
299
|
+
"recommendation": "Review API implementation and error handling"
|
|
300
|
+
})
|
|
301
|
+
else:
|
|
302
|
+
issues.append({
|
|
303
|
+
"type": "API Error",
|
|
304
|
+
"field": "rawGraphQLResponse.errors",
|
|
305
|
+
"value": error_code,
|
|
306
|
+
"issue": "Super Connector API returned error: {}".format(error_desc),
|
|
307
|
+
"recommendation": "Review API implementation and error handling"
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
# Check status code
|
|
311
|
+
status_code = super_connector_data.get('statuscode')
|
|
312
|
+
if status_code and status_code != '200':
|
|
313
|
+
issues.append({
|
|
314
|
+
"type": "Non-200 Status Code",
|
|
315
|
+
"field": "statuscode",
|
|
316
|
+
"value": status_code,
|
|
317
|
+
"issue": "API returned status code {} instead of 200".format(status_code),
|
|
318
|
+
"recommendation": "Check API health and error handling"
|
|
319
|
+
})
|
|
276
320
|
|
|
277
321
|
# Check for multiple eligibility records (this is actually good, but worth noting)
|
|
278
322
|
if "rawGraphQLResponse" in super_connector_data:
|
|
@@ -15,9 +15,10 @@ MediBot/update_json.py,sha256=9FJZb-32EujpKuSoCjyCbdTdthOIuhcMoN4Wchuzn8A,2508
|
|
|
15
15
|
MediBot/update_medicafe.py,sha256=rx1zUvCI99JRdr8c1csMGI2uJBl3pqusvX-xr3KhmR4,11881
|
|
16
16
|
MediLink/MediLink.py,sha256=O3VSLm2s5viCRBL1is7Loj_nSaLMMcFZ-weXAmVp_20,21588
|
|
17
17
|
MediLink/MediLink_277_decoder.py,sha256=Z3hQK2j-YzdXjov6aDlDRc7M_auFBnl3se4OF5q6_04,4358
|
|
18
|
-
MediLink/MediLink_837p_cob_library.py,sha256
|
|
18
|
+
MediLink/MediLink_837p_cob_library.py,sha256=-Rn40XFUAi_0CxcqSALlXiQmgWH2FE0THkNmxkAJAO0,29755
|
|
19
19
|
MediLink/MediLink_837p_encoder.py,sha256=OiYU2cyr9rFBGv7XOwYuZjCKWbUNb9vN2TcX6vvUZWM,27242
|
|
20
|
-
MediLink/MediLink_837p_encoder_library.py,sha256=
|
|
20
|
+
MediLink/MediLink_837p_encoder_library.py,sha256=0NwTIiRw76oleRn-S1Dn-Rv8IBUqjz7dn1W_MT9LA_o,47076
|
|
21
|
+
MediLink/MediLink_837p_utilities.py,sha256=Bi91S1aJbsEOpWXp_IOUgCQ76IPiOJNkOfXXtcirzmI,10416
|
|
21
22
|
MediLink/MediLink_API_Generator.py,sha256=vBZ8moR9tvv7mb200HlZnJrk1y-bQi8E16I2r41vgVM,10345
|
|
22
23
|
MediLink/MediLink_API_v2.py,sha256=mcIgLnXPS_NaUBrkKJ8mxCUaQ0AuQUeU1vG6DoplbVY,7733
|
|
23
24
|
MediLink/MediLink_API_v3.py,sha256=D17yXicLRvHfEsx5c-VUNZlke5oSnclQu6cKJACzeHA,40745
|
|
@@ -27,8 +28,8 @@ MediLink/MediLink_ClaimStatus.py,sha256=DkUL5AhmuaHsdKiQG1btciJIuexl0OLXBEH40j1K
|
|
|
27
28
|
MediLink/MediLink_ConfigLoader.py,sha256=u9ecB0SIN7zuJAo8KcoQys95BtyAo-8S2n4mRd0S3XU,4356
|
|
28
29
|
MediLink/MediLink_DataMgmt.py,sha256=jrTAPSNVzs1wwYl1g0_8Mda3k2B27CbaSw8Pu2qmThw,33058
|
|
29
30
|
MediLink/MediLink_Decoder.py,sha256=Suw9CmUHgoe0ZW8sJP_pIO8URBrhO5FmxFF8RcUj9lI,13318
|
|
30
|
-
MediLink/MediLink_Deductible.py,sha256=
|
|
31
|
-
MediLink/MediLink_Deductible_Validator.py,sha256=
|
|
31
|
+
MediLink/MediLink_Deductible.py,sha256=nD9dwStQY34FYmnuqg361UgFX8vLpZk88Im0LZJ45IQ,36732
|
|
32
|
+
MediLink/MediLink_Deductible_Validator.py,sha256=2g-lZd-Y5fJ1mfP87vM6oABg0t5Om-7EkEkilVvDWYY,22888
|
|
32
33
|
MediLink/MediLink_Down.py,sha256=hrDODhs-zRfOKCdiRGENN5Czu-AvdtwJj4Q7grcRXME,6518
|
|
33
34
|
MediLink/MediLink_ERA_decoder.py,sha256=MiOtDcXnmevPfHAahIlTLlUc14VcQWAor9Xa7clA2Ts,8710
|
|
34
35
|
MediLink/MediLink_Gmail.py,sha256=OYsASNgP4YSTaSnj9XZxPPiy0cw41JC-suLIgRyNrlQ,31439
|
|
@@ -48,8 +49,8 @@ MediLink/test.py,sha256=kSvvJRL_3fWuNS3_x4hToOnUljGLoeEw6SUTHQWQRJk,3108
|
|
|
48
49
|
MediLink/test_cob_library.py,sha256=wUMv0-Y6fNsKcAs8Z9LwfmEBRO7oBzBAfWmmzwoNd1g,13841
|
|
49
50
|
MediLink/test_validation.py,sha256=FJrfdUFK--xRScIzrHCg1JeGdm0uJEoRnq6CgkP2lwM,4154
|
|
50
51
|
MediLink/webapp.html,sha256=JPKT559aFVBi1r42Hz7C77Jj0teZZRumPhBev8eSOLk,19806
|
|
51
|
-
medicafe-0.250720.
|
|
52
|
-
medicafe-0.250720.
|
|
53
|
-
medicafe-0.250720.
|
|
54
|
-
medicafe-0.250720.
|
|
55
|
-
medicafe-0.250720.
|
|
52
|
+
medicafe-0.250720.1.dist-info/LICENSE,sha256=65lb-vVujdQK7uMH3RRJSMwUW-WMrMEsc5sOaUn2xUk,1096
|
|
53
|
+
medicafe-0.250720.1.dist-info/METADATA,sha256=O51wyX4FSnXp7JwidtHgLoexu1Loazewxl2FPfyRczY,5501
|
|
54
|
+
medicafe-0.250720.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
|
55
|
+
medicafe-0.250720.1.dist-info/top_level.txt,sha256=3uOwR4q_SP8Gufk2uCHoKngAgbtdOwQC6Qjl7ViBa_c,17
|
|
56
|
+
medicafe-0.250720.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|