medicafe 0.250822.2__py3-none-any.whl → 0.250822.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.
@@ -42,7 +42,7 @@ else:
42
42
  load_and_parse_z_data = None
43
43
 
44
44
  # Import API functions using centralized import pattern
45
- MediLink_API_v3 = smart_import(['MediCafe.api_core', 'MediCafe.api_core_backup'])
45
+ MediLink_API_v3 = smart_import(['MediCafe.api_core'])
46
46
  fetch_payer_name_from_api = getattr(MediLink_API_v3, 'fetch_payer_name_from_api', None) if MediLink_API_v3 else None
47
47
 
48
48
  # Module-level cache to prevent redundant API calls
MediBot/__init__.py CHANGED
@@ -19,7 +19,7 @@ Smart Import Integration:
19
19
  medibot_main = get_components('medibot_main')
20
20
  """
21
21
 
22
- __version__ = "0.250822.2"
22
+ __version__ = "0.250822.3"
23
23
  __author__ = "Daniel Vidaud"
24
24
  __email__ = "daniel@personalizedtransformation.com"
25
25
 
MediCafe/__init__.py CHANGED
@@ -27,7 +27,7 @@ Smart Import System:
27
27
  api_suite = get_api_access()
28
28
  """
29
29
 
30
- __version__ = "0.250822.2"
30
+ __version__ = "0.250822.3"
31
31
  __author__ = "Daniel Vidaud"
32
32
  __email__ = "daniel@personalizedtransformation.com"
33
33
 
MediCafe/api_core.py CHANGED
@@ -118,10 +118,107 @@ except ImportError:
118
118
  class TokenCache:
119
119
  def __init__(self):
120
120
  self.tokens = {}
121
- def get(self, endpoint_name, current_time):
122
- return None
123
- def set(self, endpoint_name, access_token, expires_in, current_time):
124
- pass
121
+
122
+ # -----------------------------------------------------------------------------
123
+ # Endpoint-specific payer ID management (crosswalk-backed with hardcoded default)
124
+ # -----------------------------------------------------------------------------
125
+ # Intent:
126
+ # - Validate payer IDs against the endpoint actually being called.
127
+ # - Persist endpoint-specific payer ID lists into the crosswalk so they can be
128
+ # updated over time without changing code.
129
+ # - For OPTUMAI: use the augmented list (includes LIFE1, WELM2, etc.).
130
+ # - For UHCAPI (including its Super Connector fallback): strictly enforce the
131
+ # known-good UHC payer IDs only.
132
+ # - Future: OPTUMAI will expose a dedicated endpoint that returns its current
133
+ # valid payer list. When available, this function should fetch and refresh the
134
+ # crosswalk entry automatically (likely weekly/monthly), replacing the
135
+ # hardcoded default below. The UHCAPI Super Connector will eventually be
136
+ # deprecated; when removed, cleanup the UHC-specific paths accordingly.
137
+
138
+ try:
139
+ # Prefer using existing crosswalk persistence utilities
140
+ from MediBot.MediBot_Crosswalk_Utils import ensure_full_config_loaded, save_crosswalk
141
+ except Exception:
142
+ ensure_full_config_loaded = None
143
+ save_crosswalk = None
144
+
145
+ def _get_default_endpoint_payer_ids(endpoint_name):
146
+ """
147
+ Return hardcoded default payer IDs for a given endpoint.
148
+
149
+ NOTE: Defaults are used when crosswalk does not yet contain a list.
150
+ """
151
+ # UHC-only list – keep STRICT. Do not augment with non-UHC payers.
152
+ uhc_payer_ids = [
153
+ "87726", "03432", "96385", "95467", "86050", "86047", "95378", "06111", "37602"
154
+ ]
155
+
156
+ # OPTUMAI – augmented list (subject to growth once the API adds a payer-list endpoint)
157
+ optumai_payer_ids = [
158
+ "87726", "06111", "25463", "37602", "39026", "74227", "65088", "81400",
159
+ "03432", "86050", "86047", "95378", "95467", "LIFE1", "WELM2"
160
+ ]
161
+
162
+ if endpoint_name == 'OPTUMAI':
163
+ return optumai_payer_ids
164
+ # Default to UHCAPI for any other endpoint name
165
+ return uhc_payer_ids
166
+
167
+ def get_valid_payer_ids_for_endpoint(client, endpoint_name):
168
+ """
169
+ Resolve the valid payer IDs for a specific endpoint using crosswalk storage
170
+ with a safe fallback to hardcoded defaults.
171
+
172
+ Behavior:
173
+ - Attempts to read crosswalk['endpoint_payer_ids'][endpoint_name].
174
+ - If missing, initializes with hardcoded defaults and persists to crosswalk
175
+ (non-interactive) so that future sessions use the saved list.
176
+ - Future: For OPTUMAI, replace the hardcoded default by calling the API's
177
+ payer-list endpoint once available, then update the crosswalk.
178
+ """
179
+ try:
180
+ # Load full config + crosswalk (non-destructive)
181
+ base_config = None
182
+ crosswalk = None
183
+ if ensure_full_config_loaded is not None:
184
+ base_config, crosswalk = ensure_full_config_loaded(
185
+ getattr(client, 'config', None),
186
+ getattr(client, 'crosswalk', None)
187
+ )
188
+ else:
189
+ # Fallback: attempt to load via MediLink_ConfigLoader directly
190
+ # If we reach this fallback, it means ensure_full_config_loaded is not available.
191
+ # This is unexpected in normal operation and should be alerted.
192
+ print("Warning: IN api_core, ensure_full_config_loaded is not available; falling back to MediLink_ConfigLoader.load_configuration().")
193
+ MediLink_ConfigLoader.log(
194
+ "Fallback: ensure_full_config_loaded not available in get_valid_payer_ids_for_endpoint; using MediLink_ConfigLoader.load_configuration().",
195
+ level="WARNING"
196
+ )
197
+ base_config, crosswalk = MediLink_ConfigLoader.load_configuration()
198
+
199
+ # Extract any existing stored list
200
+ cw_ep = crosswalk.get('endpoint_payer_ids', {}) if isinstance(crosswalk, dict) else {}
201
+ existing = cw_ep.get(endpoint_name)
202
+ if isinstance(existing, list) and len(existing) > 0:
203
+ return existing
204
+
205
+ # Initialize from defaults and persist to crosswalk
206
+ defaults = _get_default_endpoint_payer_ids(endpoint_name)
207
+ if isinstance(crosswalk, dict):
208
+ if 'endpoint_payer_ids' not in crosswalk:
209
+ crosswalk['endpoint_payer_ids'] = {}
210
+ crosswalk['endpoint_payer_ids'][endpoint_name] = list(defaults)
211
+
212
+ # Persist without interactive prompts; ignore errors silently to avoid breaking flows
213
+ if save_crosswalk is not None:
214
+ try:
215
+ save_crosswalk(client, base_config, crosswalk, skip_api_operations=True)
216
+ except Exception:
217
+ pass
218
+ return defaults
219
+ except Exception:
220
+ # As a last resort, return a safe default for the endpoint
221
+ return _get_default_endpoint_payer_ids(endpoint_name)
125
222
 
126
223
  class BaseAPIClient:
127
224
  def __init__(self, config):
@@ -792,12 +889,14 @@ def get_eligibility_v3(client, payer_id, provider_last_name, search_option, date
792
889
  if not all([client, payer_id, provider_last_name, search_option, date_of_birth, member_id, npi]):
793
890
  raise ValueError("All required parameters must have values: client, payer_id, provider_last_name, search_option, date_of_birth, member_id, npi")
794
891
 
795
- # Validate payer_id
796
- valid_payer_ids = ["87726", "06111", "25463", "37602", "39026", "74227", "65088", "81400", "03432", "86050", "86047", "95378", "95467"]
797
- if payer_id not in valid_payer_ids:
798
- raise ValueError("Invalid payer_id: {}. Must be one of: {}".format(payer_id, ", ".join(valid_payer_ids)))
799
-
892
+ # Endpoint is UHCAPI for this v3 REST call
800
893
  endpoint_name = 'UHCAPI'
894
+
895
+ # Validate payer_id strictly against UHC list
896
+ valid_payer_ids = get_valid_payer_ids_for_endpoint(client, endpoint_name)
897
+ if payer_id not in valid_payer_ids:
898
+ raise ValueError("Invalid payer_id: {} for endpoint {}. Must be one of: {}".format(
899
+ payer_id, endpoint_name, ", ".join(valid_payer_ids)))
801
900
  from MediCafe.core_utils import extract_medilink_config
802
901
  medi = extract_medilink_config(client.config)
803
902
  url_extension = medi.get('endpoints', {}).get(endpoint_name, {}).get('additional_endpoints', {}).get('eligibility_v3', '')
@@ -856,11 +955,6 @@ def get_eligibility_super_connector(client, payer_id, provider_last_name, search
856
955
  if not all([client, payer_id, provider_last_name, search_option, date_of_birth, member_id, npi]):
857
956
  raise ValueError("All required parameters must have values: client, payer_id, provider_last_name, search_option, date_of_birth, member_id, npi")
858
957
 
859
- # Validate payer_id
860
- valid_payer_ids = ["87726", "06111", "25463", "37602", "39026", "74227", "65088", "81400", "03432", "86050", "86047", "95378", "95467"]
861
- if payer_id not in valid_payer_ids:
862
- raise ValueError("Invalid payer_id: {}. Must be one of: {}".format(payer_id, ", ".join(valid_payer_ids)))
863
-
864
958
  # Prefer OPTUMAI endpoint if configured, otherwise fall back to legacy UHCAPI super connector
865
959
  try:
866
960
  endpoints_cfg = client.config['MediLink_Config']['endpoints']
@@ -889,6 +983,14 @@ def get_eligibility_super_connector(client, payer_id, provider_last_name, search
889
983
  except Exception:
890
984
  url_extension = None
891
985
 
986
+ # Validate payer_id against the selected endpoint's list
987
+ # - If OPTUMAI is used, allow the augmented list (includes LIFE1, WELM2, etc.).
988
+ # - If UHCAPI fallback is used, enforce strict UHC list only.
989
+ valid_payer_ids = get_valid_payer_ids_for_endpoint(client, endpoint_name)
990
+ if payer_id not in valid_payer_ids:
991
+ raise ValueError("Invalid payer_id: {} for endpoint {}. Must be one of: {}".format(
992
+ payer_id, endpoint_name, ", ".join(valid_payer_ids)))
993
+
892
994
  if not url_extension:
893
995
  raise ValueError("Eligibility endpoint not configured for {}".format(endpoint_name))
894
996
 
MediCafe/core_utils.py CHANGED
@@ -499,9 +499,7 @@ def get_api_client_factory():
499
499
  """
500
500
  # Try multiple import paths for factory
501
501
  import_specs = [
502
- ('MediCafe.api_factory', 'APIClientFactory'),
503
- ('MediLink.MediLink_API_Factory', 'APIClientFactory'), # Legacy fallback
504
- ('MediLink_API_Factory', 'APIClientFactory') # Legacy fallback
502
+ ('MediCafe.api_factory', 'APIClientFactory')
505
503
  ]
506
504
 
507
505
  APIClientFactory = import_with_alternatives(import_specs)
@@ -50,6 +50,11 @@ UPGRADED TO LATEST CORE_UTILS:
50
50
  - Improved import error handling with fallbacks
51
51
  """
52
52
  # MediLink_Deductible.py
53
+ """
54
+ TODO Consdier the possibility of being CSV agnostic and looking for the date of service up to 60 days old and
55
+ then with an option to select specific patients to look up for all the valid rows.
56
+
57
+ """
53
58
  import os, sys, json
54
59
  from datetime import datetime
55
60
 
@@ -116,14 +121,15 @@ except ImportError as e:
116
121
 
117
122
  # Function to check if the date format is correct
118
123
  def validate_and_format_date(date_str):
119
- # Comprehensive list of common DOB date formats
120
- date_formats = [
121
- # 4-digit year formats
124
+ """
125
+ Enhanced date parsing that handles ambiguous formats intelligently.
126
+ For ambiguous formats like MM/DD vs DD/MM, uses heuristics to determine the most likely interpretation.
127
+ """
128
+ import re
129
+
130
+ # First, try unambiguous formats (4-digit years, month names, etc.)
131
+ unambiguous_formats = [
122
132
  '%Y-%m-%d', # 1990-01-15
123
- '%m/%d/%Y', # 01/15/1990
124
- '%m-%d-%Y', # 01-15-1990
125
- '%d-%m-%Y', # 15-01-1990
126
- '%d/%m/%Y', # 15/01/1990
127
133
  '%d-%b-%Y', # 15-Jan-1990
128
134
  '%d %b %Y', # 15 Jan 1990
129
135
  '%b %d, %Y', # Jan 15, 1990
@@ -132,46 +138,114 @@ def validate_and_format_date(date_str):
132
138
  '%B %d %Y', # January 15 1990
133
139
  '%Y/%m/%d', # 1990/01/15
134
140
  '%Y%m%d', # 19900115
141
+ '%y%m%d', # 900115 (unambiguous compact format)
142
+ ]
143
+
144
+ # Try unambiguous formats first
145
+ for fmt in unambiguous_formats:
146
+ try:
147
+ if '%y' in fmt:
148
+ parsed_date = datetime.strptime(date_str, fmt)
149
+ if parsed_date.year < 50:
150
+ parsed_date = parsed_date.replace(year=parsed_date.year + 2000)
151
+ elif parsed_date.year < 100:
152
+ parsed_date = parsed_date.replace(year=parsed_date.year + 1900)
153
+ return parsed_date.strftime('%Y-%m-%d')
154
+ else:
155
+ return datetime.strptime(date_str, fmt).strftime('%Y-%m-%d')
156
+ except ValueError:
157
+ continue
158
+
159
+ # Handle potentially ambiguous formats with smart heuristics
160
+ # Check if it's a MM/DD/YYYY or DD/MM/YYYY pattern
161
+ ambiguous_pattern = re.match(r'^(\d{1,2})[/-](\d{1,2})[/-](\d{4})$', date_str)
162
+ if ambiguous_pattern:
163
+ first_num, second_num, year = map(int, ambiguous_pattern.groups())
164
+
165
+ # If first number > 12, it must be DD/MM/YYYY format
166
+ if first_num > 12:
167
+ try:
168
+ return datetime(int(year), int(second_num), int(first_num)).strftime('%Y-%m-%d')
169
+ except ValueError:
170
+ return None
171
+
172
+ # If second number > 12, it must be MM/DD/YYYY format
173
+ elif second_num > 12:
174
+ try:
175
+ return datetime(int(year), int(first_num), int(second_num)).strftime('%Y-%m-%d')
176
+ except ValueError:
177
+ return None
178
+
179
+ # Both numbers could be valid months (1-12), need to make an educated guess
180
+ else:
181
+ # Preference heuristic: In US context, MM/DD/YYYY is more common
182
+ # But also consider: if first number is 1-12 and second is 1-31, both are possible
183
+ # Default to MM/DD/YYYY for US-centric systems, but this could be configurable
184
+ try:
185
+ # Try MM/DD/YYYY first (US preference)
186
+ return datetime(int(year), int(first_num), int(second_num)).strftime('%Y-%m-%d')
187
+ except ValueError:
188
+ try:
189
+ # If that fails, try DD/MM/YYYY
190
+ return datetime(int(year), int(second_num), int(first_num)).strftime('%Y-%m-%d')
191
+ except ValueError:
192
+ return None
193
+
194
+ # Handle 2-digit year ambiguous formats
195
+ ambiguous_2digit_pattern = re.match(r'^(\d{1,2})[/-](\d{1,2})[/-](\d{2})$', date_str)
196
+ if ambiguous_2digit_pattern:
197
+ first_num, second_num, year = map(int, ambiguous_2digit_pattern.groups())
198
+
199
+ # Apply same logic as above, but handle 2-digit year
200
+ year = 2000 + year if year < 50 else 1900 + year
201
+
202
+ if first_num > 12:
203
+ try:
204
+ return datetime(year, second_num, first_num).strftime('%Y-%m-%d')
205
+ except ValueError:
206
+ return None
207
+ elif second_num > 12:
208
+ try:
209
+ return datetime(year, first_num, second_num).strftime('%Y-%m-%d')
210
+ except ValueError:
211
+ return None
212
+ else:
213
+ # Default to MM/DD/YY (US preference)
214
+ try:
215
+ return datetime(year, first_num, second_num).strftime('%Y-%m-%d')
216
+ except ValueError:
217
+ try:
218
+ return datetime(year, second_num, first_num).strftime('%Y-%m-%d')
219
+ except ValueError:
220
+ return None
135
221
 
136
- # 2-digit year formats
137
- '%m/%d/%y', # 01/15/90
222
+ # Try remaining formats that are less likely to be ambiguous
223
+ remaining_formats = [
224
+ '%m-%d-%Y', # 01-15-1990
225
+ '%d-%m-%Y', # 15-01-1990
226
+ '%d/%m/%Y', # 15/01/1990
138
227
  '%m-%d-%y', # 01-15-90
139
- '%d/%m/%y', # 15/01/90
140
228
  '%d-%m-%y', # 15-01-90
141
- '%d %b %y', # 15 Jan 90
142
229
  '%b %d, %y', # Jan 15, 90
143
230
  '%b %d %y', # Jan 15 90
144
231
  '%y/%m/%d', # 90/01/15
145
232
  '%y-%m-%d', # 90-01-15
146
- '%y%m%d', # 900115
147
-
148
- # Single digit formats (no leading zeros)
149
- '%m/%d/%Y', # 1/15/1990 (already covered above)
150
- '%m-%d-%Y', # 1-15-1990 (already covered above)
151
- '%d/%m/%Y', # 15/1/1990 (already covered above)
152
- '%d-%m-%Y', # 15-1-1990 (already covered above)
153
- '%m/%d/%y', # 1/15/90 (already covered above)
154
- '%m-%d-%y', # 1-15-90 (already covered above)
155
- '%d/%m/%y', # 15/1/90 (already covered above)
156
- '%d-%m-%y', # 15-1-90 (already covered above)
157
233
  ]
158
234
 
159
- for fmt in date_formats:
235
+ for fmt in remaining_formats:
160
236
  try:
161
- # For 2-digit years, assume 20th/21st century
162
237
  if '%y' in fmt:
163
238
  parsed_date = datetime.strptime(date_str, fmt)
164
- # Handle year 00-99: assume 1950-2049 range
165
239
  if parsed_date.year < 50:
166
240
  parsed_date = parsed_date.replace(year=parsed_date.year + 2000)
167
241
  elif parsed_date.year < 100:
168
242
  parsed_date = parsed_date.replace(year=parsed_date.year + 1900)
169
- formatted_date = parsed_date.strftime('%Y-%m-%d')
243
+ return parsed_date.strftime('%Y-%m-%d')
170
244
  else:
171
- formatted_date = datetime.strptime(date_str, fmt).strftime('%Y-%m-%d')
172
- return formatted_date
245
+ return datetime.strptime(date_str, fmt).strftime('%Y-%m-%d')
173
246
  except ValueError:
174
247
  continue
248
+
175
249
  return None
176
250
 
177
251
  # Use latest core_utils configuration cache for better performance
@@ -203,7 +277,8 @@ if provider_last_name == 'Unknown':
203
277
  MediLink_ConfigLoader.log("Warning: provider_last_name was not found in the configuration.", level="WARNING")
204
278
 
205
279
  # Define the list of payer_id's to iterate over
206
- payer_ids = ['87726', '03432', '96385', '95467', '86050', '86047', '95378', '06111', '37602'] # United Healthcare.
280
+ payer_ids = ['87726', '03432', '96385', '95467', '86050', '86047', '95378', '06111', '37602'] # United Healthcare ONLY.
281
+
207
282
 
208
283
  # Get the latest CSV
209
284
  CSV_FILE_PATH = config.get('CSV_FILE_PATH', "")
@@ -222,10 +297,11 @@ summary_valid_rows = [
222
297
  for row in valid_rows
223
298
  ]
224
299
 
225
- # Print summary of valid rows
226
- print("\n--- Summary of Valid Rows ---")
227
- for row in summary_valid_rows:
228
- print("DOB: {}, Member ID: {}, Payer ID: {}".format(row['DOB'], row['Ins1 Member ID'], row['Ins1 Payer ID']))
300
+ # Display enhanced summary of valid rows using unified display philosophy
301
+ from MediLink_Display_Utils import display_enhanced_deductible_table
302
+
303
+ # Use the enhanced table display for pre-API context
304
+ display_enhanced_deductible_table(valid_rows, context="pre_api")
229
305
 
230
306
  # List of patients with DOB and MemberID from CSV data with fallback
231
307
  patients = [
@@ -274,6 +350,15 @@ def manual_deductible_lookup():
274
350
  eligibility_data = get_eligibility_info(client, payer_id, provider_last_name, formatted_dob, member_id, npi, run_validation=run_validation, is_manual_lookup=True)
275
351
  if eligibility_data:
276
352
  found_data = True
353
+
354
+ # Convert to enhanced format and display
355
+ enhanced_result = convert_eligibility_to_enhanced_format(eligibility_data, formatted_dob, member_id)
356
+ if enhanced_result:
357
+ print("\n" + "=" * 60)
358
+ display_enhanced_deductible_table([enhanced_result], context="post_api",
359
+ title="Manual Lookup Result")
360
+ print("=" * 60)
361
+
277
362
  # Generate unique output file for manual request
278
363
  output_file_name = "eligibility_report_manual_{}_{}.txt".format(member_id, formatted_dob)
279
364
  output_file_path = os.path.join(os.getenv('TEMP'), output_file_name)
@@ -282,8 +367,6 @@ def manual_deductible_lookup():
282
367
  "Patient Name", "DOB", "Insurance Type", "PayID", "Policy Status", "Remaining Amt")
283
368
  output_file.write(table_header + "\n")
284
369
  output_file.write("-" * len(table_header) + "\n")
285
- print(table_header)
286
- print("-" * len(table_header))
287
370
  display_eligibility_info(eligibility_data, formatted_dob, member_id, output_file)
288
371
 
289
372
  # Ask if user wants to open the report
@@ -799,10 +882,11 @@ def is_super_connector_response_format(data):
799
882
  """Determine if the response is in Super Connector format (has rawGraphQLResponse)"""
800
883
  return data is not None and "rawGraphQLResponse" in data
801
884
 
802
- # Function to extract required fields and display in a tabular format
803
- def display_eligibility_info(data, dob, member_id, output_file):
885
+ # Function to convert eligibility data to enhanced display format
886
+ def convert_eligibility_to_enhanced_format(data, dob, member_id, patient_id="", service_date=""):
887
+ """Convert API eligibility response to enhanced display format"""
804
888
  if data is None:
805
- return
889
+ return None
806
890
 
807
891
  # Determine which API response format we're dealing with
808
892
  if is_legacy_response_format(data):
@@ -821,14 +905,21 @@ def display_eligibility_info(data, dob, member_id, output_file):
821
905
  patient_info['firstName'],
822
906
  patient_info['middleName'],
823
907
  patient_info['lastName']
824
- ).strip()[:20]
908
+ ).strip()
825
909
 
826
- # Display patient information in a table row format
827
- table_row = "{:<20} | {:<10} | {:<40} | {:<5} | {:<14} | {:<14}".format(
828
- patient_name, dob, insurance_info['insuranceType'],
829
- insurance_info['payerId'], policy_status, remaining_amount)
830
- output_file.write(table_row + "\n")
831
- print(table_row) # Print to console for progressive display
910
+ return {
911
+ 'patient_id': patient_id,
912
+ 'patient_name': patient_name,
913
+ 'dob': dob,
914
+ 'member_id': member_id,
915
+ 'payer_id': insurance_info['payerId'],
916
+ 'service_date_display': service_date,
917
+ 'service_date_sort': datetime.min, # Will be enhanced later
918
+ 'status': 'Processed',
919
+ 'insurance_type': insurance_info['insuranceType'],
920
+ 'policy_status': policy_status,
921
+ 'remaining_amount': remaining_amount
922
+ }
832
923
 
833
924
  elif is_super_connector_response_format(data):
834
925
  # Handle Super Connector API response format
@@ -841,19 +932,47 @@ def display_eligibility_info(data, dob, member_id, output_file):
841
932
  patient_info['firstName'],
842
933
  patient_info['middleName'],
843
934
  patient_info['lastName']
844
- ).strip()[:20]
935
+ ).strip()
845
936
 
846
- # Display patient information in a table row format
847
- table_row = "{:<20} | {:<10} | {:<40} | {:<5} | {:<14} | {:<14}".format(
848
- patient_name, dob, insurance_info['insuranceType'],
849
- insurance_info['payerId'], policy_status, remaining_amount)
850
- output_file.write(table_row + "\n")
851
- print(table_row) # Print to console for progressive display
937
+ return {
938
+ 'patient_id': patient_id,
939
+ 'patient_name': patient_name,
940
+ 'dob': dob,
941
+ 'member_id': member_id,
942
+ 'payer_id': insurance_info['payerId'],
943
+ 'service_date_display': service_date,
944
+ 'service_date_sort': datetime.min, # Will be enhanced later
945
+ 'status': 'Processed',
946
+ 'insurance_type': insurance_info['insuranceType'],
947
+ 'policy_status': policy_status,
948
+ 'remaining_amount': remaining_amount
949
+ }
852
950
 
853
951
  else:
854
952
  # Unknown response format - log for debugging
855
- MediLink_ConfigLoader.log("Unknown response format in display_eligibility_info", level="WARNING")
953
+ MediLink_ConfigLoader.log("Unknown response format in convert_eligibility_to_enhanced_format", level="WARNING")
856
954
  MediLink_ConfigLoader.log("Response structure: {}".format(json.dumps(data, indent=2)), level="DEBUG")
955
+ return None
956
+
957
+ # Function to extract required fields and display in a tabular format
958
+ def display_eligibility_info(data, dob, member_id, output_file, patient_id="", service_date=""):
959
+ """Legacy display function - converts to enhanced format and displays"""
960
+ if data is None:
961
+ return
962
+
963
+ # Convert to enhanced format
964
+ enhanced_data = convert_eligibility_to_enhanced_format(data, dob, member_id, patient_id, service_date)
965
+ if enhanced_data:
966
+ # Write to output file in legacy format for compatibility
967
+ table_row = "{:<20} | {:<10} | {:<40} | {:<5} | {:<14} | {:<14}".format(
968
+ enhanced_data['patient_name'][:20],
969
+ enhanced_data['dob'],
970
+ enhanced_data['insurance_type'][:40],
971
+ enhanced_data['payer_id'][:5],
972
+ enhanced_data['policy_status'][:14],
973
+ enhanced_data['remaining_amount'][:14])
974
+ output_file.write(table_row + "\n")
975
+ print(table_row) # Print to console for progressive display
857
976
 
858
977
  # Global mode flags (will be set in main)
859
978
  LEGACY_MODE = False
@@ -928,60 +1047,80 @@ if __name__ == "__main__":
928
1047
  print("Batch processing cancelled.")
929
1048
  continue
930
1049
 
1050
+ # PERFORMANCE FIX: Optimize patient-payer processing to avoid O(PxN) complexity
1051
+ # Instead of nested loops, process each patient once and try payer_ids until success
1052
+ # TODO: We should be able to determine the correct payer_id for each patient ahead of time
1053
+ # by looking up their insurance information from the CSV data or crosswalk mapping.
1054
+ # This would eliminate the need to try multiple payer_ids per patient and make this O(N).
1055
+ # CLARIFICATION: In production, use the payer_id from the CSV/crosswalk as primary.
1056
+ # Retain multi-payer probing behind a DEBUG/DIAGNOSTIC feature toggle only.
1057
+ # Suggested flag: DEBUG_MODE_PAYER_PROBE = False (module-level), default False.
1058
+ errors = []
1059
+ validation_reports = []
1060
+ processed_count = 0
1061
+ validation_files_created = [] # Track validation files that were actually created
1062
+ eligibility_results = [] # Collect all results for enhanced display
1063
+
1064
+ for dob, member_id in patients:
1065
+ processed_count += 1
1066
+ print("Processing patient {}/{}: Member ID {}, DOB {}".format(
1067
+ processed_count, len(patients), member_id, dob))
1068
+
1069
+ # Try each payer_id for this patient until we get a successful response
1070
+ patient_processed = False
1071
+ for payer_id in payer_ids:
1072
+ try:
1073
+ # Run with validation enabled only in debug mode
1074
+ run_validation = DEBUG_MODE
1075
+ eligibility_data = get_eligibility_info(client, payer_id, provider_last_name, dob, member_id, npi, run_validation=run_validation, is_manual_lookup=False)
1076
+ if eligibility_data is not None:
1077
+ # Convert to enhanced format and collect
1078
+ enhanced_result = convert_eligibility_to_enhanced_format(eligibility_data, dob, member_id)
1079
+ if enhanced_result:
1080
+ eligibility_results.append(enhanced_result)
1081
+ patient_processed = True
1082
+
1083
+ # Track validation file creation in debug mode
1084
+ if DEBUG_MODE:
1085
+ validation_file_path = os.path.join(os.getenv('TEMP'), 'validation_report_{}_{}.txt'.format(member_id, dob))
1086
+ if os.path.exists(validation_file_path):
1087
+ validation_files_created.append(validation_file_path)
1088
+ print(" Validation report created: {}".format(os.path.basename(validation_file_path)))
1089
+
1090
+ break # Stop trying other payer_ids for this patient once we get a response
1091
+ except Exception as e:
1092
+ # Continue trying other payer_ids
1093
+ continue
1094
+
1095
+ # If no payer_id worked for this patient, log the error
1096
+ if not patient_processed:
1097
+ error_msg = "No successful payer_id found for patient"
1098
+ errors.append((dob, member_id, error_msg))
1099
+
1100
+ # Display results using enhanced table
1101
+ if eligibility_results:
1102
+ print("\n" + "=" * 80)
1103
+ display_enhanced_deductible_table(eligibility_results, context="post_api")
1104
+ print("=" * 80)
1105
+
1106
+ # Write results to file for legacy compatibility
931
1107
  output_file_path = os.path.join(os.getenv('TEMP'), 'eligibility_report.txt')
932
1108
  with open(output_file_path, 'w') as output_file:
933
1109
  table_header = "{:<20} | {:<10} | {:<40} | {:<5} | {:<14} | {:<14}".format(
934
1110
  "Patient Name", "DOB", "Insurance Type", "PayID", "Policy Status", "Remaining Amt")
935
1111
  output_file.write(table_header + "\n")
936
1112
  output_file.write("-" * len(table_header) + "\n")
937
- print(table_header)
938
- print("-" * len(table_header))
939
-
940
- # PERFORMANCE FIX: Optimize patient-payer processing to avoid O(PxN) complexity
941
- # Instead of nested loops, process each patient once and try payer_ids until success
942
- # TODO: We should be able to determine the correct payer_id for each patient ahead of time
943
- # by looking up their insurance information from the CSV data or crosswalk mapping.
944
- # This would eliminate the need to try multiple payer_ids per patient and make this O(N).
945
- # CLARIFICATION: In production, use the payer_id from the CSV/crosswalk as primary.
946
- # Retain multi-payer probing behind a DEBUG/DIAGNOSTIC feature toggle only.
947
- # Suggested flag: DEBUG_MODE_PAYER_PROBE = False (module-level), default False.
948
- errors = []
949
- validation_reports = []
950
- processed_count = 0
951
- validation_files_created = [] # Track validation files that were actually created
952
1113
 
953
- for dob, member_id in patients:
954
- processed_count += 1
955
- print("Processing patient {}/{}: Member ID {}, DOB {}".format(
956
- processed_count, len(patients), member_id, dob))
957
-
958
- # Try each payer_id for this patient until we get a successful response
959
- patient_processed = False
960
- for payer_id in payer_ids:
961
- try:
962
- # Run with validation enabled only in debug mode
963
- run_validation = DEBUG_MODE
964
- eligibility_data = get_eligibility_info(client, payer_id, provider_last_name, dob, member_id, npi, run_validation=run_validation, is_manual_lookup=False)
965
- if eligibility_data is not None:
966
- display_eligibility_info(eligibility_data, dob, member_id, output_file)
967
- patient_processed = True
968
-
969
- # Track validation file creation in debug mode
970
- if DEBUG_MODE:
971
- validation_file_path = os.path.join(os.getenv('TEMP'), 'validation_report_{}_{}.txt'.format(member_id, dob))
972
- if os.path.exists(validation_file_path):
973
- validation_files_created.append(validation_file_path)
974
- print(" Validation report created: {}".format(os.path.basename(validation_file_path)))
975
-
976
- break # Stop trying other payer_ids for this patient once we get a response
977
- except Exception as e:
978
- # Continue trying other payer_ids
979
- continue
980
-
981
- # If no payer_id worked for this patient, log the error
982
- if not patient_processed:
983
- error_msg = "No successful payer_id found for patient"
984
- errors.append((dob, member_id, error_msg))
1114
+ # Write all results to file
1115
+ for result in eligibility_results:
1116
+ table_row = "{:<20} | {:<10} | {:<40} | {:<5} | {:<14} | {:<14}".format(
1117
+ result['patient_name'][:20],
1118
+ result['dob'],
1119
+ result['insurance_type'][:40],
1120
+ result['payer_id'][:5],
1121
+ result['policy_status'][:14],
1122
+ result['remaining_amount'][:14])
1123
+ output_file.write(table_row + "\n")
985
1124
 
986
1125
  # Display errors if any
987
1126
  if errors: