medicafe 0.250728.9__py3-none-any.whl → 0.250805.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of medicafe might be problematic. Click here for more details.

Files changed (57) hide show
  1. MediBot/MediBot.bat +233 -19
  2. MediBot/MediBot.py +138 -46
  3. MediBot/MediBot_Crosswalk_Library.py +127 -623
  4. MediBot/MediBot_Crosswalk_Utils.py +618 -0
  5. MediBot/MediBot_Preprocessor.py +72 -17
  6. MediBot/MediBot_Preprocessor_lib.py +470 -76
  7. MediBot/MediBot_UI.py +32 -17
  8. MediBot/MediBot_dataformat_library.py +68 -20
  9. MediBot/MediBot_docx_decoder.py +120 -19
  10. MediBot/MediBot_smart_import.py +180 -0
  11. MediBot/__init__.py +89 -0
  12. MediBot/get_medicafe_version.py +25 -0
  13. MediBot/update_json.py +35 -6
  14. MediBot/update_medicafe.py +19 -1
  15. MediCafe/MediLink_ConfigLoader.py +160 -0
  16. MediCafe/__init__.py +171 -0
  17. MediCafe/__main__.py +222 -0
  18. MediCafe/api_core.py +1098 -0
  19. MediCafe/api_core_backup.py +427 -0
  20. MediCafe/api_factory.py +306 -0
  21. MediCafe/api_utils.py +356 -0
  22. MediCafe/core_utils.py +450 -0
  23. MediCafe/graphql_utils.py +445 -0
  24. MediCafe/logging_config.py +123 -0
  25. MediCafe/logging_demo.py +61 -0
  26. MediCafe/migration_helpers.py +463 -0
  27. MediCafe/smart_import.py +436 -0
  28. MediLink/MediLink_837p_cob_library.py +28 -28
  29. MediLink/MediLink_837p_encoder.py +33 -34
  30. MediLink/MediLink_837p_encoder_library.py +226 -150
  31. MediLink/MediLink_837p_utilities.py +129 -5
  32. MediLink/MediLink_API_Generator.py +83 -60
  33. MediLink/MediLink_API_v3.py +1 -1
  34. MediLink/MediLink_ClaimStatus.py +177 -31
  35. MediLink/MediLink_DataMgmt.py +378 -63
  36. MediLink/MediLink_Decoder.py +20 -1
  37. MediLink/MediLink_Deductible.py +155 -28
  38. MediLink/MediLink_Display_Utils.py +72 -0
  39. MediLink/MediLink_Down.py +127 -5
  40. MediLink/MediLink_Gmail.py +712 -653
  41. MediLink/MediLink_PatientProcessor.py +257 -0
  42. MediLink/MediLink_UI.py +85 -71
  43. MediLink/MediLink_Up.py +28 -4
  44. MediLink/MediLink_insurance_utils.py +227 -230
  45. MediLink/MediLink_main.py +248 -0
  46. MediLink/MediLink_smart_import.py +264 -0
  47. MediLink/__init__.py +93 -1
  48. MediLink/insurance_type_integration_test.py +13 -3
  49. MediLink/test.py +1 -1
  50. MediLink/test_timing.py +59 -0
  51. {medicafe-0.250728.9.dist-info → medicafe-0.250805.0.dist-info}/METADATA +1 -1
  52. medicafe-0.250805.0.dist-info/RECORD +81 -0
  53. medicafe-0.250805.0.dist-info/entry_points.txt +2 -0
  54. {medicafe-0.250728.9.dist-info → medicafe-0.250805.0.dist-info}/top_level.txt +1 -0
  55. medicafe-0.250728.9.dist-info/RECORD +0 -59
  56. {medicafe-0.250728.9.dist-info → medicafe-0.250805.0.dist-info}/LICENSE +0 -0
  57. {medicafe-0.250728.9.dist-info → medicafe-0.250805.0.dist-info}/WHEEL +0 -0
@@ -1,13 +1,29 @@
1
1
  import csv, os, re, subprocess, time
2
2
  from datetime import datetime, timedelta
3
3
 
4
+ # Import centralized logging configuration
5
+ try:
6
+ from MediCafe.logging_config import PERFORMANCE_LOGGING
7
+ except ImportError:
8
+ # Fallback to local flag if centralized config is not available
9
+ PERFORMANCE_LOGGING = False
10
+
4
11
  # Need this for running Medibot and MediLink
12
+ from MediCafe.core_utils import get_shared_config_loader
13
+ MediLink_ConfigLoader = get_shared_config_loader()
5
14
  try:
6
- import MediLink_ConfigLoader
7
- import MediLink_UI
15
+ import MediLink_Display_Utils
8
16
  except ImportError:
9
- from . import MediLink_ConfigLoader
10
- from . import MediLink_UI
17
+ from MediLink import MediLink_Display_Utils
18
+
19
+ # MediBot imports will be done locally in functions to avoid circular imports
20
+ def _get_medibot_function(module_name, function_name):
21
+ """Dynamically import MediBot functions when needed to avoid circular imports."""
22
+ try:
23
+ module = __import__('MediBot.{}'.format(module_name), fromlist=[function_name])
24
+ return getattr(module, function_name)
25
+ except (ImportError, AttributeError):
26
+ return None
11
27
 
12
28
  # Helper function to slice and strip values with optional key suffix
13
29
  def slice_data(data, slices, suffix=''):
@@ -29,16 +45,17 @@ def parse_fixed_width_data(personal_info, insurance_info, service_info, service_
29
45
  insurance_slices = config['fixedWidthSlices']['insurance_slices']
30
46
  service_slices = config['fixedWidthSlices']['service_slices']
31
47
 
32
- # Parse each segment
48
+ # Parse each segment - core 3-line record structure
33
49
  parsed_data = {}
34
- parsed_data.update(slice_data(personal_info, personal_slices))
35
- parsed_data.update(slice_data(insurance_info, insurance_slices))
36
- parsed_data.update(slice_data(service_info, service_slices))
50
+ parsed_data.update(slice_data(personal_info, personal_slices)) # Line 1: Personal info
51
+ parsed_data.update(slice_data(insurance_info, insurance_slices)) # Line 2: Insurance info
52
+ parsed_data.update(slice_data(service_info, service_slices)) # Line 3: Service info
37
53
 
38
- if service_info_2:
54
+ # Parse reserved expansion lines (future-ready design)
55
+ if service_info_2: # Line 4: Reserved for additional service data
39
56
  parsed_data.update(slice_data(service_info_2, service_slices, suffix='_2'))
40
57
 
41
- if service_info_3:
58
+ if service_info_3: # Line 5: Reserved for additional service data
42
59
  parsed_data.update(slice_data(service_info_3, service_slices, suffix='_3'))
43
60
 
44
61
  # Replace underscores with spaces in first and last names since this is downstream of MediSoft.
@@ -53,52 +70,174 @@ def parse_fixed_width_data(personal_info, insurance_info, service_info, service_
53
70
 
54
71
  # Function to read fixed-width Medisoft output and extract claim data
55
72
  def read_fixed_width_data(file_path):
56
- # Reads the fixed width data from the file and yields each patient's
57
- # personal, insurance, and service information.
58
- MediLink_ConfigLoader.log("Starting to read fixed width data...")
59
- with open(file_path, 'r') as file:
60
- lines_buffer = [] # Buffer to hold lines for current patient data
61
-
62
- def yield_record(buffer):
63
- personal_info = buffer[0]
64
- insurance_info = buffer[1]
65
- service_info = buffer[2]
66
- service_info_2 = buffer[3] if len(buffer) > 3 else None
67
- service_info_3 = buffer[4] if len(buffer) > 4 else None
68
- MediLink_ConfigLoader.log("Successfully read data from file: {}".format(file_path), level="INFO")
69
- return personal_info, insurance_info, service_info, service_info_2, service_info_3
70
-
71
- for line in file:
72
- stripped_line = line.strip()
73
- if stripped_line:
74
- lines_buffer.append(stripped_line)
75
- if 3 <= len(lines_buffer) <= 5:
76
- next_line = file.readline().strip()
77
- if not next_line:
78
- yield yield_record(lines_buffer)
79
- lines_buffer.clear()
80
- else:
81
- if len(lines_buffer) >= 3:
82
- yield yield_record(lines_buffer)
83
- lines_buffer.clear()
84
-
85
- if lines_buffer: # Yield any remaining buffer if file ends without a blank line
86
- yield yield_record(lines_buffer)
73
+ """
74
+ Legacy function maintained for backward compatibility.
75
+ Reads fixed-width Medisoft data with RESERVED 5-line patient record format.
76
+
77
+ DESIGN NOTE: This implements a reserved record structure where each patient
78
+ record can contain 3-5 lines (currently using 3, with 2 reserved for future expansion).
79
+ The peek-ahead logic is intentional to handle variable-length records and maintain
80
+ proper spacing between patient records.
81
+ """
82
+ # Use the consolidated function with Medisoft-specific configuration
83
+ medisoft_config = {
84
+ 'mode': 'medisoft_records',
85
+ 'min_lines': 3,
86
+ 'max_lines': 5,
87
+ 'skip_header': False
88
+ }
89
+ return read_consolidated_fixed_width_data(file_path, config=medisoft_config)
87
90
 
88
- # TODO (Refactor) Consider consolidating with the other read_fixed_with_data
89
91
  def read_general_fixed_width_data(file_path, slices):
90
- # handle any fixed-width data based on provided slice definitions
92
+ """
93
+ Legacy function maintained for backward compatibility.
94
+ Handles fixed-width data based on provided slice definitions.
95
+ """
96
+ # Use the consolidated function with slice-based configuration
97
+ slice_config = {
98
+ 'mode': 'slice_based',
99
+ 'slices': slices,
100
+ 'skip_header': True
101
+ }
102
+ return read_consolidated_fixed_width_data(file_path, config=slice_config)
103
+
104
+ # CONSOLIDATED IMPLEMENTATION - Replaces both read_fixed_width_data and read_general_fixed_width_data
105
+ def read_consolidated_fixed_width_data(file_path, config=None):
106
+ """
107
+ Unified function for reading various fixed-width data formats.
108
+
109
+ Parameters:
110
+ - file_path: Path to the fixed-width data file
111
+ - config: Configuration dictionary with the following keys:
112
+ - mode: 'medisoft_records' (reserved 5-line format) or 'slice_based'
113
+ - slices: Dictionary of field slices (for slice_based mode)
114
+ - skip_header: Boolean, whether to skip first line
115
+ - min_lines: Minimum lines per record (for medisoft_records mode)
116
+ - max_lines: Maximum lines per record (for medisoft_records mode)
117
+
118
+ Returns:
119
+ - Generator yielding parsed records based on the specified mode
120
+ """
121
+ if config is None:
122
+ config = {'mode': 'medisoft_records', 'min_lines': 3, 'max_lines': 5, 'skip_header': False}
123
+
124
+ mode = config.get('mode', 'medisoft_records')
125
+
91
126
  try:
92
127
  with open(file_path, 'r', encoding='utf-8') as file:
93
- next(file) # Skip the header
94
- for line_number, line in enumerate(file, start=1):
95
- insurance_name = {key: line[start:end].strip() for key, (start, end) in slices.items()}
96
- yield insurance_name, line_number
128
+ # Skip header if configured
129
+ if config.get('skip_header', False):
130
+ next(file)
131
+
132
+ if mode == 'medisoft_records':
133
+ # Handle Medisoft reserved 5-line patient record format
134
+ yield from _process_medisoft_records(file, file_path, config)
135
+ elif mode == 'slice_based':
136
+ # Handle slice-based field extraction
137
+ yield from _process_slice_based_records(file, file_path, config)
138
+ else:
139
+ raise ValueError("Invalid mode '{}'. Use 'medisoft_records' or 'slice_based'".format(mode))
140
+
97
141
  except FileNotFoundError:
98
142
  print("File not found: {}".format(file_path))
99
143
  MediLink_ConfigLoader.log("File not found: {}".format(file_path), level="ERROR")
100
144
  return
101
145
 
146
+ def _process_medisoft_records(file, file_path, config):
147
+ """Helper function to process Medisoft-style multi-line patient records
148
+
149
+ RECORD FORMAT DESIGN (RESERVED 5-LINE STRUCTURE):
150
+ ════════════════════════════════════════════════════════════════
151
+ Each patient record is designed as a RESERVED 5-line block:
152
+
153
+ Line 1: Personal Info (REQUIRED - Always present)
154
+ Line 2: Insurance Info (REQUIRED - Always present)
155
+ Line 3: Service Info (REQUIRED - Always present)
156
+ Line 4: Service Info 2 (RESERVED - Future expansion)
157
+ Line 5: Service Info 3 (RESERVED - Future expansion)
158
+ [Blank line] (Record separator)
159
+
160
+ CURRENT PRODUCTION STATE:
161
+ ────────────────────────────
162
+ - Currently using: 3 active lines per record
163
+ - Lines 4-5: Reserved for future service data expansion
164
+ - Blank lines: Maintain proper record separation
165
+ - Total: 5-line reserved format ready for expansion
166
+
167
+ PEEK-AHEAD LOGIC PURPOSE:
168
+ ────────────────────────────
169
+ The peek-ahead logic serves to:
170
+ 1. Detect when we've reached the end of a record (blank line)
171
+ 2. Allow for future expansion to 4-5 lines without code changes
172
+ 3. Maintain proper spacing between patient records
173
+ 4. Handle variable-length records (3-5 lines) gracefully
174
+
175
+ FUTURE EXPANSION READY:
176
+ ────────────────────────────
177
+ When additional service lines are needed:
178
+ - No code changes required in reader logic
179
+ - Parser automatically handles service_info_2 and service_info_3
180
+ - Slice definitions can be extended in configuration
181
+ - Maintains backward compatibility with 3-line records
182
+ ════════════════════════════════════════════════════════════════
183
+ """
184
+ MediLink_ConfigLoader.log("Starting to read fixed width data...")
185
+ lines_buffer = [] # Buffer to hold lines for current patient data
186
+ min_lines = config.get('min_lines', 3)
187
+ max_lines = config.get('max_lines', 5)
188
+
189
+ def yield_record(buffer):
190
+ personal_info = buffer[0] # Line 1: Always present (personal data)
191
+ insurance_info = buffer[1] # Line 2: Always present (insurance data)
192
+ service_info = buffer[2] # Line 3: Always present (primary service data)
193
+ service_info_2 = buffer[3] if len(buffer) > 3 else None # Line 4: Reserved (future expansion)
194
+ service_info_3 = buffer[4] if len(buffer) > 4 else None # Line 5: Reserved (future expansion)
195
+ MediLink_ConfigLoader.log("Successfully read data from file: {}".format(file_path), level="INFO")
196
+ return personal_info, insurance_info, service_info, service_info_2, service_info_3
197
+
198
+ # RESERVED RECORD FORMAT PROCESSING
199
+ # This logic handles the current 3-line + 2-reserved-line format
200
+ # and is ready for future expansion without modification
201
+ for line in file:
202
+ stripped_line = line.strip()
203
+ if stripped_line:
204
+ lines_buffer.append(stripped_line)
205
+ # Check if we're within the reserved record size (3-5 lines)
206
+ if min_lines <= len(lines_buffer) <= max_lines:
207
+ # Peek ahead to detect record boundary (intentional design)
208
+ next_line = file.readline().strip()
209
+ if not next_line:
210
+ # Found record separator (blank line) - yield complete record
211
+ yield yield_record(lines_buffer)
212
+ lines_buffer.clear()
213
+ # If next_line has content, continue building the record
214
+ # (This handles future expansion to 4-5 line records)
215
+ else:
216
+ # Blank line encountered - end of current record
217
+ if len(lines_buffer) >= min_lines:
218
+ yield yield_record(lines_buffer)
219
+ lines_buffer.clear()
220
+
221
+ if lines_buffer: # Yield any remaining buffer if file ends without a blank line
222
+ yield yield_record(lines_buffer)
223
+
224
+ def _process_slice_based_records(file, file_path, config):
225
+ """Helper function to process slice-based field extraction"""
226
+ slices = config.get('slices', {})
227
+ for line_number, line in enumerate(file, start=1):
228
+ extracted_data = {key: line[start:end].strip() for key, (start, end) in slices.items()}
229
+ yield extracted_data, line_number
230
+
231
+ # TODO (Refactor) COMPLETED: Consolidated read_fixed_width_data and read_general_fixed_width_data
232
+ # into read_consolidated_fixed_width_data with unified configuration-based approach.
233
+ # Legacy functions maintained for backward compatibility.
234
+ #
235
+ # DESIGN NOTE: The reserved 5-line record format is intentional architecture:
236
+ # - Lines 1-3: Active data (personal, insurance, service)
237
+ # - Lines 4-5: Reserved for future service expansion
238
+ # - Peek-ahead logic maintains proper record spacing and handles variable-length records
239
+ # Next steps: Update all callers to use the new consolidated function directly.
240
+
102
241
  def consolidate_csvs(source_directory, file_prefix="Consolidated", interactive=False):
103
242
  """
104
243
  Consolidate CSV files in the source directory into a single CSV file.
@@ -411,9 +550,40 @@ def build_command(winscp_path, winscp_log_path, endpoint_config, remote_director
411
550
 
412
551
  # Handle filemask input
413
552
  if filemask:
414
- # TODO: Implement logic to translate filemask into WinSCP syntax
415
- # This should handle cases where filemask is a list, JSON, dictionary, or None.
416
- # Example: Convert to a string like "*.{ext1}|*.{ext2}|*.{ext3}".
553
+ # TODO (MEDIUM PRIORITY - WinSCP Filemask Implementation):
554
+ # PROBLEM: Need to translate various filemask input formats into proper WinSCP syntax.
555
+ # Current implementation is incomplete and may not handle all edge cases.
556
+ #
557
+ # WINSCP FILEMASK SYNTAX REQUIREMENTS:
558
+ # - Multiple extensions: "*.ext1|*.ext2|*.ext3" (pipe-separated)
559
+ # - Single extension: "*.ext"
560
+ # - All files: "*" or "*.*"
561
+ # - Date patterns: "*YYYYMMDD*" for date-based filtering
562
+ # - Size patterns: ">100K" for files larger than 100KB
563
+ # - Combined: "*.csv|*.txt;>1K" (semicolon for AND conditions)
564
+ #
565
+ # INPUT FORMATS TO HANDLE:
566
+ # 1. List: ['csv', 'txt', 'pdf'] -> "*.csv|*.txt|*.pdf"
567
+ # 2. Dictionary: {'extensions': ['csv'], 'size': '>1K'} -> "*.csv;>1K"
568
+ # 3. String: "csv,txt" -> "*.csv|*.txt" (comma-separated to pipe-separated)
569
+ # 4. None/Empty: -> "*" (all files)
570
+ #
571
+ # IMPLEMENTATION STEPS:
572
+ # 1. Add helper function normalize_filemask(filemask) -> str
573
+ # 2. Handle list input: join with "|" prefix with "*."
574
+ # 3. Handle dict input: combine extensions with other filters using ";"
575
+ # 4. Handle string input: split on comma and normalize
576
+ # 5. Add validation for WinSCP-compatible patterns
577
+ # 6. Add logging for debugging filemask translations
578
+ #
579
+ # TESTING SCENARIOS:
580
+ # - filemask=['csv', 'txt'] should work for CSV and TXT files
581
+ # - filemask={'extensions': ['pdf'], 'min_size': '1MB'} for large PDFs
582
+ # - filemask="era,835" for claims response files
583
+ # - filemask=None should download all files
584
+ #
585
+ # FILES TO MODIFY: This file (execute_winscp_command function)
586
+ # REFERENCE: WinSCP documentation on file masks and filters
417
587
  if isinstance(filemask, list):
418
588
  filemask_str = '|'.join(['*.' + ext for ext in filemask])
419
589
  elif isinstance(filemask, dict):
@@ -488,7 +658,32 @@ def execute_winscp_command(command, operation_type, files, local_storage_path):
488
658
  MediLink_ConfigLoader.log("WinSCP {} operation completed successfully.".format(operation_type))
489
659
 
490
660
  if operation_type == 'download':
491
- downloaded_files = list_downloaded_files(local_storage_path) # BUG This isn't behaving correctly because the local_storage_path isn't where winscp is dumping the files
661
+ downloaded_files = list_downloaded_files(local_storage_path)
662
+ # TODO (HIGH PRIORITY - WinSCP Path Configuration Issue):
663
+ # PROBLEM: WinSCP is not downloading files to the expected local_storage_path directory.
664
+ # The list_downloaded_files() function is checking the wrong location.
665
+ #
666
+ # INVESTIGATION STEPS:
667
+ # 1. Check WinSCP logs to determine actual download destination:
668
+ # - Look in config['MediLink_Config']['local_claims_path'] + "winscp_download.log"
669
+ # - Parse log entries for "file downloaded to:" or similar patterns
670
+ # 2. Compare actual WinSCP download path vs configured local_storage_path
671
+ # 3. Check if WinSCP uses different path conventions (forward/backward slashes)
672
+ #
673
+ # IMPLEMENTATION OPTIONS:
674
+ # Option A: Fix WinSCP command to use correct target directory
675
+ # - Update the lcd_command generation in execute_winscp_command()
676
+ # - Ensure local_storage_path is properly escaped for WinSCP
677
+ # Option B: Update list_downloaded_files() to check actual WinSCP location
678
+ # - Add function get_actual_winscp_download_path() that parses logs
679
+ # - Call list_downloaded_files(get_actual_winscp_download_path())
680
+ # Option C: Add configuration parameter for WinSCP-specific download path
681
+ # - Add 'winscp_download_path' to config
682
+ # - Default to local_storage_path if not specified
683
+ #
684
+ # RECOMMENDED: Option A (fix root cause) + Option C (explicit config)
685
+ # FILES TO MODIFY: This file (execute_winscp_command, list_downloaded_files functions)
686
+ # TESTING: Verify downloads work correctly after fix with various file types
492
687
  MediLink_ConfigLoader.log("Files currently located in local_storage_path: {}".format(downloaded_files), level="DEBUG")
493
688
 
494
689
  if not downloaded_files:
@@ -537,40 +732,58 @@ def detect_new_files(directory_path, file_extension='.DAT'):
537
732
  :param file_extension: Extension of the files to detect.
538
733
  :return: A tuple containing a list of paths to new files detected in the directory and a flag indicating if a new file was just renamed.
539
734
  """
735
+ import time
736
+ detect_start = time.time()
737
+ if PERFORMANCE_LOGGING:
738
+ MediLink_ConfigLoader.log("File detection started for directory: {}".format(directory_path), level="INFO")
739
+
540
740
  MediLink_ConfigLoader.log("Scanning directory: {}".format(directory_path), level="INFO")
541
741
  detected_file_paths = []
542
742
  file_flagged = False
543
743
 
544
744
  try:
745
+ listdir_start = time.time()
545
746
  filenames = os.listdir(directory_path)
546
- MediLink_ConfigLoader.log("Files in directory: {}".format(filenames), level="INFO")
747
+ listdir_end = time.time()
748
+ if PERFORMANCE_LOGGING:
749
+ print("Directory listing completed in {:.2f} seconds".format(listdir_end - listdir_start))
750
+
751
+ # Batch log the files found instead of logging each one individually
752
+ matching_files = []
753
+ for filename in filenames:
754
+ if filename.endswith(file_extension):
755
+ matching_files.append(filename)
756
+
757
+ if matching_files:
758
+ MediLink_ConfigLoader.log("Found {} files with extension {}: {}".format(
759
+ len(matching_files), file_extension, matching_files), level="INFO")
547
760
 
761
+ file_check_start = time.time()
548
762
  for filename in filenames:
549
- MediLink_ConfigLoader.log("Checking file: {}".format(filename), level="INFO")
550
763
  if filename.endswith(file_extension):
551
- MediLink_ConfigLoader.log("File matches extension: {}".format(file_extension), level="INFO")
552
764
  name, ext = os.path.splitext(filename)
553
- MediLink_ConfigLoader.log("File name: {}, File extension: {}".format(name, ext), level="INFO")
554
765
 
555
766
  if not is_timestamped(name):
556
- MediLink_ConfigLoader.log("File is not timestamped: {}".format(filename), level="INFO")
557
767
  new_name = "{}_{}{}".format(name, datetime.now().strftime('%Y%m%d_%H%M%S'), ext)
558
768
  os.rename(os.path.join(directory_path, filename), os.path.join(directory_path, new_name))
559
769
  MediLink_ConfigLoader.log("Renamed file from {} to {}".format(filename, new_name), level="INFO")
560
770
  file_flagged = True
561
771
  filename = new_name
562
- else:
563
- MediLink_ConfigLoader.log("File is already timestamped: {}".format(filename), level="INFO")
564
772
 
565
773
  file_path = os.path.join(directory_path, filename)
566
774
  detected_file_paths.append(file_path)
567
- MediLink_ConfigLoader.log("Detected file path: {}".format(file_path), level="INFO")
775
+
776
+ file_check_end = time.time()
777
+ if PERFORMANCE_LOGGING:
778
+ print("File checking completed in {:.2f} seconds".format(file_check_end - file_check_start))
568
779
 
569
780
  except Exception as e:
570
781
  MediLink_ConfigLoader.log("Error occurred: {}".format(str(e)), level="INFO")
571
782
 
572
- MediLink_ConfigLoader.log("Detected files: {}".format(detected_file_paths), level="INFO")
573
- MediLink_ConfigLoader.log("File flagged status: {}".format(file_flagged), level="INFO")
783
+ detect_end = time.time()
784
+ if PERFORMANCE_LOGGING:
785
+ print("File detection completed in {:.2f} seconds".format(detect_end - detect_start))
786
+ MediLink_ConfigLoader.log("Detected {} files, flagged: {}".format(len(detected_file_paths), file_flagged), level="INFO")
574
787
 
575
788
  return detected_file_paths, file_flagged
576
789
 
@@ -632,7 +845,7 @@ def bulk_edit_insurance_types(detailed_patient_data, insurance_options):
632
845
  new_insurance_type = input("Enter new insurance type (or press Enter to keep current): ").strip().upper()
633
846
 
634
847
  if new_insurance_type == 'LIST':
635
- MediLink_UI.display_insurance_options(insurance_options)
848
+ MediLink_Display_Utils.display_insurance_options(insurance_options)
636
849
  continue
637
850
 
638
851
  elif not new_insurance_type:
@@ -664,4 +877,106 @@ def review_and_confirm_changes(detailed_patient_data, insurance_options):
664
877
  insurance_description = insurance_options.get(insurance_type, "Unknown")
665
878
  print("{:<20} {:<10} {:<30}".format(data['patient_name'], insurance_type, insurance_description))
666
879
  confirm = input("\nConfirm changes? (y/n): ").strip().lower()
667
- return confirm in ['y', 'yes', '']
880
+ return confirm in ['y', 'yes', '']
881
+
882
+
883
+ def update_suggested_endpoint_with_user_preference(detailed_patient_data, patient_index, new_endpoint, config, crosswalk):
884
+ """
885
+ Updates the suggested endpoint for a patient and optionally updates the crosswalk
886
+ for future patients with the same insurance.
887
+
888
+ :param detailed_patient_data: List of patient data dictionaries
889
+ :param patient_index: Index of the patient being updated
890
+ :param new_endpoint: The new endpoint selected by the user
891
+ :param config: Configuration settings
892
+ :param crosswalk: Crosswalk data for in-memory updates
893
+ :return: Updated crosswalk if changes were made, None otherwise
894
+ """
895
+ # Note: load_insurance_data_from_mains is now imported at module level
896
+
897
+ data = detailed_patient_data[patient_index]
898
+ original_suggested = data.get('suggested_endpoint')
899
+
900
+ # Update the patient's endpoint preference
901
+ data['user_preferred_endpoint'] = new_endpoint
902
+ data['confirmed_endpoint'] = new_endpoint
903
+
904
+ # If user changed from the original suggestion, offer to update crosswalk
905
+ if original_suggested != new_endpoint:
906
+ primary_insurance = data.get('primary_insurance')
907
+ patient_name = data.get('patient_name')
908
+
909
+ print("\nYou changed the endpoint for {} from {} to {}.".format(patient_name, original_suggested, new_endpoint))
910
+ update_future = input("Would you like to use {} as the default endpoint for future patients with {}? (Y/N): ".format(new_endpoint, primary_insurance)).strip().lower()
911
+
912
+ if update_future in ['y', 'yes']:
913
+ # Find the payer ID associated with this insurance
914
+ load_insurance_data_from_mains = _get_medibot_function('MediBot_Preprocessor_lib', 'load_insurance_data_from_mains')
915
+ insurance_to_id = load_insurance_data_from_mains(config) if load_insurance_data_from_mains else {}
916
+ insurance_id = insurance_to_id.get(primary_insurance)
917
+
918
+ if insurance_id:
919
+ # Find the payer ID in crosswalk and update it
920
+ updated = False
921
+ for payer_id, payer_data in crosswalk.get('payer_id', {}).items():
922
+ medisoft_ids = [str(id) for id in payer_data.get('medisoft_id', [])]
923
+ if str(insurance_id) in medisoft_ids:
924
+ # Update the crosswalk in memory
925
+ crosswalk['payer_id'][payer_id]['endpoint'] = new_endpoint
926
+ MediLink_ConfigLoader.log("Updated crosswalk in memory: Payer ID {} ({}) now defaults to {}".format(payer_id, primary_insurance, new_endpoint), level="INFO")
927
+
928
+ # Update suggested_endpoint for other patients with same insurance in current batch
929
+ for other_data in detailed_patient_data:
930
+ if (other_data.get('primary_insurance') == primary_insurance and
931
+ 'user_preferred_endpoint' not in other_data):
932
+ other_data['suggested_endpoint'] = new_endpoint
933
+
934
+ updated = True
935
+ break
936
+
937
+ if updated:
938
+ # Save the updated crosswalk to disk immediately using API bypass mode
939
+ if save_crosswalk_immediately(config, crosswalk):
940
+ print("Updated default endpoint for {} to {}".format(primary_insurance, new_endpoint))
941
+ else:
942
+ print("Updated endpoint preference (will be saved during next crosswalk update)")
943
+ return crosswalk
944
+ else:
945
+ MediLink_ConfigLoader.log("Could not find payer ID in crosswalk for insurance {}".format(primary_insurance), level="WARNING")
946
+ else:
947
+ MediLink_ConfigLoader.log("Could not find insurance ID for {} to update crosswalk".format(primary_insurance), level="WARNING")
948
+
949
+ return None
950
+
951
+
952
+ def save_crosswalk_immediately(config, crosswalk):
953
+ """
954
+ Saves the crosswalk to disk immediately using API bypass mode.
955
+
956
+ :param config: Configuration settings
957
+ :param crosswalk: Crosswalk data to save
958
+ :return: True if saved successfully, False otherwise
959
+ """
960
+ try:
961
+ # Note: save_crosswalk is imported at module level
962
+
963
+ # Save using API bypass mode (no client needed, skip API operations)
964
+ save_crosswalk = _get_medibot_function('MediBot_Crosswalk_Utils', 'save_crosswalk')
965
+ if not save_crosswalk:
966
+ save_crosswalk = _get_medibot_function('MediBot_Crosswalk_Library', 'save_crosswalk')
967
+
968
+ success = save_crosswalk(None, config, crosswalk, skip_api_operations=True) if save_crosswalk else False
969
+
970
+ if success:
971
+ MediLink_ConfigLoader.log("Successfully saved crosswalk with updated endpoint preferences", level="INFO")
972
+ else:
973
+ MediLink_ConfigLoader.log("Failed to save crosswalk - preferences will be saved during next crosswalk update", level="WARNING")
974
+
975
+ return success
976
+
977
+ except ImportError:
978
+ MediLink_ConfigLoader.log("Could not import MediBot_Crosswalk_Library for saving crosswalk", level="ERROR")
979
+ return False
980
+ except Exception as e:
981
+ MediLink_ConfigLoader.log("Error saving crosswalk: {}".format(e), level="ERROR")
982
+ return False
@@ -1,6 +1,25 @@
1
1
  # MediLink_Decoder.py
2
2
  import os, sys, csv
3
- from MediLink_ConfigLoader import load_configuration, log
3
+
4
+ # Add workspace directory to Python path for MediCafe imports
5
+ current_dir = os.path.dirname(os.path.abspath(__file__))
6
+ workspace_dir = os.path.dirname(current_dir)
7
+ if workspace_dir not in sys.path:
8
+ sys.path.insert(0, workspace_dir)
9
+
10
+ from MediCafe.core_utils import get_shared_config_loader
11
+
12
+ # Get shared config loader
13
+ MediLink_ConfigLoader = get_shared_config_loader()
14
+ if MediLink_ConfigLoader:
15
+ load_configuration = MediLink_ConfigLoader.load_configuration
16
+ log = MediLink_ConfigLoader.log
17
+ else:
18
+ # Fallback functions if config loader is not available
19
+ def load_configuration():
20
+ return {}, {}
21
+ def log(message, level="INFO"):
22
+ print("[{}] {}".format(level, message))
4
23
  from MediLink_Parser import parse_era_content, parse_277_content, parse_277IBR_content, parse_277EBR_content, parse_dpt_content, parse_ebt_content, parse_ibt_content
5
24
 
6
25
  # Define new_fieldnames globally