medicafe 0.250912.0__tar.gz → 0.250930.0__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 (84) hide show
  1. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediBot/__init__.py +1 -1
  2. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediCafe/MediLink_ConfigLoader.py +7 -0
  3. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediCafe/__init__.py +1 -1
  4. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediCafe/api_core.py +151 -10
  5. medicafe-0.250930.0/MediCafe/error_reporter.py +287 -0
  6. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/MediLink_837p_encoder_library.py +18 -15
  7. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/MediLink_Deductible.py +47 -3
  8. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/MediLink_Display_Utils.py +37 -1
  9. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/MediLink_main.py +57 -0
  10. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/__init__.py +1 -1
  11. {medicafe-0.250912.0/medicafe.egg-info → medicafe-0.250930.0}/PKG-INFO +1 -1
  12. {medicafe-0.250912.0 → medicafe-0.250930.0/medicafe.egg-info}/PKG-INFO +1 -1
  13. {medicafe-0.250912.0 → medicafe-0.250930.0}/medicafe.egg-info/SOURCES.txt +1 -0
  14. {medicafe-0.250912.0 → medicafe-0.250930.0}/setup.py +1 -1
  15. {medicafe-0.250912.0 → medicafe-0.250930.0}/LICENSE +0 -0
  16. {medicafe-0.250912.0 → medicafe-0.250930.0}/MANIFEST.in +0 -0
  17. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediBot/MediBot.bat +0 -0
  18. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediBot/MediBot.py +0 -0
  19. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediBot/MediBot_Charges.py +0 -0
  20. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediBot/MediBot_Crosswalk_Library.py +0 -0
  21. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediBot/MediBot_Crosswalk_Utils.py +0 -0
  22. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediBot/MediBot_Notepad_Utils.py +0 -0
  23. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediBot/MediBot_Post.py +0 -0
  24. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediBot/MediBot_Preprocessor.py +0 -0
  25. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediBot/MediBot_Preprocessor_lib.py +0 -0
  26. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediBot/MediBot_UI.py +0 -0
  27. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediBot/MediBot_dataformat_library.py +0 -0
  28. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediBot/MediBot_debug.bat +0 -0
  29. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediBot/MediBot_docx_decoder.py +0 -0
  30. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediBot/MediBot_smart_import.py +0 -0
  31. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediBot/clear_cache.bat +0 -0
  32. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediBot/crash_diagnostic.bat +0 -0
  33. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediBot/f_drive_diagnostic.bat +0 -0
  34. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediBot/full_debug_suite.bat +0 -0
  35. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediBot/get_medicafe_version.py +0 -0
  36. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediBot/process_csvs.bat +0 -0
  37. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediBot/update_json.py +0 -0
  38. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediBot/update_medicafe.py +0 -0
  39. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediCafe/__main__.py +0 -0
  40. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediCafe/api_factory.py +0 -0
  41. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediCafe/api_utils.py +0 -0
  42. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediCafe/core_utils.py +0 -0
  43. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediCafe/deductible_utils.py +0 -0
  44. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediCafe/graphql_utils.py +0 -0
  45. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediCafe/logging_config.py +0 -0
  46. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediCafe/logging_demo.py +0 -0
  47. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediCafe/migration_helpers.py +0 -0
  48. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediCafe/smart_import.py +0 -0
  49. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediCafe/submission_index.py +0 -0
  50. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/InsuranceTypeService.py +0 -0
  51. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/MediLink_837p_cob_library.py +0 -0
  52. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/MediLink_837p_encoder.py +0 -0
  53. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/MediLink_837p_utilities.py +0 -0
  54. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/MediLink_API_Generator.py +0 -0
  55. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/MediLink_Azure.py +0 -0
  56. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/MediLink_Charges.py +0 -0
  57. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/MediLink_ClaimStatus.py +0 -0
  58. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/MediLink_DataMgmt.py +0 -0
  59. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/MediLink_Decoder.py +0 -0
  60. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/MediLink_Deductible_Validator.py +0 -0
  61. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/MediLink_Down.py +0 -0
  62. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/MediLink_Gmail.py +0 -0
  63. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/MediLink_Mailer.py +0 -0
  64. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/MediLink_Parser.py +0 -0
  65. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/MediLink_PatientProcessor.py +0 -0
  66. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/MediLink_Scan.py +0 -0
  67. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/MediLink_Scheduler.py +0 -0
  68. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/MediLink_UI.py +0 -0
  69. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/MediLink_Up.py +0 -0
  70. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/MediLink_insurance_utils.py +0 -0
  71. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/MediLink_smart_import.py +0 -0
  72. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/Soumit_api.py +0 -0
  73. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/gmail_http_utils.py +0 -0
  74. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/gmail_oauth_utils.py +0 -0
  75. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/openssl.cnf +0 -0
  76. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/test.py +0 -0
  77. {medicafe-0.250912.0 → medicafe-0.250930.0}/MediLink/webapp.html +0 -0
  78. {medicafe-0.250912.0 → medicafe-0.250930.0}/README.md +0 -0
  79. {medicafe-0.250912.0 → medicafe-0.250930.0}/medicafe.egg-info/dependency_links.txt +0 -0
  80. {medicafe-0.250912.0 → medicafe-0.250930.0}/medicafe.egg-info/entry_points.txt +0 -0
  81. {medicafe-0.250912.0 → medicafe-0.250930.0}/medicafe.egg-info/not-zip-safe +0 -0
  82. {medicafe-0.250912.0 → medicafe-0.250930.0}/medicafe.egg-info/requires.txt +0 -0
  83. {medicafe-0.250912.0 → medicafe-0.250930.0}/medicafe.egg-info/top_level.txt +0 -0
  84. {medicafe-0.250912.0 → medicafe-0.250930.0}/setup.cfg +0 -0
@@ -19,7 +19,7 @@ Smart Import Integration:
19
19
  medibot_main = get_components('medibot_main')
20
20
  """
21
21
 
22
- __version__ = "0.250912.0"
22
+ __version__ = "0.250930.0"
23
23
  __author__ = "Daniel Vidaud"
24
24
  __email__ = "daniel@personalizedtransformation.com"
25
25
 
@@ -33,6 +33,13 @@ def get_default_config():
33
33
  'level': 'INFO',
34
34
  'console_output': True
35
35
  },
36
+ 'error_reporting': {
37
+ 'enabled': False,
38
+ 'endpoint_url': '',
39
+ 'auth_token': '',
40
+ 'insecure_http': False,
41
+ 'max_bundle_bytes': 2097152
42
+ },
36
43
  # STRATEGIC NOTE (COB Configuration): COB library is fully implemented and ready
37
44
  # To enable COB functionality, add the following configuration:
38
45
  # 'cob_settings': {
@@ -27,7 +27,7 @@ Smart Import System:
27
27
  api_suite = get_api_access()
28
28
  """
29
29
 
30
- __version__ = "0.250912.0"
30
+ __version__ = "0.250930.0"
31
31
  __author__ = "Daniel Vidaud"
32
32
  __email__ = "daniel@personalizedtransformation.com"
33
33
 
@@ -6,7 +6,7 @@ Moved from MediLink to centralize shared API operations.
6
6
  COMPATIBILITY: Python 3.4.4 and Windows XP compatible
7
7
  """
8
8
 
9
- import time, json, os, traceback, sys, requests
9
+ import time, json, os, traceback, sys
10
10
 
11
11
  # Import centralized logging configuration
12
12
  try:
@@ -689,21 +689,61 @@ class APIClient(BaseAPIClient):
689
689
  MediLink_ConfigLoader.log(log_message, level="ERROR")
690
690
  raise
691
691
 
692
- def fetch_payer_name_from_api(client, payer_id, config, primary_endpoint='AVAILITY'):
692
+ def fetch_payer_name_from_api(*args, **kwargs):
693
693
  """
694
- Fetches the payer name using the provided APIClient instance.
694
+ Fetch payer name by Payer ID with backward-compatible calling styles.
695
695
 
696
- :param client: An instance of APIClient
697
- :param payer_id: The payer ID to fetch
698
- :param primary_endpoint: The primary endpoint to use
699
- :return: The payer name if found
696
+ Supported call signatures:
697
+ - fetch_payer_name_from_api(client, payer_id, config, primary_endpoint='AVAILITY')
698
+ - fetch_payer_name_from_api(payer_id, config, primary_endpoint='AVAILITY') # client inferred via factory
699
+ - fetch_payer_name_from_api(payer_id=payer_id, config=config, client=client, primary_endpoint='AVAILITY')
700
700
  """
701
- # Ensure client is an instance of APIClient
701
+ # Normalize arguments
702
+ client = None
703
+ payer_id = None
704
+ config = None
705
+ primary_endpoint = kwargs.get('primary_endpoint', 'AVAILITY')
706
+
707
+ if 'client' in kwargs:
708
+ client = kwargs.get('client')
709
+ if 'payer_id' in kwargs:
710
+ payer_id = kwargs.get('payer_id')
711
+ if 'config' in kwargs:
712
+ config = kwargs.get('config')
713
+
714
+ if len(args) >= 3 and isinstance(args[0], APIClient):
715
+ client = args[0]
716
+ payer_id = args[1]
717
+ config = args[2]
718
+ if len(args) >= 4 and 'primary_endpoint' not in kwargs:
719
+ primary_endpoint = args[3]
720
+ elif len(args) >= 2 and isinstance(args[0], str) and client is None:
721
+ # Called as (payer_id, config, [primary_endpoint])
722
+ payer_id = args[0]
723
+ config = args[1]
724
+ if len(args) >= 3 and 'primary_endpoint' not in kwargs:
725
+ primary_endpoint = args[2]
726
+ elif len(args) == 1 and isinstance(args[0], APIClient) and (payer_id is None or config is None):
727
+ # Partial positional with client first, other params via kwargs
728
+ client = args[0]
729
+
730
+ # Acquire client via factory if not provided
731
+ if client is None:
732
+ try:
733
+ from MediCafe.core_utils import get_api_client
734
+ client = get_api_client()
735
+ if client is None:
736
+ client = APIClient()
737
+ except Exception:
738
+ client = APIClient()
739
+
740
+ # Basic validation
702
741
  if not isinstance(client, APIClient):
703
742
  error_message = "Invalid client provided. Expected an instance of APIClient."
704
- print(error_message)
705
743
  MediLink_ConfigLoader.log(error_message, level="ERROR")
706
- exit(1) # Exit the script
744
+ raise ValueError(error_message)
745
+ if payer_id is None or config is None:
746
+ raise ValueError("Missing required arguments: payer_id and config are required")
707
747
 
708
748
  # TODO: FUTURE IMPLEMENTATION - Remove AVAILITY default when other endpoints have payer-list APIs
709
749
  # Currently defaulting to AVAILITY as it's the only endpoint with confirmed payer-list functionality
@@ -750,6 +790,66 @@ def fetch_payer_name_from_api(client, payer_id, config, primary_endpoint='AVAILI
750
790
  # 3. Fall back to endpoints with basic payer lookup if available
751
791
  # 4. Use AVAILITY as final fallback
752
792
 
793
+ # If HTTP client is unavailable or endpoints missing, use offline fallback only when allowed (TestMode or env flag)
794
+ try:
795
+ http_unavailable = (requests is None) # type: ignore[name-defined]
796
+ except Exception:
797
+ http_unavailable = True
798
+ # Determine whether offline fallback is permitted
799
+ # Align with main flow: default True when TestMode key is absent
800
+ offline_allowed = True
801
+ try:
802
+ from MediCafe.core_utils import extract_medilink_config
803
+ medi_local = extract_medilink_config(config)
804
+ offline_allowed = bool(medi_local.get('TestMode', True))
805
+ except Exception:
806
+ offline_allowed = True
807
+ try:
808
+ if os.environ.get('MEDICAFE_OFFLINE_PERMISSIVE', '').strip().lower() in ('1', 'true', 'yes', 'y'):
809
+ offline_allowed = True
810
+ except Exception:
811
+ pass
812
+
813
+ if offline_allowed and (http_unavailable or not isinstance(endpoints, dict) or not endpoints):
814
+ try:
815
+ # Prefer crosswalk mapping when available
816
+ try:
817
+ _, cw = MediLink_ConfigLoader.load_configuration()
818
+ except Exception:
819
+ cw = {}
820
+ payer_mappings = {}
821
+ try:
822
+ if isinstance(cw, dict):
823
+ payer_mappings = cw.get('payer_mappings', {}) or {}
824
+ except Exception:
825
+ payer_mappings = {}
826
+
827
+ if payer_id in payer_mappings:
828
+ resolved = payer_mappings.get(payer_id)
829
+ MediLink_ConfigLoader.log(
830
+ "Using crosswalk mapping for payer {} -> {} (offline)".format(payer_id, resolved),
831
+ level="WARNING",
832
+ console_output=CONSOLE_LOGGING
833
+ )
834
+ return resolved
835
+ # Safe minimal hardcoded map as last resort (test/offline only)
836
+ fallback_map = {
837
+ '87726': 'UnitedHealthcare',
838
+ '06111': 'Aetna',
839
+ '03432': 'Cigna',
840
+ '95378': 'Anthem Blue Cross',
841
+ '95467': 'Blue Shield',
842
+ }
843
+ if payer_id in fallback_map:
844
+ MediLink_ConfigLoader.log(
845
+ "Using offline fallback for payer {} -> {}".format(payer_id, fallback_map[payer_id]),
846
+ level="WARNING",
847
+ console_output=CONSOLE_LOGGING
848
+ )
849
+ return fallback_map[payer_id]
850
+ except Exception:
851
+ pass
852
+
753
853
  # Define endpoint rotation logic with payer-list capability detection
754
854
  available_endpoints = []
755
855
 
@@ -820,6 +920,47 @@ def fetch_payer_name_from_api(client, payer_id, config, primary_endpoint='AVAILI
820
920
  error_message = "Error calling {0} for Payer ID {1}. Exception: {2}".format(endpoint_name, payer_id, e)
821
921
  MediLink_ConfigLoader.log(error_message, level="INFO")
822
922
 
923
+ # Offline/local fallback mapping for common payer IDs when API endpoints are unavailable
924
+ # Only when offline fallback is permitted
925
+ if offline_allowed:
926
+ try:
927
+ # Prefer crosswalk mapping first
928
+ try:
929
+ _, cw = MediLink_ConfigLoader.load_configuration()
930
+ except Exception:
931
+ cw = {}
932
+ payer_mappings = {}
933
+ try:
934
+ if isinstance(cw, dict):
935
+ payer_mappings = cw.get('payer_mappings', {}) or {}
936
+ except Exception:
937
+ payer_mappings = {}
938
+ if payer_id in payer_mappings:
939
+ resolved = payer_mappings.get(payer_id)
940
+ MediLink_ConfigLoader.log(
941
+ "Using crosswalk mapping for payer {} -> {} (offline)".format(payer_id, resolved),
942
+ level="WARNING",
943
+ console_output=CONSOLE_LOGGING
944
+ )
945
+ return resolved
946
+ # Minimal fallback map if crosswalk has no mapping (still offline-only)
947
+ fallback_map = {
948
+ '87726': 'UnitedHealthcare',
949
+ '06111': 'Aetna',
950
+ '03432': 'Cigna',
951
+ '95378': 'Anthem Blue Cross',
952
+ '95467': 'Blue Shield',
953
+ }
954
+ if payer_id in fallback_map:
955
+ MediLink_ConfigLoader.log(
956
+ "Using offline fallback for payer {} -> {}".format(payer_id, fallback_map[payer_id]),
957
+ level="WARNING",
958
+ console_output=CONSOLE_LOGGING
959
+ )
960
+ return fallback_map[payer_id]
961
+ except Exception:
962
+ pass
963
+
823
964
  # If all endpoints fail
824
965
  final_error_message = "All endpoints exhausted for Payer ID {0}.".format(payer_id)
825
966
  print(final_error_message)
@@ -0,0 +1,287 @@
1
+ import os, sys, time, json, zipfile, hashlib, platform
2
+
3
+ try:
4
+ import requests
5
+ except Exception:
6
+ requests = None
7
+
8
+ from MediCafe.MediLink_ConfigLoader import load_configuration, log as mc_log
9
+
10
+
11
+ ASCII_SAFE_REPLACEMENTS = [
12
+ ('"', '"'),
13
+ ("'", "'"),
14
+ ]
15
+
16
+
17
+ def _safe_ascii(text):
18
+ try:
19
+ if text is None:
20
+ return ''
21
+ if isinstance(text, bytes):
22
+ try:
23
+ text = text.decode('ascii', 'ignore')
24
+ except Exception:
25
+ text = text.decode('utf-8', 'ignore')
26
+ else:
27
+ text = str(text)
28
+ return text.encode('ascii', 'ignore').decode('ascii', 'ignore')
29
+ except Exception:
30
+ return ''
31
+
32
+
33
+ def _tail_file(path, max_lines):
34
+ lines = []
35
+ try:
36
+ with open(path, 'r') as f:
37
+ for line in f:
38
+ lines.append(line)
39
+ if len(lines) > max_lines:
40
+ lines.pop(0)
41
+ return ''.join(lines)
42
+ except Exception:
43
+ return ''
44
+
45
+
46
+ def _get_latest_log_path(local_storage_path):
47
+ try:
48
+ files = []
49
+ for name in os.listdir(local_storage_path or '.'):
50
+ if name.startswith('Log_') and name.endswith('.log'):
51
+ files.append(os.path.join(local_storage_path, name))
52
+ if not files:
53
+ return None
54
+ files.sort(key=lambda p: os.path.getmtime(p))
55
+ return files[-1]
56
+ except Exception:
57
+ return None
58
+
59
+
60
+ def _redact(text):
61
+ # Best-effort ASCII redaction: mask obvious numeric IDs and bearer tokens
62
+ try:
63
+ text = _safe_ascii(text)
64
+ import re
65
+ patterns = [
66
+ (r'\b(\d{3}-?\d{2}-?\d{4})\b', '***-**-****'),
67
+ (r'\b(\d{9,11})\b', '*********'),
68
+ (r'Bearer\s+[A-Za-z0-9\-._~+/]+=*', 'Bearer ***'),
69
+ (r'Authorization:\s*[^\n\r]+', 'Authorization: ***'),
70
+ ]
71
+ for pat, rep in patterns:
72
+ text = re.sub(pat, rep, text)
73
+ return text
74
+ except Exception:
75
+ return text
76
+
77
+
78
+ def _ensure_dir(path):
79
+ try:
80
+ if not os.path.exists(path):
81
+ os.makedirs(path)
82
+ return True
83
+ except Exception:
84
+ return False
85
+
86
+
87
+ def _compute_report_id(zip_path):
88
+ try:
89
+ h = hashlib.sha256()
90
+ with open(zip_path, 'rb') as f:
91
+ chunk = f.read(256 * 1024)
92
+ h.update(chunk)
93
+ return 'mc-{}-{}'.format(int(time.time()), h.hexdigest()[:12])
94
+ except Exception:
95
+ return 'mc-{}-{}'.format(int(time.time()), '000000000000')
96
+
97
+
98
+ def collect_support_bundle(include_traceback=True, max_log_lines=2000):
99
+ config, _ = load_configuration()
100
+ medi = config.get('MediLink_Config', {})
101
+ local_storage_path = medi.get('local_storage_path', '.')
102
+ queue_dir = os.path.join(local_storage_path, 'reports_queue')
103
+ _ensure_dir(queue_dir)
104
+
105
+ stamp = time.strftime('%Y%m%d_%H%M%S')
106
+ bundle_name = 'support_report_{}.zip'.format(stamp)
107
+ zip_path = os.path.join(queue_dir, bundle_name)
108
+
109
+ latest_log = _get_latest_log_path(local_storage_path)
110
+ log_tail = _tail_file(latest_log, max_log_lines) if latest_log else ''
111
+ log_tail = _redact(log_tail)
112
+
113
+ traceback_txt = ''
114
+ if include_traceback:
115
+ try:
116
+ trace_path = os.path.join(local_storage_path, 'traceback.txt')
117
+ if os.path.exists(trace_path):
118
+ with open(trace_path, 'r') as tf:
119
+ traceback_txt = _redact(tf.read())
120
+ except Exception:
121
+ traceback_txt = ''
122
+
123
+ meta = {
124
+ 'app_version': _safe_ascii(_get_version()),
125
+ 'python_version': _safe_ascii(sys.version.split(' ')[0]),
126
+ 'platform': _safe_ascii(platform.platform()),
127
+ 'timestamp': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
128
+ 'error_summary': _safe_ascii(_first_line(traceback_txt)),
129
+ 'traceback_present': bool(traceback_txt),
130
+ 'config_flags': {
131
+ 'console_logging': bool(medi.get('logging', {}).get('console_output', False)),
132
+ 'test_mode': bool(medi.get('TestMode', False))
133
+ }
134
+ }
135
+
136
+ try:
137
+ with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as z:
138
+ z.writestr('meta.json', json.dumps(meta, ensure_ascii=True, indent=2))
139
+ if latest_log and log_tail:
140
+ z.writestr('log_tail.txt', log_tail)
141
+ if traceback_txt:
142
+ z.writestr('traceback.txt', traceback_txt)
143
+ z.writestr('README.txt', _readme_text())
144
+ return zip_path
145
+ except Exception as e:
146
+ mc_log('Error creating support bundle: {}'.format(e), level='ERROR')
147
+ return None
148
+
149
+
150
+ def _first_line(text):
151
+ try:
152
+ for line in (text or '').splitlines():
153
+ line = line.strip()
154
+ if line:
155
+ return line[:200]
156
+ return ''
157
+ except Exception:
158
+ return ''
159
+
160
+
161
+ def _readme_text():
162
+ return (
163
+ "MediCafe Support Bundle\n\n"
164
+ "This archive contains a redacted log tail, optional traceback, and metadata.\n"
165
+ "You may submit this bundle automatically from the app or send it manually to support.\n"
166
+ )
167
+
168
+
169
+ def _get_version():
170
+ try:
171
+ from MediCafe import __version__
172
+ return __version__
173
+ except Exception:
174
+ return 'unknown'
175
+
176
+
177
+ def submit_support_bundle(zip_path):
178
+ config, _ = load_configuration()
179
+ medi = config.get('MediLink_Config', {})
180
+ rep = medi.get('error_reporting', {}) if isinstance(medi, dict) else {}
181
+ endpoint_url = _safe_ascii(rep.get('endpoint_url', ''))
182
+ auth_token = _safe_ascii(rep.get('auth_token', ''))
183
+ insecure = bool(rep.get('insecure_http', False))
184
+ max_bytes = int(rep.get('max_bundle_bytes', 2097152))
185
+
186
+ if not requests:
187
+ print("[ERROR] requests module not available; cannot submit report.")
188
+ return False
189
+ if not endpoint_url:
190
+ print("[ERROR] error_reporting.endpoint_url not configured.")
191
+ return False
192
+ if not os.path.exists(zip_path):
193
+ print("[ERROR] Bundle not found: {}".format(zip_path))
194
+ return False
195
+ try:
196
+ size = os.path.getsize(zip_path)
197
+ if size > max_bytes:
198
+ print("[INFO] Bundle size {} exceeds cap {}; rebuilding smaller not implemented here.".format(size, max_bytes))
199
+ except Exception:
200
+ pass
201
+
202
+ report_id = _compute_report_id(zip_path)
203
+ headers = {
204
+ 'X-Auth-Token': auth_token or '',
205
+ 'X-Report-Id': report_id,
206
+ 'User-Agent': 'MediCafe-Reporter/1.0'
207
+ }
208
+
209
+ # Prepare meta.json stream derived from inside the zip for server convenience
210
+ meta_json = '{}'
211
+ try:
212
+ with zipfile.ZipFile(zip_path, 'r') as z:
213
+ if 'meta.json' in z.namelist():
214
+ meta_json = z.read('meta.json')
215
+ except Exception:
216
+ meta_json = '{}'
217
+
218
+ try:
219
+ bundle_fh = open(zip_path, 'rb')
220
+ files = {
221
+ 'meta': ('meta.json', meta_json, 'application/json'),
222
+ 'bundle': (os.path.basename(zip_path), bundle_fh, 'application/zip')
223
+ }
224
+ r = requests.post(endpoint_url, headers=headers, files=files, timeout=(10, 20), verify=(not insecure))
225
+ code = getattr(r, 'status_code', None)
226
+ if code == 200:
227
+ print("[SUCCESS] Report submitted. ID: {}".format(report_id))
228
+ return True
229
+ elif code == 401:
230
+ print("[ERROR] Unauthorized (401). Check error_reporting.auth_token.")
231
+ return False
232
+ elif code == 413:
233
+ print("[ERROR] Too large (413). Consider reducing max log lines.")
234
+ return False
235
+ else:
236
+ print("[ERROR] Submission failed with status {}".format(code))
237
+ return False
238
+ except Exception as e:
239
+ print("[ERROR] Submission exception: {}".format(e))
240
+ return False
241
+ finally:
242
+ try:
243
+ bundle_fh.close()
244
+ except Exception:
245
+ pass
246
+
247
+
248
+ def flush_queued_reports():
249
+ config, _ = load_configuration()
250
+ medi = config.get('MediLink_Config', {})
251
+ local_storage_path = medi.get('local_storage_path', '.')
252
+ queue_dir = os.path.join(local_storage_path, 'reports_queue')
253
+ if not os.path.isdir(queue_dir):
254
+ return 0, 0
255
+ count_ok = 0
256
+ count_total = 0
257
+ for name in sorted(os.listdir(queue_dir)):
258
+ if not name.endswith('.zip'):
259
+ continue
260
+ zip_path = os.path.join(queue_dir, name)
261
+ count_total += 1
262
+ print("Attempting upload of queued report: {}".format(name))
263
+ ok = submit_support_bundle(zip_path)
264
+ if ok:
265
+ try:
266
+ os.remove(zip_path)
267
+ except Exception:
268
+ pass
269
+ count_ok += 1
270
+ return count_ok, count_total
271
+
272
+
273
+ def capture_unhandled_traceback(exc_type, exc_value, exc_traceback):
274
+ try:
275
+ config, _ = load_configuration()
276
+ medi = config.get('MediLink_Config', {})
277
+ local_storage_path = medi.get('local_storage_path', '.')
278
+ trace_path = os.path.join(local_storage_path, 'traceback.txt')
279
+ import traceback
280
+ text = ''.join(traceback.format_exception(exc_type, exc_value, exc_traceback))
281
+ text = _redact(text)
282
+ with open(trace_path, 'w') as f:
283
+ f.write(text)
284
+ print("An error occurred. A traceback was saved to {}".format(trace_path))
285
+ except Exception:
286
+ pass
287
+
@@ -87,21 +87,24 @@ except (ImportError, SystemError):
87
87
  except ImportError:
88
88
  MediLink_UI = None
89
89
 
90
- # Import MediBot crosswalk utilities with fallback - using dynamic imports to avoid circular dependencies
91
- try:
92
- import MediBot_Crosswalk_Utils
93
- update_crosswalk_with_corrected_payer_id = MediBot_Crosswalk_Utils.update_crosswalk_with_corrected_payer_id
94
- update_crosswalk_with_new_payer_id = MediBot_Crosswalk_Utils.update_crosswalk_with_new_payer_id
95
- except ImportError:
96
- # Fallback to main library if utility import fails
97
- try:
98
- import MediBot_Crosswalk_Library
99
- update_crosswalk_with_corrected_payer_id = MediBot_Crosswalk_Library.update_crosswalk_with_corrected_payer_id
100
- update_crosswalk_with_new_payer_id = MediBot_Crosswalk_Library.update_crosswalk_with_new_payer_id
101
- except (ImportError, AttributeError):
102
- # If functions are not available, set to None for graceful handling
103
- update_crosswalk_with_corrected_payer_id = None
104
- update_crosswalk_with_new_payer_id = None
90
+ # Import MediBot crosswalk utilities via centralized import helpers
91
+ from MediCafe.core_utils import import_medibot_module
92
+
93
+ # Resolve required functions directly at import-time using centralized helper (Py 3.4.4 friendly)
94
+ update_crosswalk_with_new_payer_id = (
95
+ import_medibot_module('MediBot_Crosswalk_Utils', 'update_crosswalk_with_new_payer_id') or
96
+ import_medibot_module('MediBot_Crosswalk_Library', 'update_crosswalk_with_new_payer_id')
97
+ )
98
+
99
+ update_crosswalk_with_corrected_payer_id = (
100
+ import_medibot_module('MediBot_Crosswalk_Utils', 'update_crosswalk_with_corrected_payer_id') or
101
+ import_medibot_module('MediBot_Crosswalk_Library', 'update_crosswalk_with_corrected_payer_id')
102
+ )
103
+
104
+ if not callable(update_crosswalk_with_new_payer_id):
105
+ raise RuntimeError("Crosswalk update function not available (new payer id). Ensure MediBot_Crosswalk_Utils or MediBot_Crosswalk_Library is importable.")
106
+ if not callable(update_crosswalk_with_corrected_payer_id):
107
+ raise RuntimeError("Crosswalk update function not available (corrected payer id). Ensure MediBot_Crosswalk_Utils or MediBot_Crosswalk_Library is importable.")
105
108
 
106
109
  # Import enhanced insurance selection with fallback
107
110
  # XP/Python34 Compatibility: Enhanced error handling with verbose output
@@ -390,17 +390,50 @@ def manual_deductible_lookup():
390
390
  # Already processed data from merge_responses
391
391
  enhanced_result = eligibility_data
392
392
  elif convert_eligibility_to_enhanced_format is not None:
393
+ # Attempt CSV backfill context for manual route
394
+ csv_row = patient_row_index.get((formatted_dob, member_id))
395
+ derived_patient_id = ""
396
+ derived_service_date = ""
397
+ if csv_row:
398
+ try:
399
+ derived_patient_id = str(csv_row.get('Patient ID #2', csv_row.get('Patient ID', '')))
400
+ derived_service_date = str(csv_row.get('Service Date', ''))
401
+ except Exception:
402
+ derived_patient_id = ""
403
+ derived_service_date = ""
393
404
  # Raw API data needs conversion with patient info
394
- enhanced_result = convert_eligibility_to_enhanced_format(eligibility_data, formatted_dob, member_id, "", "")
405
+ enhanced_result = convert_eligibility_to_enhanced_format(
406
+ eligibility_data, formatted_dob, member_id, derived_patient_id, derived_service_date
407
+ )
395
408
  else:
396
409
  # Fallback if utility function not available
397
410
  enhanced_result = None
398
411
  if enhanced_result:
399
412
  try:
400
413
  # Backfill with CSV row data when available
401
- enhanced_result = backfill_enhanced_result(enhanced_result, None)
414
+ csv_row = patient_row_index.get((formatted_dob, member_id))
415
+ enhanced_result = backfill_enhanced_result(enhanced_result, csv_row)
416
+ except Exception:
417
+ pass
418
+ # Ensure patient_id present; warn/log and set surrogate if missing
419
+ try:
420
+ pid = str(enhanced_result.get('patient_id', '')).strip()
421
+ if not pid:
422
+ surrogate = "{}:{}".format(formatted_dob, member_id)
423
+ enhanced_result['patient_id'] = surrogate
424
+ print("Warning: Missing Patient ID; using surrogate key {}".format(surrogate))
425
+ MediLink_ConfigLoader.log(
426
+ "Manual lookup: Missing Patient ID; using surrogate key {}".format(surrogate),
427
+ level="WARNING"
428
+ )
402
429
  except Exception:
403
430
  pass
431
+ # Ensure patient_name not blank
432
+ try:
433
+ if not str(enhanced_result.get('patient_name', '')).strip():
434
+ enhanced_result['patient_name'] = 'Unknown Patient'
435
+ except Exception:
436
+ enhanced_result['patient_name'] = 'Unknown Patient'
404
437
  print("\n" + "=" * 60)
405
438
  display_enhanced_deductible_table([enhanced_result], context="post_api",
406
439
  title="Manual Lookup Result")
@@ -441,7 +474,18 @@ def manual_deductible_lookup():
441
474
  "Patient Name", "DOB", "Insurance Type", "PayID", "Policy Status", "Remaining Amt")
442
475
  output_file.write(table_header + "\n")
443
476
  output_file.write("-" * len(table_header) + "\n")
444
- display_eligibility_info(eligibility_data, formatted_dob, member_id, output_file)
477
+ # Write directly from enhanced_result to ensure CSV backfill/defaults are preserved
478
+ if enhanced_result:
479
+ table_row = "{:<20} | {:<10} | {:<40} | {:<5} | {:<14} | {:<14}".format(
480
+ enhanced_result['patient_name'][:20],
481
+ enhanced_result['dob'],
482
+ enhanced_result['insurance_type'][:40],
483
+ enhanced_result['payer_id'][:5],
484
+ enhanced_result['policy_status'][:14],
485
+ enhanced_result['remaining_amount'][:14])
486
+ output_file.write(table_row + "\n")
487
+ else:
488
+ display_eligibility_info(eligibility_data, formatted_dob, member_id, output_file)
445
489
 
446
490
  # Ask if user wants to open the report
447
491
  open_report = input("\nEligibility data found! Open the report? (Y/N): ").strip().lower()
@@ -177,6 +177,20 @@ def _normalize_pre_api_data(row):
177
177
  member_id = row.get('Primary Policy Number', row.get('Ins1 Member ID', '')).strip()
178
178
  payer_id = row.get('Ins1 Payer ID', '')
179
179
  patient_id = row.get('Patient ID #2', row.get('Patient ID', ''))
180
+
181
+ # Surrogate key and warnings if patient_id missing/blank
182
+ if not str(patient_id).strip():
183
+ surrogate = "{}:{}".format(dob, member_id)
184
+ patient_id = surrogate
185
+ try:
186
+ # Print visible warning and log as WARNING event
187
+ print("Warning: Missing Patient ID in CSV row; using surrogate key {}".format(surrogate))
188
+ MediLink_ConfigLoader.log(
189
+ "Missing Patient ID in CSV; using surrogate key {}".format(surrogate),
190
+ level="WARNING"
191
+ )
192
+ except Exception:
193
+ pass
180
194
 
181
195
  return {
182
196
  'patient_id': str(patient_id),
@@ -200,7 +214,7 @@ def _normalize_post_api_data(eligibility_result):
200
214
  try:
201
215
  # Handle the enhanced format that comes from convert_eligibility_to_enhanced_format
202
216
  if isinstance(eligibility_result, dict):
203
- return {
217
+ normalized = {
204
218
  'patient_id': str(eligibility_result.get('patient_id', '')),
205
219
  'patient_name': str(eligibility_result.get('patient_name', '')),
206
220
  'dob': str(eligibility_result.get('dob', '')),
@@ -214,6 +228,28 @@ def _normalize_post_api_data(eligibility_result):
214
228
  'remaining_amount': str(eligibility_result.get('remaining_amount', '')),
215
229
  'data_source': str(eligibility_result.get('data_source', ''))
216
230
  }
231
+
232
+ # Default unknown patient name when blank
233
+ try:
234
+ if not normalized['patient_name'].strip():
235
+ normalized['patient_name'] = 'Unknown Patient'
236
+ except Exception:
237
+ normalized['patient_name'] = 'Unknown Patient'
238
+
239
+ # Surrogate key and warnings if patient_id missing/blank
240
+ try:
241
+ if not normalized['patient_id'].strip():
242
+ surrogate = "{}:{}".format(normalized.get('dob', ''), normalized.get('member_id', ''))
243
+ normalized['patient_id'] = surrogate
244
+ print("Warning: Missing Patient ID in eligibility result; using surrogate key {}".format(surrogate))
245
+ MediLink_ConfigLoader.log(
246
+ "Missing Patient ID in eligibility result; using surrogate key {}".format(surrogate),
247
+ level="WARNING"
248
+ )
249
+ except Exception:
250
+ pass
251
+
252
+ return normalized
217
253
  else:
218
254
  MediLink_ConfigLoader.log("Unexpected eligibility result format: {}".format(type(eligibility_result)), level="WARNING")
219
255
  return None
@@ -22,6 +22,7 @@ if PERFORMANCE_LOGGING:
22
22
 
23
23
  # Now import core utilities after path setup
24
24
  from MediCafe.core_utils import get_shared_config_loader, setup_module_paths, extract_medilink_config
25
+ from MediCafe.error_reporter import flush_queued_reports, collect_support_bundle, submit_support_bundle, capture_unhandled_traceback
25
26
  setup_module_paths(__file__)
26
27
 
27
28
  # Import modules after path setup
@@ -57,6 +58,8 @@ def _tools_menu(config, medi):
57
58
  print("\nMaintenance Tools:")
58
59
  options = [
59
60
  "Rebuild submission index now",
61
+ "Submit Error Report (online)",
62
+ "Create Support Bundle (offline)",
60
63
  "Back"
61
64
  ]
62
65
  MediLink_UI.display_menu(options)
@@ -75,6 +78,27 @@ def _tools_menu(config, medi):
75
78
  except Exception as e:
76
79
  print("Index rebuild error: {}".format(e))
77
80
  elif choice == '2':
81
+ try:
82
+ print("\nSubmitting Error Report (online)...")
83
+ zip_path = collect_support_bundle(include_traceback=True)
84
+ if not zip_path:
85
+ print("Failed to create support bundle.")
86
+ else:
87
+ ok = submit_support_bundle(zip_path)
88
+ if not ok:
89
+ print("Submission failed. Bundle saved at {} for later retry.".format(zip_path))
90
+ except Exception as e:
91
+ print("Error during report submission: {}".format(e))
92
+ elif choice == '3':
93
+ try:
94
+ zip_path = collect_support_bundle(include_traceback=True)
95
+ if zip_path:
96
+ print("Support bundle created: {}".format(zip_path))
97
+ else:
98
+ print("Failed to create support bundle.")
99
+ except Exception as e:
100
+ print("Error creating support bundle: {}".format(e))
101
+ elif choice == '4':
78
102
  break
79
103
  else:
80
104
  MediLink_UI.display_invalid_choice()
@@ -167,6 +191,15 @@ def main_menu():
167
191
  if PERFORMANCE_LOGGING:
168
192
  print("Welcome display completed in {:.2f} seconds".format(welcome_end - welcome_start))
169
193
 
194
+ # Startup: flush any queued error reports (non-blocking style)
195
+ try:
196
+ print("\nChecking for queued error reports...")
197
+ uploaded, total = flush_queued_reports()
198
+ if total:
199
+ print("Queued reports: {} | Uploaded now: {}".format(total, uploaded))
200
+ except Exception:
201
+ pass
202
+
170
203
  # Show message if new records were found during boot-time scan. TODO we need this to use the 'Last acknowledgements update:' timestamp to decide if it has already run in the last day so
171
204
  # that we're not running it multiple times in rapid succession automatically. (user-initiated checks are fine like via selection of (1. Check for new remittances))
172
205
  if ack_result:
@@ -428,6 +461,11 @@ if __name__ == "__main__":
428
461
  total_start_time = time.time()
429
462
  exit_code = 0
430
463
  try:
464
+ # Install unhandled exception hook to capture tracebacks
465
+ try:
466
+ sys.excepthook = capture_unhandled_traceback
467
+ except Exception:
468
+ pass
431
469
  main_menu()
432
470
  except ValueError as e:
433
471
  # Graceful domain error: show concise message without traceback, then exit
@@ -448,6 +486,25 @@ if __name__ == "__main__":
448
486
  # Unexpected error: still avoid full traceback, present succinct notice
449
487
  sys.stderr.write("An unexpected error occurred; process halted.\n")
450
488
  sys.stderr.write(str(e) + "\n")
489
+ # Offer to create and submit an error report
490
+ try:
491
+ ans = input("Create and submit an error report now? (y/N): ").strip().lower()
492
+ except Exception:
493
+ ans = 'n'
494
+ if ans in ['y', 'yes']:
495
+ try:
496
+ from MediCafe.error_reporter import collect_support_bundle, submit_support_bundle
497
+ zip_path = collect_support_bundle(include_traceback=True)
498
+ if not zip_path:
499
+ print("Failed to create support bundle.")
500
+ else:
501
+ ok = submit_support_bundle(zip_path)
502
+ if ok:
503
+ print("Report submitted successfully.")
504
+ else:
505
+ print("Submission failed. Bundle saved at {} for later retry.".format(zip_path))
506
+ except Exception as _erre:
507
+ print("Error while creating/submitting report: {}".format(_erre))
451
508
  sys.stderr.write("\nPress Enter to exit...\n")
452
509
  try:
453
510
  input()
@@ -22,7 +22,7 @@ Smart Import Integration:
22
22
  datamgmt = get_components('medilink_datamgmt')
23
23
  """
24
24
 
25
- __version__ = "0.250912.0"
25
+ __version__ = "0.250930.0"
26
26
  __author__ = "Daniel Vidaud"
27
27
  __email__ = "daniel@personalizedtransformation.com"
28
28
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: medicafe
3
- Version: 0.250912.0
3
+ Version: 0.250930.0
4
4
  Summary: MediCafe
5
5
  Home-page: https://github.com/katanada2/MediCafe
6
6
  Author: Daniel Vidaud
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: medicafe
3
- Version: 0.250912.0
3
+ Version: 0.250930.0
4
4
  Summary: MediCafe
5
5
  Home-page: https://github.com/katanada2/MediCafe
6
6
  Author: Daniel Vidaud
@@ -33,6 +33,7 @@ MediCafe/api_factory.py
33
33
  MediCafe/api_utils.py
34
34
  MediCafe/core_utils.py
35
35
  MediCafe/deductible_utils.py
36
+ MediCafe/error_reporter.py
36
37
  MediCafe/graphql_utils.py
37
38
  MediCafe/logging_config.py
38
39
  MediCafe/logging_demo.py
@@ -54,7 +54,7 @@ if long_description_text is None:
54
54
 
55
55
  setup(
56
56
  name='medicafe',
57
- version="0.250912.0",
57
+ version="0.250930.0",
58
58
  description='MediCafe',
59
59
  long_description=long_description_text,
60
60
  long_description_content_type='text/markdown',
File without changes
File without changes
File without changes
File without changes