medicafe 0.250822.1__tar.gz → 0.250822.3__tar.gz

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.
Files changed (88) hide show
  1. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot_Crosswalk_Library.py +1 -1
  2. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/__init__.py +1 -1
  3. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediCafe/__init__.py +1 -1
  4. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediCafe/api_core.py +116 -14
  5. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediCafe/core_utils.py +1 -3
  6. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_Deductible.py +264 -75
  7. medicafe-0.250822.3/MediLink/MediLink_Display_Utils.py +419 -0
  8. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/__init__.py +1 -1
  9. {medicafe-0.250822.1/medicafe.egg-info → medicafe-0.250822.3}/PKG-INFO +1 -1
  10. {medicafe-0.250822.1 → medicafe-0.250822.3/medicafe.egg-info}/PKG-INFO +1 -1
  11. {medicafe-0.250822.1 → medicafe-0.250822.3}/medicafe.egg-info/SOURCES.txt +0 -5
  12. {medicafe-0.250822.1 → medicafe-0.250822.3}/setup.py +1 -1
  13. medicafe-0.250822.1/MediCafe/api_core_backup.py +0 -428
  14. medicafe-0.250822.1/MediLink/MediLink_Display_Utils.py +0 -95
  15. medicafe-0.250822.1/MediLink/insurance_type_integration_test.py +0 -361
  16. medicafe-0.250822.1/MediLink/test_cob_library.py +0 -436
  17. medicafe-0.250822.1/MediLink/test_timing.py +0 -59
  18. medicafe-0.250822.1/MediLink/test_validation.py +0 -127
  19. {medicafe-0.250822.1 → medicafe-0.250822.3}/LICENSE +0 -0
  20. {medicafe-0.250822.1 → medicafe-0.250822.3}/MANIFEST.in +0 -0
  21. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot.bat +0 -0
  22. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot.py +0 -0
  23. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot_Charges.py +0 -0
  24. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot_Crosswalk_Utils.py +0 -0
  25. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot_Notepad_Utils.py +0 -0
  26. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot_Post.py +0 -0
  27. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot_Preprocessor.py +0 -0
  28. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot_Preprocessor_lib.py +0 -0
  29. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot_UI.py +0 -0
  30. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot_dataformat_library.py +0 -0
  31. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot_debug.bat +0 -0
  32. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot_docx_decoder.py +0 -0
  33. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot_smart_import.py +0 -0
  34. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/clear_cache.bat +0 -0
  35. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/crash_diagnostic.bat +0 -0
  36. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/f_drive_diagnostic.bat +0 -0
  37. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/full_debug_suite.bat +0 -0
  38. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/get_medicafe_version.py +0 -0
  39. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/process_csvs.bat +0 -0
  40. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/update_json.py +0 -0
  41. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/update_medicafe.py +0 -0
  42. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediCafe/MediLink_ConfigLoader.py +0 -0
  43. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediCafe/__main__.py +0 -0
  44. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediCafe/api_factory.py +0 -0
  45. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediCafe/api_utils.py +0 -0
  46. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediCafe/graphql_utils.py +0 -0
  47. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediCafe/logging_config.py +0 -0
  48. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediCafe/logging_demo.py +0 -0
  49. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediCafe/migration_helpers.py +0 -0
  50. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediCafe/smart_import.py +0 -0
  51. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediCafe/submission_index.py +0 -0
  52. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/InsuranceTypeService.py +0 -0
  53. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_837p_cob_library.py +0 -0
  54. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_837p_encoder.py +0 -0
  55. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_837p_encoder_library.py +0 -0
  56. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_837p_utilities.py +0 -0
  57. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_API_Generator.py +0 -0
  58. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_Azure.py +0 -0
  59. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_Charges.py +0 -0
  60. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_ClaimStatus.py +0 -0
  61. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_DataMgmt.py +0 -0
  62. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_Decoder.py +0 -0
  63. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_Deductible_Validator.py +0 -0
  64. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_Down.py +0 -0
  65. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_Gmail.py +0 -0
  66. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_Mailer.py +0 -0
  67. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_Parser.py +0 -0
  68. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_PatientProcessor.py +0 -0
  69. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_Scan.py +0 -0
  70. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_Scheduler.py +0 -0
  71. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_UI.py +0 -0
  72. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_Up.py +0 -0
  73. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_insurance_utils.py +0 -0
  74. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_main.py +0 -0
  75. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_smart_import.py +0 -0
  76. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/Soumit_api.py +0 -0
  77. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/gmail_http_utils.py +0 -0
  78. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/gmail_oauth_utils.py +0 -0
  79. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/openssl.cnf +0 -0
  80. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/test.py +0 -0
  81. {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/webapp.html +0 -0
  82. {medicafe-0.250822.1 → medicafe-0.250822.3}/README.md +0 -0
  83. {medicafe-0.250822.1 → medicafe-0.250822.3}/medicafe.egg-info/dependency_links.txt +0 -0
  84. {medicafe-0.250822.1 → medicafe-0.250822.3}/medicafe.egg-info/entry_points.txt +0 -0
  85. {medicafe-0.250822.1 → medicafe-0.250822.3}/medicafe.egg-info/not-zip-safe +0 -0
  86. {medicafe-0.250822.1 → medicafe-0.250822.3}/medicafe.egg-info/requires.txt +0 -0
  87. {medicafe-0.250822.1 → medicafe-0.250822.3}/medicafe.egg-info/top_level.txt +0 -0
  88. {medicafe-0.250822.1 → medicafe-0.250822.3}/setup.cfg +0 -0
@@ -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
@@ -19,7 +19,7 @@ Smart Import Integration:
19
19
  medibot_main = get_components('medibot_main')
20
20
  """
21
21
 
22
- __version__ = "0.250822.1"
22
+ __version__ = "0.250822.3"
23
23
  __author__ = "Daniel Vidaud"
24
24
  __email__ = "daniel@personalizedtransformation.com"
25
25
 
@@ -27,7 +27,7 @@ Smart Import System:
27
27
  api_suite = get_api_access()
28
28
  """
29
29
 
30
- __version__ = "0.250822.1"
30
+ __version__ = "0.250822.3"
31
31
  __author__ = "Daniel Vidaud"
32
32
  __email__ = "daniel@personalizedtransformation.com"
33
33
 
@@ -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
 
@@ -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,12 +121,131 @@ 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
- for fmt in ('%Y-%m-%d', '%m/%d/%Y', '%d-%b-%Y', '%d-%m-%Y'):
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 = [
132
+ '%Y-%m-%d', # 1990-01-15
133
+ '%d-%b-%Y', # 15-Jan-1990
134
+ '%d %b %Y', # 15 Jan 1990
135
+ '%b %d, %Y', # Jan 15, 1990
136
+ '%b %d %Y', # Jan 15 1990
137
+ '%B %d, %Y', # January 15, 1990
138
+ '%B %d %Y', # January 15 1990
139
+ '%Y/%m/%d', # 1990/01/15
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:
120
146
  try:
121
- formatted_date = datetime.strptime(date_str, fmt).strftime('%Y-%m-%d')
122
- return formatted_date
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')
123
156
  except ValueError:
124
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
221
+
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
227
+ '%m-%d-%y', # 01-15-90
228
+ '%d-%m-%y', # 15-01-90
229
+ '%b %d, %y', # Jan 15, 90
230
+ '%b %d %y', # Jan 15 90
231
+ '%y/%m/%d', # 90/01/15
232
+ '%y-%m-%d', # 90-01-15
233
+ ]
234
+
235
+ for fmt in remaining_formats:
236
+ try:
237
+ if '%y' in fmt:
238
+ parsed_date = datetime.strptime(date_str, fmt)
239
+ if parsed_date.year < 50:
240
+ parsed_date = parsed_date.replace(year=parsed_date.year + 2000)
241
+ elif parsed_date.year < 100:
242
+ parsed_date = parsed_date.replace(year=parsed_date.year + 1900)
243
+ return parsed_date.strftime('%Y-%m-%d')
244
+ else:
245
+ return datetime.strptime(date_str, fmt).strftime('%Y-%m-%d')
246
+ except ValueError:
247
+ continue
248
+
125
249
  return None
126
250
 
127
251
  # Use latest core_utils configuration cache for better performance
@@ -153,7 +277,8 @@ if provider_last_name == 'Unknown':
153
277
  MediLink_ConfigLoader.log("Warning: provider_last_name was not found in the configuration.", level="WARNING")
154
278
 
155
279
  # Define the list of payer_id's to iterate over
156
- 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
+
157
282
 
158
283
  # Get the latest CSV
159
284
  CSV_FILE_PATH = config.get('CSV_FILE_PATH', "")
@@ -172,10 +297,11 @@ summary_valid_rows = [
172
297
  for row in valid_rows
173
298
  ]
174
299
 
175
- # Print summary of valid rows
176
- print("\n--- Summary of Valid Rows ---")
177
- for row in summary_valid_rows:
178
- 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")
179
305
 
180
306
  # List of patients with DOB and MemberID from CSV data with fallback
181
307
  patients = [
@@ -224,6 +350,15 @@ def manual_deductible_lookup():
224
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)
225
351
  if eligibility_data:
226
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
+
227
362
  # Generate unique output file for manual request
228
363
  output_file_name = "eligibility_report_manual_{}_{}.txt".format(member_id, formatted_dob)
229
364
  output_file_path = os.path.join(os.getenv('TEMP'), output_file_name)
@@ -232,8 +367,6 @@ def manual_deductible_lookup():
232
367
  "Patient Name", "DOB", "Insurance Type", "PayID", "Policy Status", "Remaining Amt")
233
368
  output_file.write(table_header + "\n")
234
369
  output_file.write("-" * len(table_header) + "\n")
235
- print(table_header)
236
- print("-" * len(table_header))
237
370
  display_eligibility_info(eligibility_data, formatted_dob, member_id, output_file)
238
371
 
239
372
  # Ask if user wants to open the report
@@ -749,10 +882,11 @@ def is_super_connector_response_format(data):
749
882
  """Determine if the response is in Super Connector format (has rawGraphQLResponse)"""
750
883
  return data is not None and "rawGraphQLResponse" in data
751
884
 
752
- # Function to extract required fields and display in a tabular format
753
- 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"""
754
888
  if data is None:
755
- return
889
+ return None
756
890
 
757
891
  # Determine which API response format we're dealing with
758
892
  if is_legacy_response_format(data):
@@ -771,14 +905,21 @@ def display_eligibility_info(data, dob, member_id, output_file):
771
905
  patient_info['firstName'],
772
906
  patient_info['middleName'],
773
907
  patient_info['lastName']
774
- ).strip()[:20]
908
+ ).strip()
775
909
 
776
- # Display patient information in a table row format
777
- table_row = "{:<20} | {:<10} | {:<40} | {:<5} | {:<14} | {:<14}".format(
778
- patient_name, dob, insurance_info['insuranceType'],
779
- insurance_info['payerId'], policy_status, remaining_amount)
780
- output_file.write(table_row + "\n")
781
- 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
+ }
782
923
 
783
924
  elif is_super_connector_response_format(data):
784
925
  # Handle Super Connector API response format
@@ -791,19 +932,47 @@ def display_eligibility_info(data, dob, member_id, output_file):
791
932
  patient_info['firstName'],
792
933
  patient_info['middleName'],
793
934
  patient_info['lastName']
794
- ).strip()[:20]
935
+ ).strip()
795
936
 
796
- # Display patient information in a table row format
797
- table_row = "{:<20} | {:<10} | {:<40} | {:<5} | {:<14} | {:<14}".format(
798
- patient_name, dob, insurance_info['insuranceType'],
799
- insurance_info['payerId'], policy_status, remaining_amount)
800
- output_file.write(table_row + "\n")
801
- 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
+ }
802
950
 
803
951
  else:
804
952
  # Unknown response format - log for debugging
805
- 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")
806
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
807
976
 
808
977
  # Global mode flags (will be set in main)
809
978
  LEGACY_MODE = False
@@ -878,60 +1047,80 @@ if __name__ == "__main__":
878
1047
  print("Batch processing cancelled.")
879
1048
  continue
880
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
881
1107
  output_file_path = os.path.join(os.getenv('TEMP'), 'eligibility_report.txt')
882
1108
  with open(output_file_path, 'w') as output_file:
883
1109
  table_header = "{:<20} | {:<10} | {:<40} | {:<5} | {:<14} | {:<14}".format(
884
1110
  "Patient Name", "DOB", "Insurance Type", "PayID", "Policy Status", "Remaining Amt")
885
1111
  output_file.write(table_header + "\n")
886
1112
  output_file.write("-" * len(table_header) + "\n")
887
- print(table_header)
888
- print("-" * len(table_header))
889
-
890
- # PERFORMANCE FIX: Optimize patient-payer processing to avoid O(PxN) complexity
891
- # Instead of nested loops, process each patient once and try payer_ids until success
892
- # TODO: We should be able to determine the correct payer_id for each patient ahead of time
893
- # by looking up their insurance information from the CSV data or crosswalk mapping.
894
- # This would eliminate the need to try multiple payer_ids per patient and make this O(N).
895
- # CLARIFICATION: In production, use the payer_id from the CSV/crosswalk as primary.
896
- # Retain multi-payer probing behind a DEBUG/DIAGNOSTIC feature toggle only.
897
- # Suggested flag: DEBUG_MODE_PAYER_PROBE = False (module-level), default False.
898
- errors = []
899
- validation_reports = []
900
- processed_count = 0
901
- validation_files_created = [] # Track validation files that were actually created
902
1113
 
903
- for dob, member_id in patients:
904
- processed_count += 1
905
- print("Processing patient {}/{}: Member ID {}, DOB {}".format(
906
- processed_count, len(patients), member_id, dob))
907
-
908
- # Try each payer_id for this patient until we get a successful response
909
- patient_processed = False
910
- for payer_id in payer_ids:
911
- try:
912
- # Run with validation enabled only in debug mode
913
- run_validation = DEBUG_MODE
914
- eligibility_data = get_eligibility_info(client, payer_id, provider_last_name, dob, member_id, npi, run_validation=run_validation, is_manual_lookup=False)
915
- if eligibility_data is not None:
916
- display_eligibility_info(eligibility_data, dob, member_id, output_file)
917
- patient_processed = True
918
-
919
- # Track validation file creation in debug mode
920
- if DEBUG_MODE:
921
- validation_file_path = os.path.join(os.getenv('TEMP'), 'validation_report_{}_{}.txt'.format(member_id, dob))
922
- if os.path.exists(validation_file_path):
923
- validation_files_created.append(validation_file_path)
924
- print(" Validation report created: {}".format(os.path.basename(validation_file_path)))
925
-
926
- break # Stop trying other payer_ids for this patient once we get a response
927
- except Exception as e:
928
- # Continue trying other payer_ids
929
- continue
930
-
931
- # If no payer_id worked for this patient, log the error
932
- if not patient_processed:
933
- error_msg = "No successful payer_id found for patient"
934
- 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")
935
1124
 
936
1125
  # Display errors if any
937
1126
  if errors: