medicafe 0.250813.2__py3-none-any.whl → 0.250814.3__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.
MediLink/MediLink_APIs.py DELETED
@@ -1,138 +0,0 @@
1
- # Unused archive backup. This has been superceded by API_v2
2
-
3
- import time, requests
4
- try:
5
- from MediLink import MediLink_ConfigLoader
6
- except ImportError:
7
- import MediLink_ConfigLoader
8
-
9
- # Fetches the payer name from API based on the payer ID.
10
- def fetch_payer_name_from_api(payer_id, config, primary_endpoint='AVAILITY'):
11
- """
12
- Fetches the payer name from the API using the provided payer ID.
13
-
14
- Args:
15
- payer_id (str): The ID of the payer.
16
- config (dict): Configuration settings.
17
- primary_endpoint (str): The primary endpoint for resolving payer information.
18
-
19
- Raises:
20
- ValueError: If all endpoints are exhausted without finding the payer.
21
-
22
- Returns:
23
- str: The fetched payer name.
24
- """
25
- # Reload for safety
26
- config, _ = MediLink_ConfigLoader.load_configuration()
27
-
28
- # Step 1: Retrieve endpoint configurations
29
- endpoints = config['MediLink_Config']['endpoints']
30
- tried_endpoints = []
31
-
32
- # Step 2: Check if the primary endpoint is specified and is valid
33
- if primary_endpoint and primary_endpoint in endpoints:
34
- endpoint_order = [primary_endpoint] + [endpoint for endpoint in endpoints if endpoint != primary_endpoint]
35
- else:
36
- endpoint_order = list(endpoints.keys())
37
-
38
- # Step 3: Iterate through available endpoints in specified order
39
- for endpoint_name in endpoint_order:
40
- endpoint_config = endpoints[endpoint_name]
41
- if not all(key in endpoint_config for key in ['token_url', 'client_id', 'client_secret']):
42
- MediLink_ConfigLoader.log("Skipping {} due to missing API keys.".format(endpoint_name), config, level="WARNING")
43
- continue
44
-
45
- # Step 4: Get access token for the endpoint
46
- token = get_access_token(endpoint_config)
47
- api_url = endpoint_config.get("api_url", "")
48
- headers = {'Authorization': 'Bearer {}'.format(token), 'Accept': 'application/json'}
49
- params = {'payerId': payer_id}
50
-
51
- try:
52
- # Step 5: Make API call to fetch payer name
53
- response = requests.get(api_url, headers=headers, params=params)
54
- response.raise_for_status()
55
- data = response.json()
56
- if 'payers' in data and data['payers']:
57
- payer = data['payers'][0]
58
- payer_name = payer.get('displayName') or payer.get('name')
59
-
60
- # Log successful match
61
- MediLink_ConfigLoader.log("Successfully found payer at {} for ID {}: {}".format(endpoint_name, payer_id, payer_name), config, level="INFO")
62
-
63
- return payer_name
64
- else:
65
- MediLink_ConfigLoader.log("No payer found at {} for ID: {}. Trying next available endpoint.".format(endpoint_name, payer_id), config, level="INFO")
66
- except requests.RequestException as e:
67
- # Step 6: Log API call failure
68
- MediLink_ConfigLoader.log("API call failed at {} for Payer ID '{}': {}".format(endpoint_name, payer_id, str(e)), config, level="ERROR")
69
- tried_endpoints.append(endpoint_name)
70
-
71
- # Step 7: Log all endpoints exhaustion and raise error
72
- error_message = "All endpoints exhausted for Payer ID {}. Endpoints tried: {}".format(payer_id, ', '.join(tried_endpoints))
73
- MediLink_ConfigLoader.log(error_message, config, level="CRITICAL")
74
- raise ValueError(error_message)
75
-
76
- # Test Case for API fetch
77
- #payer_id = "11347"
78
- #config = load_configuration()
79
- #payer_name = fetch_payer_name_from_api(payer_id, config, endpoint='AVAILITY')
80
- #print(payer_id, payer_name)
81
-
82
- # Initialize a global dictionary to store the access token and its expiry time
83
- # TODO (Low API) This will need to get setup for each endpoint separately.
84
- token_cache = {
85
- 'access_token': None,
86
- 'expires_at': 0 # Timestamp of when the token expires
87
- }
88
-
89
- def get_access_token(endpoint_config):
90
- current_time = time.time()
91
-
92
- # Check if the cached token is still valid
93
- if token_cache['access_token'] and token_cache['expires_at'] > current_time:
94
- return token_cache['access_token']
95
-
96
- # Validate endpoint configuration
97
- if not endpoint_config or not all(k in endpoint_config for k in ['client_id', 'client_secret', 'token_url']):
98
- raise ValueError("Endpoint configuration is incomplete or missing necessary fields.")
99
-
100
- # Extract credentials and URL from the config
101
- CLIENT_ID = endpoint_config.get("client_id")
102
- CLIENT_SECRET = endpoint_config.get("client_secret")
103
- url = endpoint_config.get("token_url")
104
-
105
- # Setup the data payload and headers for the HTTP request
106
- data = {
107
- 'grant_type': 'client_credentials',
108
- 'client_id': CLIENT_ID,
109
- 'client_secret': CLIENT_SECRET,
110
- 'scope': 'hipaa'
111
- }
112
- headers = {
113
- 'Content-Type': 'application/x-www-form-urlencoded'
114
- }
115
-
116
- try:
117
- # Perform the HTTP request to get the access token
118
- response = requests.post(url, headers=headers, data=data)
119
- response.raise_for_status() # This will raise an exception for HTTP error statuses
120
- json_response = response.json()
121
- access_token = json_response.get('access_token')
122
- expires_in = json_response.get('expires_in', 3600) # Default to 3600 seconds if not provided
123
-
124
- if not access_token:
125
- raise ValueError("No access token returned by the server.")
126
-
127
- # Store the access token and calculate the expiry time
128
- token_cache['access_token'] = access_token
129
- token_cache['expires_at'] = current_time + expires_in - 120 # Subtracting 120 seconds to provide buffer
130
-
131
- return access_token
132
- except requests.RequestException as e:
133
- # Handle HTTP errors (e.g., network problems, invalid response)
134
- error_msg = "Failed to retrieve access token: {0}. Response status: {1}".format(str(e), response.status_code if response else 'No response')
135
- raise Exception(error_msg)
136
- except ValueError as e:
137
- # Handle specific errors like missing access token
138
- raise Exception("Configuration or server response error: {0}".format(str(e)))
@@ -1,87 +0,0 @@
1
- # MediLink_ConfigLoader.py
2
- import os, json, logging, sys, platform, yaml
3
- from datetime import datetime
4
- from collections import OrderedDict
5
-
6
- """
7
- This function should be generalizable to have a initialization script over all the Medi* functions
8
- """
9
- def load_configuration(config_path=os.path.join(os.path.dirname(__file__), '..', 'json', 'config.json'), crosswalk_path=os.path.join(os.path.dirname(__file__), '..', 'json', 'crosswalk.json')):
10
- """
11
- Loads endpoint configuration, credentials, and other settings from JSON or YAML files.
12
-
13
- Returns: A tuple containing dictionaries with configuration settings for the main config and crosswalk.
14
- """
15
- # TODO (Low Config Upgrade) The Medicare / Private differentiator flag probably needs to be pulled or passed to this.
16
- # BUG Hardcode sucks. This should probably be some local env variable.
17
- # Detect the operating system
18
- if platform.system() == 'Windows' and platform.release() == 'XP':
19
- # Use F: paths for Windows XP
20
- config_path = "F:\\Medibot\\json\\config.json"
21
- crosswalk_path = "F:\\Medibot\\json\\crosswalk.json"
22
- else:
23
- # Use G: paths for other versions of Windows
24
- config_path = "G:\\My Drive\\Codes\\MediCafe\\json\\config.json"
25
- crosswalk_path = "G:\\My Drive\\Codes\\MediCafe\\json\\crosswalk.json"
26
-
27
- try:
28
- with open(config_path, 'r') as config_file:
29
- if config_path.endswith('.yaml') or config_path.endswith('.yml'):
30
- config = yaml.safe_load(config_file)
31
- elif config_path.endswith('.json'):
32
- config = json.load(config_file, object_pairs_hook=OrderedDict)
33
- else:
34
- raise ValueError("Unsupported configuration format.")
35
-
36
- if 'MediLink_Config' not in config:
37
- raise KeyError("MediLink_Config key is missing from the loaded configuration.")
38
-
39
- with open(crosswalk_path, 'r') as crosswalk_file:
40
- crosswalk = json.load(crosswalk_file)
41
-
42
- return config, crosswalk
43
- except ValueError as e:
44
- if isinstance(e, UnicodeDecodeError):
45
- print("Error decoding file: {}".format(e))
46
- else:
47
- print("Error parsing file: {}".format(e))
48
- sys.exit(1) # Exit the script due to a critical error in configuration loading
49
- except FileNotFoundError:
50
- print("One or both configuration files not found. Config: {}, Crosswalk: {}".format(config_path, crosswalk_path))
51
- sys.exit(1) # Exit the script due to a critical error in configuration loading
52
- except KeyError as e:
53
- print("Critical configuration is missing: {}".format(e))
54
- sys.exit(1) # Exit the script due to a critical error in configuration loading
55
- except Exception as e:
56
- print("An unexpected error occurred while loading the configuration: {}".format(e))
57
- sys.exit(1) # Exit the script due to a critical error in configuration loading
58
-
59
- # Logs messages with optional error type and claim data.
60
- def log(message, config=None, level="INFO", error_type=None, claim=None, verbose=False):
61
-
62
- # If config is not provided, load it
63
- if config is None:
64
- config, _ = load_configuration()
65
-
66
- # Setup logger if not already configured
67
- if not logging.root.handlers:
68
- local_storage_path = config['MediLink_Config'].get('local_storage_path', '.') if isinstance(config, dict) else '.'
69
- log_filename = datetime.now().strftime("Log_%m%d%Y.log")
70
- log_filepath = os.path.join(local_storage_path, log_filename)
71
-
72
- # Set logging level based on verbosity
73
- logging_level = logging.DEBUG if verbose else logging.INFO
74
-
75
- logging.basicConfig(level=logging_level,
76
- format='%(asctime)s - %(levelname)s - %(message)s',
77
- filename=log_filepath,
78
- filemode='a')
79
-
80
- # Prepare log message
81
- claim_data = " - Claim Data: {}".format(claim) if claim else ""
82
- error_info = " - Error Type: {}".format(error_type) if error_type else ""
83
- full_message = "{} {}{}".format(message, claim_data, error_info)
84
-
85
- # Log the message
86
- logger = logging.getLogger()
87
- getattr(logger, level.lower())(full_message)
@@ -1,192 +0,0 @@
1
- import os
2
- import sys
3
- import csv
4
- from MediLink_ConfigLoader import load_configuration, log
5
- from MediLink_DataMgmt import consolidate_csvs
6
-
7
-
8
- """
9
- 1. ERA File Processing: Implement robust mechanisms for reading and parsing ERA files, addressing potential file integrity issues and accommodating scenarios with multiple payer addresses within a single ERA.
10
- 2. Wildcard File Processing: Enable effective batch processing of ERA files using wildcard patterns in the `--era_file_path` argument, resulting in a unified CSV output.
11
- 3. Date of Service Parsing: Enhance the parsing logic for 'Date of Service' to accommodate different segment identifiers, improving data extraction reliability.
12
- 4. Payer Address Extraction: Fine-tune the logic for extracting payer and provider addresses from ERA files, ensuring only relevant information is captured.
13
- 5. De-persisting Intermediate Files.
14
- """
15
-
16
- # ERA Parser
17
- def parse_era_content(content):
18
- extracted_data = []
19
- normalized_content = content.replace('~\n', '~')
20
- lines = normalized_content.split('~')
21
-
22
- # Reset these values for each new CLP segment
23
- record = {}
24
- check_eft, payer_address = None, None
25
- allowed_amount, write_off, patient_responsibility, adjustment_amount = 0, 0, 0, 0
26
- is_payer_section = False # Flag to identify payer section for accurate address capture
27
-
28
- for line in lines:
29
- segments = line.split('*')
30
-
31
- if segments[0] == 'TRN' and len(segments) > 2:
32
- check_eft = segments[2]
33
-
34
- # Determine the start and end of the payer section to correctly capture the payer's address
35
- if segments[0] == 'N1':
36
- if segments[1] == 'PR': # Payer information starts
37
- is_payer_section = True
38
- # payer_name = segments[2] # Can capture payer name here if needed
39
- elif segments[1] == 'PE': # Provider information starts, ending payer section
40
- is_payer_section = False
41
-
42
- # Correctly capture payer address only within payer section
43
- if is_payer_section and segments[0] == 'N3' and len(segments) > 1:
44
- payer_address = segments[1]
45
-
46
- if segments[0] == 'CLP' and len(segments) >= 5:
47
- if record:
48
- if adjustment_amount == 0 and (write_off > 0 or patient_responsibility > 0):
49
- adjustment_amount = write_off + patient_responsibility
50
-
51
- # Finalize and append the current record before starting a new one
52
- record.update({
53
- # 'Payer Name': payer_name,
54
- 'Payer Address': payer_address,
55
- 'Allowed Amount': allowed_amount,
56
- 'Write Off': write_off,
57
- 'Patient Responsibility': patient_responsibility,
58
- 'Adjustment Amount': adjustment_amount,
59
- })
60
- extracted_data.append(record)
61
-
62
- # Reset variables for the next record
63
- allowed_amount, write_off, patient_responsibility, adjustment_amount = 0, 0, 0, 0
64
- # payer_address = None # Reset address for the next CLP segment if it changes within one ERA file (so no. disable.)
65
-
66
- # Initialize a new record
67
- record = {
68
- 'Check EFT': check_eft,
69
- 'Chart Number': segments[1],
70
- 'Payer Address': payer_address,
71
- 'Amount Paid': segments[4],
72
- 'Charge': segments[3], # Total submitted charges for the claim
73
- }
74
-
75
- elif segments[0] == 'CAS':
76
- # Parsing CAS segments for Write Off and Patient Responsibility
77
- if segments[1] == 'CO': # Write Off
78
- write_off += float(segments[3])
79
- elif segments[1] == 'PR': # Patient Responsibility
80
- patient_responsibility += float(segments[3])
81
- elif segments[1] == 'OA': # Capture Adjustment Amount from CAS*OA segment
82
- adjustment_amount += float(segments[3])
83
-
84
- elif segments[0] == 'AMT' and segments[1] == 'B6':
85
- # Allowed Amount from AMT segment
86
- allowed_amount += float(segments[2])
87
-
88
- elif segments[0] == 'DTM' and (segments[1] == '232' or segments[1] == '472'):
89
- record['Date of Service'] = segments[2]
90
-
91
-
92
- if record:
93
- # Final record handling
94
- if adjustment_amount == 0 and (write_off > 0 or patient_responsibility > 0):
95
- adjustment_amount = write_off + patient_responsibility
96
- # Append the last record
97
- record.update({
98
- 'Allowed Amount': allowed_amount,
99
- 'Write Off': write_off,
100
- 'Patient Responsibility': patient_responsibility,
101
- 'Adjustment Amount': adjustment_amount,
102
- })
103
- extracted_data.append(record)
104
-
105
- return extracted_data
106
-
107
- def translate_era_to_csv(files, output_directory):
108
- if not os.path.exists(output_directory):
109
- os.makedirs(output_directory)
110
-
111
- for file_path in files:
112
- # Ensure the file is read correctly
113
- with open(file_path, 'r') as era_file:
114
- era_content = era_file.read()
115
-
116
- data = parse_era_content(era_content)
117
- # print("Parsed Data: ", data) # DEBUG
118
-
119
- csv_file_path = os.path.join(output_directory, os.path.basename(file_path) + '.csv')
120
-
121
- try:
122
- # Open the CSV file with explicit newline handling
123
- with open(csv_file_path, 'w', newline='') as csv_file:
124
- fieldnames = ['Date of Service',
125
- 'Check EFT',
126
- 'Chart Number',
127
- 'Payer Address',
128
- 'Amount Paid',
129
- 'Adjustment Amount',
130
- 'Allowed Amount',
131
- 'Write Off',
132
- 'Patient Responsibility',
133
- 'Charge'
134
- ]
135
- writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
136
-
137
- writer.writeheader()
138
- for record in data:
139
-
140
- # print("Writing record: ", record)
141
-
142
- writer.writerow({
143
- 'Date of Service': record.get('Date of Service', ''),
144
- 'Check EFT': record.get('Check EFT', ''),
145
- 'Chart Number': record.get('Chart Number', ''),
146
- 'Payer Address': record.get('Payer Address', ''),
147
- 'Amount Paid': record.get('Amount Paid', ''),
148
- 'Adjustment Amount': record.get('Adjustment Amount', ''),
149
- 'Allowed Amount': record.get('Allowed Amount', ''),
150
- 'Write Off': record.get('Write Off', ''),
151
- 'Patient Responsibility': record.get('Patient Responsibility', ''),
152
- 'Charge': record.get('Charge', ''),
153
- })
154
- # Explicitly flush data to ensure it's written
155
- csv_file.flush()
156
- except Exception as e:
157
- print("Error writing CSV: ", e)
158
-
159
- # User Interface
160
- def user_confirm_overwrite(chart_numbers):
161
- """Asks the user for confirmation to overwrite an existing file, showing Chart Numbers."""
162
- print("The following Chart Numbers are in the existing file:")
163
- for number in chart_numbers:
164
- print(number)
165
- return input("The file already exists. Do you want to overwrite it? (y/n): ").strip().lower() == 'y'
166
-
167
- if __name__ == "__main__":
168
- # Load configuration
169
-
170
- config, _ = load_configuration()
171
-
172
- # Setup logger
173
- local_storage_path = config['MediLink_Config']['local_storage_path']
174
-
175
- # Define output directory
176
- output_directory = os.path.join(local_storage_path, "translated_csvs")
177
-
178
- # Retrieve ERA files from command line arguments
179
- files = sys.argv[1:] # Exclude the script name
180
- if not files:
181
- log("No ERA files provided as arguments.")
182
- sys.exit(1)
183
-
184
- # Translate ERA files to CSV format
185
- translate_era_to_csv(files, output_directory)
186
-
187
- # Consolidate CSVs
188
- consolidate_csv_path = consolidate_csvs(output_directory)
189
- if consolidate_csv_path:
190
- print("Consolidated CSV File Created: {}".format(consolidate_csv_path))
191
- else:
192
- print("No CSV file was created.")