medicafe 0.250820.7__tar.gz → 0.250822.1__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.250820.7 → medicafe-0.250822.1}/MediBot/MediBot.py +16 -0
  2. medicafe-0.250822.1/MediBot/MediBot_Charges.py +133 -0
  3. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediBot/MediBot_UI.py +42 -1
  4. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediBot/__init__.py +1 -1
  5. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediBot/update_medicafe.py +66 -24
  6. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediCafe/__init__.py +1 -1
  7. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediCafe/api_core.py +164 -7
  8. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediCafe/graphql_utils.py +66 -1
  9. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediCafe/smart_import.py +2 -1
  10. medicafe-0.250822.1/MediLink/MediLink_Charges.py +517 -0
  11. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/MediLink_Deductible.py +91 -45
  12. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/MediLink_Deductible_Validator.py +40 -3
  13. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/__init__.py +1 -1
  14. {medicafe-0.250820.7/medicafe.egg-info → medicafe-0.250822.1}/PKG-INFO +1 -1
  15. {medicafe-0.250820.7 → medicafe-0.250822.1/medicafe.egg-info}/PKG-INFO +1 -1
  16. {medicafe-0.250820.7 → medicafe-0.250822.1}/medicafe.egg-info/SOURCES.txt +1 -0
  17. {medicafe-0.250820.7 → medicafe-0.250822.1}/setup.py +1 -1
  18. medicafe-0.250820.7/MediBot/MediBot_Charges.py +0 -0
  19. {medicafe-0.250820.7 → medicafe-0.250822.1}/LICENSE +0 -0
  20. {medicafe-0.250820.7 → medicafe-0.250822.1}/MANIFEST.in +0 -0
  21. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediBot/MediBot.bat +0 -0
  22. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediBot/MediBot_Crosswalk_Library.py +0 -0
  23. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediBot/MediBot_Crosswalk_Utils.py +0 -0
  24. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediBot/MediBot_Notepad_Utils.py +0 -0
  25. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediBot/MediBot_Post.py +0 -0
  26. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediBot/MediBot_Preprocessor.py +0 -0
  27. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediBot/MediBot_Preprocessor_lib.py +0 -0
  28. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediBot/MediBot_dataformat_library.py +0 -0
  29. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediBot/MediBot_debug.bat +0 -0
  30. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediBot/MediBot_docx_decoder.py +0 -0
  31. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediBot/MediBot_smart_import.py +0 -0
  32. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediBot/clear_cache.bat +0 -0
  33. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediBot/crash_diagnostic.bat +0 -0
  34. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediBot/f_drive_diagnostic.bat +0 -0
  35. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediBot/full_debug_suite.bat +0 -0
  36. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediBot/get_medicafe_version.py +0 -0
  37. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediBot/process_csvs.bat +0 -0
  38. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediBot/update_json.py +0 -0
  39. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediCafe/MediLink_ConfigLoader.py +0 -0
  40. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediCafe/__main__.py +0 -0
  41. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediCafe/api_core_backup.py +0 -0
  42. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediCafe/api_factory.py +0 -0
  43. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediCafe/api_utils.py +0 -0
  44. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediCafe/core_utils.py +0 -0
  45. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediCafe/logging_config.py +0 -0
  46. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediCafe/logging_demo.py +0 -0
  47. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediCafe/migration_helpers.py +0 -0
  48. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediCafe/submission_index.py +0 -0
  49. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/InsuranceTypeService.py +0 -0
  50. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/MediLink_837p_cob_library.py +0 -0
  51. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/MediLink_837p_encoder.py +0 -0
  52. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/MediLink_837p_encoder_library.py +0 -0
  53. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/MediLink_837p_utilities.py +0 -0
  54. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/MediLink_API_Generator.py +0 -0
  55. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/MediLink_Azure.py +0 -0
  56. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/MediLink_ClaimStatus.py +0 -0
  57. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/MediLink_DataMgmt.py +0 -0
  58. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/MediLink_Decoder.py +0 -0
  59. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/MediLink_Display_Utils.py +0 -0
  60. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/MediLink_Down.py +0 -0
  61. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/MediLink_Gmail.py +0 -0
  62. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/MediLink_Mailer.py +0 -0
  63. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/MediLink_Parser.py +0 -0
  64. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/MediLink_PatientProcessor.py +0 -0
  65. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/MediLink_Scan.py +0 -0
  66. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/MediLink_Scheduler.py +0 -0
  67. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/MediLink_UI.py +0 -0
  68. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/MediLink_Up.py +0 -0
  69. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/MediLink_insurance_utils.py +0 -0
  70. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/MediLink_main.py +0 -0
  71. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/MediLink_smart_import.py +0 -0
  72. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/Soumit_api.py +0 -0
  73. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/gmail_http_utils.py +0 -0
  74. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/gmail_oauth_utils.py +0 -0
  75. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/insurance_type_integration_test.py +0 -0
  76. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/openssl.cnf +0 -0
  77. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/test.py +0 -0
  78. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/test_cob_library.py +0 -0
  79. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/test_timing.py +0 -0
  80. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/test_validation.py +0 -0
  81. {medicafe-0.250820.7 → medicafe-0.250822.1}/MediLink/webapp.html +0 -0
  82. {medicafe-0.250820.7 → medicafe-0.250822.1}/README.md +0 -0
  83. {medicafe-0.250820.7 → medicafe-0.250822.1}/medicafe.egg-info/dependency_links.txt +0 -0
  84. {medicafe-0.250820.7 → medicafe-0.250822.1}/medicafe.egg-info/entry_points.txt +0 -0
  85. {medicafe-0.250820.7 → medicafe-0.250822.1}/medicafe.egg-info/not-zip-safe +0 -0
  86. {medicafe-0.250820.7 → medicafe-0.250822.1}/medicafe.egg-info/requires.txt +0 -0
  87. {medicafe-0.250820.7 → medicafe-0.250822.1}/medicafe.egg-info/top_level.txt +0 -0
  88. {medicafe-0.250820.7 → medicafe-0.250822.1}/setup.cfg +0 -0
@@ -863,6 +863,22 @@ if __name__ == "__main__":
863
863
  if _ac():
864
864
  _ac().set_pause_status(True)
865
865
  _ = manage_script_pause(csv_data, error_message, reverse_mapping)
866
+ # Prototype charges enrichment - optional and safe-failed to avoid disrupting existing workflow
867
+ if e_state.config.get('ENABLE_CHARGES_ENRICHMENT', False):
868
+ try:
869
+ from MediCafe.smart_import import get_components
870
+ MediBot_Charges = get_components('medibot_charges')
871
+
872
+ field_mapping = MediBot_Preprocessor_lib.field_mapping # Ensure defined for enrichment
873
+
874
+ csv_data, field_mapping, reverse_mapping, fixed_values = MediBot_Charges.enrich_with_charges(
875
+ csv_data, field_mapping, reverse_mapping, fixed_values
876
+ )
877
+ except Exception as e:
878
+ MediLink_ConfigLoader.log("Charges enrichment failed (prototype): {}. Continuing without enrichment.".format(e), level="WARNING")
879
+ print("Warning: Charges feature skipped due to error. Proceeding with standard flow.")
880
+ else:
881
+ MediLink_ConfigLoader.log("Charges enrichment disabled in config. Skipping.", level="INFO")
866
882
  data_entry_loop(csv_data, MediBot_Preprocessor_lib.field_mapping, reverse_mapping, fixed_values)
867
883
  cleanup()
868
884
  else:
@@ -0,0 +1,133 @@
1
+ """MediBot_Charges.py
2
+
3
+ Overview
4
+ --------
5
+ This module provides helper functions for MediBot.py to calculate and bundle charges for entry into Medisoft, based on patient data from MediBot (e.g., full patient details plus user-input anesthesia minutes). It focuses on ophthalmology clinics performing cataract surgeries, where patients typically undergo procedures on both eyes as separate events but require price bundling for consistent billing. Charges are generally calculated as piecewise step-functions based on anesthesia minutes (aligned with 837p units/minutes), exhibiting linear mx+b behavior.
6
+
7
+ The module enriches patient data for backend use in MediBot.py, potentially including flags/tags for billing readiness (e.g., indicating prior surgeries, calculated charges, or bundling status). It does not handle UI display directly—that's for MediBot_UI.py or similar. Output feeds into MediLink for generating 837p files; consider enriching data in 837p-compatible format (e.g., Loop syntax for charges/minutes) for easier conversion to human-readable UI. See MediLink_Charges_Integration_Analysis.md for integration details and safety considerations.
8
+
9
+ Key Features
10
+ ------------
11
+ - Supports batch processing of up to 200 patients (typical: ~20), though serial processing is preferred for user input. Add a "save partial" flag to mitigate risks of inconsistent enriched data if batch fails midway.
12
+ - Implements price bundling to balance charges across multiple procedures (e.g., both eyes), ensuring even total costs per eye to meet patient expectations. Bundling is not affected by unmet deductibles (which instead flag claims as Pending for Deductible to delay sending).
13
+ - Flags "bundling_pending" by defaulting to expect a second procedure within 30 days from first date of service (process refund if expired). Speculate partly on diagnosis codes (e.g., ICD-10 H25.xx for bilateral cataracts), but note variability (e.g., glaucoma in one eye or prior surgery elsewhere) makes this imperfect—err on expecting return visits.
14
+ - Uses tiered pricing schedules from MediLink_Charges.py (loaded via configuration or defaults):
15
+ - Private insurance:
16
+ - $450 for 1-15 minutes
17
+ - $540 for 16-34 minutes
18
+ - $580 for 35-59 minutes (maximum allowed duration; cap at 59 if exceeded)
19
+ - Medicare: Placeholder tiers (e.g., $300 for 1-30, $350 for 31-45, $400 for 46-59); override via configuration if different from private.
20
+ - Integrates with historical transaction data (e.g., via MediSoft's MATRAN file, details TBD) for lookups, especially for patients with prior surgeries to support bundling. Never alters MATRAN data—print user notifications for any needed modifications (e.g., "Edit MATRAN for Patient ID: [ID] - Adjust prior charge from [old] to [new] for bundling"). For MediCafe-owned records, adjustments are fine.
21
+
22
+ Limitations and Assumptions
23
+ ---------------------------
24
+ - Bundling for commercial insurance depends on deductibles, claim submission timing (e.g., within 90 days of service), refund timing, and policy conditions. This is not fully specified but aims to minimize patient invoices due to non-payment risks post-surgery. Unmet deductibles flag claims but do not alter bundling.
25
+ - Does not synchronize perfectly with procedure timing (date of service) or insurance events; bundling may fail if conditions like deductibles are unmet. Over-flagging "bundling_pending" could lead to unnecessary prep—monitor via logs.
26
+ - Assumes serial processing: User inputs minutes per patient, module enriches data, repeat. Batch processing is inefficient without all data (e.g., minutes) available upfront. If batch fails midway, use "save partial" flag for recovery; no automatic rollback.
27
+ - Medicare handling uses placeholders; assumes similarity to private insurance unless configured otherwise.
28
+ - For missing historical data: Notify user (print, similar to MAINS failures; minimal logging to avoid DEBUG/INFO clutter [[memory:4184036]]), use any existing first/second visit data; prompt manually if none available (e.g., display table-like line from enhanced patient table, user fills 'minutes' column, line progresses with added charges and pre-loaded deductible data if available).
29
+ - For durations >59 minutes: Cap at 59, log minimal INFO warning (no HIPAA data), and print notify for user confirmation (rare case, likely typo).
30
+ - No direct risk to existing functionality, but bundling logic could affect billing accuracy if assumptions about insurance conditions are incorrect (e.g., over-bundling risks claim rejections; user errors in manual MATRAN edits could break audits if notifications aren't precise).
31
+
32
+ Usage
33
+ -----
34
+ Intended as a data enricher for MediBot.py to automate Medisoft charge entry. Example flow:
35
+ 1. Receive patient data from MediBot.py.
36
+ 2. User inputs anesthesia minutes (possibly via enhanced patient table in MediBot_UI.py; extend display_enhanced_patient_table() with serial pauses/prompts).
37
+ 3. Enrich data with calculated/bundled charges, flags (e.g., bundling_pending, Pending for Deductible), and 837p-compatible fields.
38
+ 4. Pass enriched data back for MediSoft entry or 837p generation. For prior billed charges, lookup without alteration; notify user for manual MATRAN edits if needed.
39
+
40
+ Integration Notes
41
+ -----------------
42
+ - Integrate with MediBot_UI.py for displaying an enhanced patient table, user input for minutes (table-like prompts where user fills 'minutes' column, line progresses), and historical lookups (e.g., for prior charges in bundling). Handle UI pauses serially; use "save partial" flag on errors.
43
+ - Run serially per patient for real-time enrichment during input.
44
+ - Future: Specify how to display bundled charges in UI and handle MATRAN file parsing for transactions (read-only).
45
+
46
+ Compatibility
47
+ -------------
48
+ Always ensure XP SP3 + Python 3.4.4 + ASCII-only environment compatibility. Avoid f-strings and include inline commentary where assumptions may differ (e.g., Medicare handling).
49
+
50
+ Note: This module is a helper; place any additional helper functions in a separate .py file per preferences.
51
+ """
52
+
53
+ from MediCafe.smart_import import get_components
54
+ from MediCafe.core_utils import get_config_loader_with_fallback
55
+
56
+ # Import MediLink_Charges via smart_import (ensures no circular imports)
57
+ MediLink_Charges = get_components('medilink_charges')
58
+ MediLink_ConfigLoader = get_config_loader_with_fallback() # Using centralized config loader for consistency with MediBot.py
59
+
60
+ # Prototype function to enrich data with charges (called from MediBot.py TODO)
61
+ def enrich_with_charges(csv_data, field_mapping, reverse_mapping, fixed_values):
62
+ # Inline commentary: This function enriches csv_data with charges using MediLink_Charges.
63
+ # Assumes serial processing; add "save partial" flag for batch recovery.
64
+ # No alterations to MATRAN - read-only lookups with user notifications.
65
+ # XP/Python 3.4.4 compatible: No f-strings, ASCII-only.
66
+
67
+ enriched_data = []
68
+ for row in csv_data:
69
+ # Get anesthesia minutes (prototype: assume from user input via UI prompt)
70
+ minutes = get_anesthesia_minutes(row, reverse_mapping) # Placeholder call to UI prompt
71
+
72
+ # Cap minutes at 59
73
+ if minutes > 59:
74
+ minutes = 59
75
+ print("Capped duration to 59 minutes for patient ID: {}".format(row.get(reverse_mapping.get('Patient ID #2', ''), 'Unknown')))
76
+ # Minimal log (no HIPAA data)
77
+ if MediLink_ConfigLoader:
78
+ MediLink_ConfigLoader.log("Capped duration to 59 minutes", level="INFO")
79
+
80
+ # Determine insurance type (prototype: from row)
81
+ insurance_type = 'medicare' if 'MEDICARE' in row.get(reverse_mapping.get('Primary Insurance', ''), '').upper() else 'private'
82
+
83
+ # Calculate charge using MediLink_Charges
84
+ charge_info = MediLink_Charges.calculate_procedure_charge(
85
+ minutes=minutes,
86
+ insurance_type=insurance_type,
87
+ procedure_code=row.get(reverse_mapping.get('Procedure Code', ''), '66984'), # Default cataract code
88
+ service_date=datetime.strptime(row.get('Surgery Date', ''), '%m-%d-%Y'),
89
+ patient_id=row.get(reverse_mapping.get('Patient ID #2', ''), '')
90
+ )
91
+
92
+ # Bundling and flagging (prototype: check for multi-eye)
93
+ prior_charges = lookup_historical_charges(charge_info.patient_id) # Read-only MATRAN lookup
94
+ if prior_charges:
95
+ bundled = MediLink_Charges.bundle_bilateral_charges([charge_info] + prior_charges)
96
+ # Notify user for MATRAN edits if needed (no alterations)
97
+ print("Edit MATRAN for Patient ID: {} - Adjust prior charge from {} to {} for bundling".format(
98
+ charge_info.patient_id, prior_charges[0].base_charge, bundled[0].adjusted_charge))
99
+ else:
100
+ # Flag bundling_pending: Expect second within 30 days, based on diagnosis
101
+ diagnosis = row.get(reverse_mapping.get('Diagnosis Code', ''), '')
102
+ if 'H25' in diagnosis.upper(): # Speculative bilateral check
103
+ charge_info.bundling_group = 'bundling_pending' # Refund if expires after 30 days
104
+
105
+ # Enrich row (prototype: add fields)
106
+ row['Calculated Charge'] = str(charge_info.adjusted_charge)
107
+ row['Minutes'] = charge_info.minutes
108
+ # Add flags (e.g., Pending for Deductible - prototype check)
109
+ if check_deductible_unmet(row): # Placeholder
110
+ row['Status'] = 'Pending for Deductible'
111
+
112
+ enriched_data.append(row)
113
+
114
+ return enriched_data, field_mapping, reverse_mapping, fixed_values # As per TODO
115
+
116
+ # Prototype helpers (add more as needed)
117
+ def get_anesthesia_minutes(row, reverse_mapping):
118
+ # Inline: Use interactive table from MediBot_UI for input.
119
+ from MediBot_UI import display_enhanced_patient_table
120
+ # Wrap single row as list (prototype)
121
+ patient_info = [(row.get('Surgery Date', ''), row.get(reverse_mapping.get('Patient Name', ''), 'Unknown'),
122
+ row.get(reverse_mapping.get('Patient ID #2', ''), ''), row.get('Diagnosis Code', ''), row)]
123
+ enriched_info = display_enhanced_patient_table(patient_info, "Enter Minutes for Patient", interactive=True)
124
+ return enriched_info[0][4]['Minutes'] # Extract from enriched dict in tuple
125
+
126
+ def lookup_historical_charges(patient_id):
127
+ # Inline: Read-only MATRAN lookup (details TBD). Never alter.
128
+ # If missing, prompt user via table-like UI.
129
+ return [] # Prototype: Return list of prior ChargeInfo
130
+
131
+ def check_deductible_unmet(row):
132
+ # Inline: Prototype check; doesn't affect bundling.
133
+ return True # Placeholder
@@ -28,7 +28,7 @@ except ImportError:
28
28
  from MediCafe.core_utils import create_config_cache
29
29
  _get_config, (_config_cache, _crosswalk_cache) = create_config_cache()
30
30
 
31
- def display_enhanced_patient_table(patient_info, title, show_line_numbers=True):
31
+ def display_enhanced_patient_table(patient_info, title, show_line_numbers=True, interactive=False):
32
32
  """
33
33
  Display an enhanced patient table with multiple surgery dates and diagnosis codes.
34
34
 
@@ -189,6 +189,47 @@ def display_enhanced_patient_table(patient_info, title, show_line_numbers=True):
189
189
  primary_format = " {:<6} | {:<" + str(max_patient_id_len) + "} | {:<" + str(max_patient_name_len) + "} | {:<" + str(max_diagnosis_len) + "}"
190
190
  print(primary_format.format(formatted_date, patient_id, patient_name, display_diagnosis))
191
191
 
192
+ if interactive:
193
+ for i in range(len(patient_info)):
194
+ # Redraw table up to current row (prototype: simple print)
195
+ print("\n" + title)
196
+ print("|" + "-" * 80 + "|") # ASCII table border
197
+ print("| Date | Name | ID | Diagnosis | Minutes | Deductible | Charge | Flags |")
198
+ print("|" + "-" * 80 + "|")
199
+
200
+ # Print previous rows (enriched)
201
+ for j in range(i):
202
+ r = patient_info[j]
203
+ print("| {} | {} | {} | {} | {} | {} | {} | {} |".format(
204
+ r[0], r[1][:10], r[2], r[3], r.get('Minutes', ''), r.get('Deductible', 'N/A'), r.get('Charge', ''), r.get('Flags', '')))
205
+
206
+ # Prompt for current row
207
+ current = patient_info[i]
208
+ print("| {} | {} | {} | {} | [Input] | {} | | |".format(
209
+ current[0], current[1][:10], current[2], current[3], current.get('Deductible', 'N/A'))) # Highlight Minutes column
210
+
211
+ while True:
212
+ try:
213
+ minutes_str = raw_input("Enter Minutes (1-59): ")
214
+ minutes = int(minutes_str)
215
+ if minutes > 59:
216
+ minutes = 59
217
+ confirm = raw_input("Capped to 59. Proceed? (Y/N): ").upper()
218
+ if confirm != 'Y': continue
219
+ if 1 <= minutes <= 59: break
220
+ print("Invalid: 1-59 only.")
221
+ except ValueError:
222
+ print("Invalid: Enter number.")
223
+
224
+ # Enrich and update row
225
+ enriched_row = enrich_single_row(current, minutes, patient_info[i][4]) # Pass original row dict
226
+ patient_info[i] = (enriched_row['Surgery Date'], enriched_row['Patient Name'], enriched_row['Patient ID #2'], enriched_row['Diagnosis Code'], enriched_row) # Update tuple
227
+
228
+ # Redraw full table after update
229
+ # (Repeat print logic above with all rows up to i)
230
+
231
+ return patient_info # Enriched
232
+
192
233
  # Function to check if a specific key is pressed
193
234
  def _get_vk_codes():
194
235
  """Get VK codes from config."""
@@ -19,7 +19,7 @@ Smart Import Integration:
19
19
  medibot_main = get_components('medibot_main')
20
20
  """
21
21
 
22
- __version__ = "0.250820.7"
22
+ __version__ = "0.250822.1"
23
23
  __author__ = "Daniel Vidaud"
24
24
  __email__ = "daniel@personalizedtransformation.com"
25
25
 
@@ -268,9 +268,17 @@ def verify_post_install(package, expected_version):
268
268
 
269
269
 
270
270
  def upgrade_package(package):
271
- # Try with [binary] first; if pip doesn't recognize it, fall back to plain package
272
- strategies = [
271
+ # Default to using existing system packages: skip dependencies first
272
+ # Light strategies: --no-deps to avoid heavy reinstall of dependencies
273
+ light_strategies = [
274
+ ['install', '--upgrade', '--no-deps', package + '[binary]', '--no-cache-dir', '--disable-pip-version-check'],
275
+ ['install', '--upgrade', '--no-deps', package, '--no-cache-dir', '--disable-pip-version-check']
276
+ ]
277
+
278
+ # Heavy strategies: allow dependencies as a last resort
279
+ heavy_strategies = [
273
280
  ['install', '--upgrade', package + '[binary]', '--no-cache-dir', '--disable-pip-version-check'],
281
+ ['install', '--upgrade', package, '--no-cache-dir', '--disable-pip-version-check'],
274
282
  ['install', '--upgrade', '--force-reinstall', package + '[binary]', '--no-cache-dir', '--disable-pip-version-check'],
275
283
  ['install', '--upgrade', '--force-reinstall', '--ignore-installed', '--user', package + '[binary]', '--no-cache-dir', '--disable-pip-version-check']
276
284
  ]
@@ -279,9 +287,11 @@ def upgrade_package(package):
279
287
  if not latest_before:
280
288
  print_status('WARNING', 'Unable to determine latest version from PyPI; proceeding with blind install')
281
289
 
282
- for idx, parts in enumerate(strategies):
290
+ print_status('INFO', 'Using existing system packages when possible (skipping dependencies)')
291
+
292
+ for idx, parts in enumerate(light_strategies):
283
293
  attempt = idx + 1
284
- print_section("Attempt {}/3".format(attempt))
294
+ print_section("Light attempt {}".format(attempt))
285
295
  cmd = [sys.executable, '-m', 'pip'] + parts
286
296
  print_status('INFO', 'Running: {} -m pip {}'.format(sys.executable, ' '.join(parts)))
287
297
  code, out, err = run_pip_install(cmd)
@@ -305,26 +315,58 @@ def upgrade_package(package):
305
315
  print(tail)
306
316
  except Exception:
307
317
  pass
308
- # Detect extras error and fall back to non-binary package
309
- if 'Invalid requirement' in err or 'extras' in err or '[binary]' in err:
310
- print_status('INFO', 'Falling back to installing without [binary] extra')
311
- fb_cmd = [sys.executable, '-m', 'pip', 'install', '--upgrade', package, '--no-cache-dir', '--disable-pip-version-check']
312
- code2, out2, err2 = run_pip_install(fb_cmd)
313
- if code2 == 0:
314
- ok, installed = verify_post_install(package, latest_before)
315
- if ok:
316
- print_status('SUCCESS', 'Installed without [binary]: {}'.format(installed))
317
- return True
318
- if err2:
319
- print(err2.strip())
320
- elif out2:
321
- try:
322
- lines2 = out2.strip().splitlines()
323
- tail2 = '\n'.join(lines2[-15:])
324
- if tail2:
325
- print(tail2)
326
- except Exception:
327
- pass
318
+ print_status('WARNING', 'pip returned non-zero exit code ({})'.format(code))
319
+
320
+ # If we reached here, light attempts did not succeed conclusively
321
+ # Check command-line flags for non-interactive approval
322
+ auto_yes = False
323
+ for arg in sys.argv[1:]:
324
+ if arg.strip().lower() in ('--aggressive', '--yes-deps', '--full-deps'):
325
+ auto_yes = True
326
+ break
327
+
328
+ proceed_heavy = auto_yes
329
+ if not proceed_heavy:
330
+ print_section('Confirmation required')
331
+ print_status('INFO', 'Light update did not complete. A full dependency reinstall may take a long time.')
332
+ print_status('INFO', 'Proceeding will uninstall/reinstall related packages as needed (e.g., requests, lxml, msal).')
333
+ try:
334
+ answer = input('Proceed with full dependency update? (y/N): ').strip().lower()
335
+ except Exception:
336
+ answer = ''
337
+ proceed_heavy = (answer == 'y' or answer == 'yes')
338
+
339
+ if not proceed_heavy:
340
+ print_status('INFO', 'User declined full dependency update. Keeping existing dependencies.')
341
+ return False
342
+
343
+ print_section('Full dependency update')
344
+ print_status('INFO', 'Running heavy update with dependencies (this may take several minutes)')
345
+
346
+ for idx, parts in enumerate(heavy_strategies):
347
+ attempt = idx + 1
348
+ print_section('Heavy attempt {}'.format(attempt))
349
+ cmd = [sys.executable, '-m', 'pip'] + parts
350
+ print_status('INFO', 'Running: {} -m pip {}'.format(sys.executable, ' '.join(parts)))
351
+ code, out, err = run_pip_install(cmd)
352
+ if code == 0:
353
+ ok, installed = verify_post_install(package, latest_before)
354
+ if ok:
355
+ print_status('SUCCESS', 'Installed version: {}'.format(installed))
356
+ return True
357
+ else:
358
+ print_status('WARNING', 'Install returned success but version not updated yet{}'.format(
359
+ '' if not installed else ' (detected {})'.format(installed)))
360
+ else:
361
+ combined = err or out
362
+ if combined:
363
+ try:
364
+ lines3 = combined.strip().splitlines()
365
+ tail3 = '\n'.join(lines3[-15:])
366
+ if tail3:
367
+ print(tail3)
368
+ except Exception:
369
+ pass
328
370
  print_status('WARNING', 'pip returned non-zero exit code ({})'.format(code))
329
371
 
330
372
  return False
@@ -27,7 +27,7 @@ Smart Import System:
27
27
  api_suite = get_api_access()
28
28
  """
29
29
 
30
- __version__ = "0.250820.7"
30
+ __version__ = "0.250822.1"
31
31
  __author__ = "Daniel Vidaud"
32
32
  __email__ = "daniel@personalizedtransformation.com"
33
33
 
@@ -861,8 +861,50 @@ def get_eligibility_super_connector(client, payer_id, provider_last_name, search
861
861
  if payer_id not in valid_payer_ids:
862
862
  raise ValueError("Invalid payer_id: {}. Must be one of: {}".format(payer_id, ", ".join(valid_payer_ids)))
863
863
 
864
- endpoint_name = 'UHCAPI'
865
- url_extension = client.config['MediLink_Config']['endpoints'][endpoint_name]['additional_endpoints']['eligibility_super_connector']
864
+ # Prefer OPTUMAI endpoint if configured, otherwise fall back to legacy UHCAPI super connector
865
+ try:
866
+ endpoints_cfg = client.config['MediLink_Config']['endpoints']
867
+ except Exception:
868
+ endpoints_cfg = {}
869
+
870
+ endpoint_name = None
871
+ url_extension = None
872
+
873
+ try:
874
+ optumai_cfg = endpoints_cfg.get('OPTUMAI', {})
875
+ optumai_additional = optumai_cfg.get('additional_endpoints', {}) if isinstance(optumai_cfg, dict) else {}
876
+ optumai_url = optumai_additional.get('eligibility_optumai')
877
+ if optumai_cfg and optumai_cfg.get('api_url') and optumai_url:
878
+ endpoint_name = 'OPTUMAI'
879
+ url_extension = optumai_url
880
+ except Exception:
881
+ # Safe ignore; will fall back below
882
+ pass
883
+
884
+ if not endpoint_name:
885
+ # Fallback to legacy UHCAPI super connector path for backward compatibility
886
+ endpoint_name = 'UHCAPI'
887
+ try:
888
+ url_extension = endpoints_cfg.get(endpoint_name, {}).get('additional_endpoints', {}).get('eligibility_super_connector')
889
+ except Exception:
890
+ url_extension = None
891
+
892
+ if not url_extension:
893
+ raise ValueError("Eligibility endpoint not configured for {}".format(endpoint_name))
894
+
895
+ # Debug/trace: indicate which endpoint/path is being used for the Super Connector call
896
+ try:
897
+ MediLink_ConfigLoader.log(
898
+ "Super Connector eligibility using endpoint '{}' with path '{}'".format(endpoint_name, url_extension),
899
+ level="INFO",
900
+ console_output=CONSOLE_LOGGING
901
+ )
902
+ except Exception:
903
+ pass
904
+ try:
905
+ print("[Eligibility] Using endpoint: {} (path: {})".format(endpoint_name, url_extension))
906
+ except Exception:
907
+ pass
866
908
 
867
909
  # Get provider TIN from config (using existing billing_provider_tin)
868
910
  from MediCafe.core_utils import extract_medilink_config
@@ -870,6 +912,19 @@ def get_eligibility_super_connector(client, payer_id, provider_last_name, search
870
912
  provider_tin = medi.get('billing_provider_tin')
871
913
  if not provider_tin:
872
914
  raise ValueError("Provider TIN not found in configuration")
915
+ # Normalize provider TIN to 9-digit numeric string
916
+ try:
917
+ provider_tin_str = ''.join([ch for ch in str(provider_tin) if ch.isdigit()])
918
+ if len(provider_tin_str) == 9:
919
+ provider_tin = provider_tin_str
920
+ else:
921
+ MediLink_ConfigLoader.log(
922
+ "Warning: Provider TIN '{}' is not 9 digits after normalization".format(provider_tin),
923
+ level="WARNING",
924
+ console_output=CONSOLE_LOGGING
925
+ )
926
+ except Exception:
927
+ pass
873
928
 
874
929
  # Construct GraphQL query variables using the consolidated module
875
930
  graphql_variables = MediLink_GraphQL.build_eligibility_variables(
@@ -896,8 +951,17 @@ def get_eligibility_super_connector(client, payer_id, provider_last_name, search
896
951
  MediLink_ConfigLoader.log("Using SAMPLE DATA from swagger documentation", level="INFO")
897
952
  else:
898
953
  # Build GraphQL request with actual data using consolidated module
899
- graphql_body = MediLink_GraphQL.build_eligibility_request(graphql_variables)
900
- MediLink_ConfigLoader.log("Using CONSTRUCTED DATA with consolidated GraphQL module", level="INFO")
954
+ # OPTUMAI now uses an enriched query aligned to production schema
955
+ try:
956
+ if endpoint_name == 'OPTUMAI' and hasattr(MediLink_GraphQL, 'build_optumai_enriched_request'):
957
+ graphql_body = MediLink_GraphQL.build_optumai_enriched_request(graphql_variables)
958
+ MediLink_ConfigLoader.log("Using OPTUMAI ENRICHED GraphQL request", level="INFO")
959
+ else:
960
+ graphql_body = MediLink_GraphQL.build_eligibility_request(graphql_variables)
961
+ MediLink_ConfigLoader.log("Using CONSTRUCTED DATA with consolidated GraphQL module", level="INFO")
962
+ except Exception:
963
+ graphql_body = MediLink_GraphQL.build_eligibility_request(graphql_variables)
964
+ MediLink_ConfigLoader.log("Fallback to standard GraphQL request body", level="WARNING")
901
965
 
902
966
  # Compare with sample data for debugging
903
967
  sample_data = MediLink_GraphQL.get_sample_eligibility_request()
@@ -924,8 +988,27 @@ def get_eligibility_super_connector(client, payer_id, provider_last_name, search
924
988
  headers = {
925
989
  'Content-Type': 'application/json',
926
990
  'Accept': 'application/json',
927
- 'tin': str(provider_tin) # Ensure TIN is a string
991
+ 'tin': str(provider_tin) # Ensure TIN is a string (used for legacy UHC super connector)
928
992
  }
993
+
994
+ # OPTUMAI requires 'providertaxid' header (mapped from billing_provider_tin)
995
+ try:
996
+ if endpoint_name == 'OPTUMAI' and provider_tin:
997
+ # OPTUMAI expects providerTaxId; remove legacy 'tin' header to avoid confusion
998
+ if 'tin' in headers:
999
+ try:
1000
+ del headers['tin']
1001
+ except Exception:
1002
+ pass
1003
+ headers['providerTaxId'] = str(provider_tin)
1004
+ # Add trace header for observability (optional per spec)
1005
+ try:
1006
+ corr_id = 'mc-{}'.format(int(time.time() * 1000))
1007
+ except Exception:
1008
+ corr_id = 'mc-{}'.format(int(time.time()))
1009
+ headers['x-optum-consumer-correlation-id'] = corr_id
1010
+ except Exception:
1011
+ pass
929
1012
 
930
1013
  # Only add env header when using sample data
931
1014
  if USE_SAMPLE_DATA:
@@ -937,12 +1020,86 @@ def get_eligibility_super_connector(client, payer_id, provider_last_name, search
937
1020
  # Log the final headers being sent
938
1021
  MediLink_ConfigLoader.log("Final headers being sent: {}".format(json.dumps(headers, indent=2)), level="DEBUG")
939
1022
 
940
- # Make the GraphQL API call
941
- response = client.make_api_call(endpoint_name, 'POST', url_extension, params=None, data=graphql_body, headers=headers)
1023
+ # Make the GraphQL API call with enhanced error diagnostics for endpoint failures
1024
+ try:
1025
+ response = client.make_api_call(endpoint_name, 'POST', url_extension, params=None, data=graphql_body, headers=headers)
1026
+ except Exception as e:
1027
+ # Best-effort diagnostics without exposing secrets or PHI
1028
+ try:
1029
+ status = getattr(getattr(e, 'response', None), 'status_code', None)
1030
+ diag = "Eligibility request to {}{} failed".format(
1031
+ endpoint_name and (endpoint_name + " ") or "", url_extension)
1032
+ if status is not None:
1033
+ diag += " with status {}".format(status)
1034
+ MediLink_ConfigLoader.log(diag, level="ERROR", console_output=CONSOLE_LOGGING)
1035
+ try:
1036
+ print("[Eligibility] Request failed (status: {}). See logs for details.".format(status))
1037
+ except Exception:
1038
+ pass
1039
+ except Exception:
1040
+ pass
1041
+
1042
+ # Progressive hardening: allow disabling fallback to UHC via config
1043
+ try:
1044
+ disable_fallback = False
1045
+ try:
1046
+ disable_fallback = bool(medi.get('optumai_disable_fallback', False))
1047
+ except Exception:
1048
+ disable_fallback = False
1049
+ if not disable_fallback and endpoint_name == 'OPTUMAI':
1050
+ fallback_url = endpoints_cfg.get('UHCAPI', {}).get('additional_endpoints', {}).get('eligibility_super_connector')
1051
+ if fallback_url:
1052
+ try:
1053
+ MediLink_ConfigLoader.log(
1054
+ "OPTUMAI call failed. Falling back to UHCAPI path '{}'".format(fallback_url),
1055
+ level="WARNING",
1056
+ console_output=CONSOLE_LOGGING
1057
+ )
1058
+ except Exception:
1059
+ pass
1060
+ try:
1061
+ print("[Eligibility] Fallback to UHCAPI (path: {})".format(fallback_url))
1062
+ except Exception:
1063
+ pass
1064
+ response = client.make_api_call('UHCAPI', 'POST', fallback_url, params=None, data=graphql_body, headers=headers)
1065
+ else:
1066
+ raise
1067
+ else:
1068
+ raise
1069
+ except Exception:
1070
+ # Propagate original error if fallback not possible or also fails
1071
+ raise
942
1072
 
943
1073
  # Transform GraphQL response to match REST API format
944
1074
  # This ensures the calling code doesn't know the difference
945
1075
  transformed_response = MediLink_GraphQL.transform_eligibility_response(response)
1076
+
1077
+ # Post-transform sanity: if non-200, emit brief diagnostics to aid validation sessions
1078
+ try:
1079
+ sc_status = transformed_response.get('statuscode') if isinstance(transformed_response, dict) else None
1080
+ if sc_status and sc_status != '200':
1081
+ msg = transformed_response.get('message')
1082
+ MediLink_ConfigLoader.log(
1083
+ "Super Connector transformed response status: {} msg: {}".format(sc_status, msg),
1084
+ level="INFO",
1085
+ console_output=CONSOLE_LOGGING
1086
+ )
1087
+ raw_errs = None
1088
+ try:
1089
+ raw = transformed_response.get('rawGraphQLResponse', {})
1090
+ raw_errs = raw.get('errors')
1091
+ except Exception:
1092
+ raw_errs = None
1093
+ if raw_errs:
1094
+ try:
1095
+ first_err = raw_errs[0]
1096
+ code = first_err.get('code') or first_err.get('extensions', {}).get('code')
1097
+ desc = first_err.get('description') or first_err.get('message')
1098
+ print("[Eligibility] GraphQL error code={} desc={}".format(code, desc))
1099
+ except Exception:
1100
+ pass
1101
+ except Exception:
1102
+ pass
946
1103
 
947
1104
  return transformed_response
948
1105
 
@@ -7,7 +7,17 @@ Handles query templates, query building, and response transformations
7
7
  import json
8
8
 
9
9
  class GraphQLQueryBuilder:
10
- """Builder class for constructing GraphQL queries for Super Connector API"""
10
+ """Builder class for constructing GraphQL queries for Super Connector API
11
+
12
+ Note on ID card images (idCardImages):
13
+ - Intentionally excluded from all active queries to avoid fetching large
14
+ image payloads and to reduce PHI handling risk in standard flows.
15
+ - If a future feature requires downloading insurance ID cards, add this
16
+ selection to the desired query:
17
+ idCardImages { side content contentType }
18
+ and ensure the implementation does NOT log or persist raw image content.
19
+ Gate any new behavior behind an explicit, opt-in feature flag.
20
+ """
11
21
 
12
22
  @staticmethod
13
23
  def get_eligibility_query():
@@ -161,6 +171,53 @@ class GraphQLQueryBuilder:
161
171
  }
162
172
  }
163
173
 
174
+ # ------------------------------------------------------------------
175
+ # OPTUMAI compatibility: minimal query that avoids fields not present
176
+ # ------------------------------------------------------------------
177
+ @staticmethod
178
+ def get_optumai_minimal_query():
179
+ """
180
+ Returns a minimal GraphQL query for OPTUMAI endpoint that avoids
181
+ fields reported as undefined by the schema.
182
+ """
183
+ return """query Query($input: EligibilityInput!) {\r\n checkEligibility(input: $input) {\r\n eligibility {\r\n eligibilityInfo {\r\n trnId\r\n member {\r\n memberId\r\n firstName\r\n lastName\r\n middleName\r\n dateOfBirth\r\n gender\r\n relationshipCode\r\n dependentSequenceNumber\r\n individualRelationship { code description }\r\n relationshipType { code description }\r\n }\r\n insuranceInfo {\r\n policyStatus\r\n planTypeDescription\r\n payerId\r\n }\r\n planLevels { level }\r\n }\r\n }\r\n }\r\n}"""
184
+
185
+ @staticmethod
186
+ def build_optumai_minimal_request(variables):
187
+ """
188
+ Builds the minimal GraphQL request body for OPTUMAI.
189
+ """
190
+ return {
191
+ "query": GraphQLQueryBuilder.get_optumai_minimal_query(),
192
+ "variables": {
193
+ "input": variables
194
+ }
195
+ }
196
+
197
+ # ------------------------------------------------------------------
198
+ # OPTUMAI compatibility: enriched query with commonly used fields
199
+ # ------------------------------------------------------------------
200
+ @staticmethod
201
+ def get_optumai_enriched_query():
202
+ """
203
+ Returns an enriched GraphQL query for OPTUMAI that includes
204
+ eligibilityInfo core fields plus planLevels details that our
205
+ downstream logic commonly uses. Avoids legacy-only fields.
206
+ """
207
+ return """query Query($input: EligibilityInput!) {\r\n checkEligibility(input: $input) {\r\n eligibility {\r\n eligibilityInfo {\r\n trnId\r\n member {\r\n memberId\r\n firstName\r\n lastName\r\n middleName\r\n dateOfBirth\r\n gender\r\n relationshipCode\r\n dependentSequenceNumber\r\n individualRelationship { code description }\r\n relationshipType { code description }\r\n }\r\n insuranceInfo {\r\n policyNumber\r\n eligibilityStartDate\r\n eligibilityEndDate\r\n planStartDate\r\n planEndDate\r\n policyStatus\r\n planTypeDescription\r\n payerId\r\n lineOfBusinessCode\r\n coverageType\r\n insuranceTypeCode\r\n insuranceType\r\n stateOfIssueCode\r\n productType\r\n productId\r\n productCode\r\n }\r\n associatedIds {\r\n alternateId\r\n medicaidRecipientId\r\n exchangeMemberId\r\n alternateSubscriberId\r\n hicNumber\r\n mbiNumber\r\n subscriberMemberFacingIdentifier\r\n survivingSpouseId\r\n subscriberId\r\n memberReplacementId\r\n legacyMemberId\r\n healthInsuranceExchangeId\r\n }\r\n planLevels {\r\n level\r\n family {\r\n networkStatus\r\n planAmount\r\n planAmountFrequency\r\n remainingAmount\r\n }\r\n individual {\r\n networkStatus\r\n planAmount\r\n planAmountFrequency\r\n remainingAmount\r\n }\r\n }\r\n }\r\n providerNetwork { status tier }\r\n }\r\n }\r\n}"""
208
+
209
+ @staticmethod
210
+ def build_optumai_enriched_request(variables):
211
+ """
212
+ Builds the enriched GraphQL request body for OPTUMAI.
213
+ """
214
+ return {
215
+ "query": GraphQLQueryBuilder.get_optumai_enriched_query(),
216
+ "variables": {
217
+ "input": variables
218
+ }
219
+ }
220
+
164
221
  class GraphQLResponseTransformer:
165
222
  """Transforms GraphQL responses to match REST API format"""
166
223
 
@@ -437,6 +494,14 @@ def build_eligibility_request(variables):
437
494
  """Build complete GraphQL request body with working format"""
438
495
  return GraphQLQueryBuilder.build_eligibility_request(variables)
439
496
 
497
+ def build_optumai_minimal_request(variables):
498
+ """Build minimal GraphQL request body for OPTUMAI"""
499
+ return GraphQLQueryBuilder.build_optumai_minimal_request(variables)
500
+
501
+ def build_optumai_enriched_request(variables):
502
+ """Build enriched GraphQL request body for OPTUMAI"""
503
+ return GraphQLQueryBuilder.build_optumai_enriched_request(variables)
504
+
440
505
  def transform_eligibility_response(graphql_response):
441
506
  """Transform GraphQL eligibility response to REST format"""
442
507
  return GraphQLResponseTransformer.transform_eligibility_response(graphql_response)