medicafe 0.250812.4__py3-none-any.whl → 0.250812.5__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.
@@ -0,0 +1,57 @@
1
+ # InsuranceTypeService.py
2
+ """
3
+ InsuranceTypeService
4
+
5
+ Phase 2 scaffolding for future direct SBR09 extraction from API (GraphQL Super Connector).
6
+
7
+ - NOT ACTIVE: This module provides structure and validation only; integration is disabled
8
+ until the API provides the SBR09 code directly and the key name is known.
9
+ - Usage intent: When feature flag 'use_sbr09_direct_from_api' is enabled and the
10
+ GraphQL API returns an SBR09-compatible code in a known field, call the centralized
11
+ MediCafe.graphql_utils.extract_sbr09_direct() and use the returned value directly
12
+ after minimal validation (no internal mapping).
13
+
14
+ Implementation notes:
15
+ - Extraction and validation are centralized in MediCafe/graphql_utils.py to avoid duplication
16
+ and allow reuse across MediLink and MediBot.
17
+ """
18
+
19
+ try:
20
+ from MediCafe.core_utils import get_shared_config_loader
21
+ except Exception:
22
+ def get_shared_config_loader():
23
+ class _Dummy:
24
+ def load_configuration(self):
25
+ return {}, {}
26
+ def log(self, *args, **kwargs):
27
+ pass
28
+ return _Dummy()
29
+
30
+ ConfigLoader = get_shared_config_loader()
31
+
32
+ # Centralized extractor (preferred)
33
+ try:
34
+ from MediCafe.graphql_utils import extract_sbr09_direct as centralized_extract_sbr09
35
+ except Exception:
36
+ centralized_extract_sbr09 = None # Fallback handled in method
37
+
38
+
39
+ class InsuranceTypeService(object):
40
+ """
41
+ Placeholder service for future direct SBR09 integration via centralized API utilities.
42
+ """
43
+ def __init__(self):
44
+ self.config, _ = ConfigLoader.load_configuration()
45
+
46
+ def get_direct_sbr09_if_available(self, api_transformed_response):
47
+ """Try to extract SBR09 directly from API response; return None if unavailable/invalid."""
48
+ try:
49
+ if centralized_extract_sbr09 is None:
50
+ return None
51
+ return centralized_extract_sbr09(api_transformed_response)
52
+ except Exception as e:
53
+ try:
54
+ ConfigLoader.log("Direct SBR09 extraction error: {}".format(e), level="WARNING")
55
+ except Exception:
56
+ pass
57
+ return None
@@ -843,9 +843,11 @@ def bulk_edit_insurance_types(detailed_patient_data, insurance_options):
843
843
  patient_name = data.get('patient_name', 'Unknown')
844
844
  current_insurance_type = data.get('insurance_type', '12')
845
845
  current_insurance_description = insurance_options.get(current_insurance_type, "Unknown")
846
+ source = data.get('insurance_type_source', '')
847
+ src_disp = 'API' if source == 'API' else ('MAN' if source == 'MANUAL' else 'DEF')
846
848
 
847
- print("({}) {:<25} | Current Ins. Type: {} - {}".format(
848
- patient_id, patient_name, current_insurance_type, current_insurance_description))
849
+ print("({}) {:<25} | Src:{} | Current Ins. Type: {} - {}".format(
850
+ patient_id, patient_name, src_disp, current_insurance_type, current_insurance_description))
849
851
 
850
852
  while True:
851
853
  new_insurance_type = input("Enter new insurance type (or press Enter to keep current): ").strip().upper()
@@ -861,6 +863,7 @@ def bulk_edit_insurance_types(detailed_patient_data, insurance_options):
861
863
  elif new_insurance_type in insurance_options:
862
864
  # Valid insurance type from config
863
865
  data['insurance_type'] = new_insurance_type
866
+ data['insurance_type_source'] = 'MANUAL'
864
867
  break
865
868
 
866
869
  else:
@@ -868,11 +871,13 @@ def bulk_edit_insurance_types(detailed_patient_data, insurance_options):
868
871
  confirm = input("Code '{}' not found in configuration. Use it anyway? (y/n): ".format(new_insurance_type)).strip().lower()
869
872
  if confirm in ['y', 'yes']:
870
873
  data['insurance_type'] = new_insurance_type
874
+ data['insurance_type_source'] = 'MANUAL'
871
875
  break
872
876
  else:
873
877
  print("Invalid insurance type. Please enter a valid code or type 'LIST' to see options.")
874
878
  continue
875
879
 
880
+
876
881
  def review_and_confirm_changes(detailed_patient_data, insurance_options):
877
882
  # Review and confirm changes
878
883
  print("\nReview changes:")
@@ -29,12 +29,21 @@ def display_patient_summaries(detailed_patient_data):
29
29
  Displays summaries of all patients and their suggested endpoints.
30
30
  """
31
31
  print("\nSummary of patient details and suggested endpoint:")
32
- for index, summary in enumerate(detailed_patient_data, start=1):
32
+
33
+ # Sort by insurance_type_source priority for clearer grouping
34
+ priority = {'API': 0, 'MANUAL': 1, 'DEFAULT': 2, 'DEFAULT_FALLBACK': 2}
35
+ def sort_key(item):
36
+ src = item.get('insurance_type_source', '')
37
+ return (priority.get(src, 2), item.get('surgery_date', ''), item.get('patient_name', ''))
38
+ sorted_data = sorted(detailed_patient_data, key=sort_key)
39
+
40
+ for index, summary in enumerate(sorted_data, start=1):
33
41
  try:
34
42
  display_file_summary(index, summary)
35
43
  except KeyError as e:
36
44
  print("Summary at index {} is missing key: {}".format(index, e))
37
45
  print() # add blank line for improved readability.
46
+ print("Legend: Src=API (auto), MAN (manual), DEF (default) | [DUP] indicates a previously submitted matching claim")
38
47
 
39
48
  def display_file_summary(index, summary):
40
49
  # Ensure surgery_date is converted to a datetime object
@@ -42,13 +51,15 @@ def display_file_summary(index, summary):
42
51
 
43
52
  # Add header row if it's the first index
44
53
  if index == 1:
45
- print("{:<3} {:5} {:<10} {:20} {:15} {:3} {:20}".format(
46
- "No.", "Date", "ID", "Name", "Primary Ins.", "IT", "Current Endpoint"
54
+ print("{:<3} {:5} {:<10} {:<20} {:<15} {:<3} {:<5} {:<8} {:<20}".format(
55
+ "No.", "Date", "ID", "Name", "Primary Ins.", "IT", "Src", "Flag", "Current Endpoint"
47
56
  ))
48
- print("-"*82)
57
+ print("-"*100)
49
58
 
50
59
  # Check if insurance_type is available; if not, set a default placeholder (this should already be '12' at this point)
51
60
  insurance_type = summary.get('insurance_type', '--')
61
+ insurance_source = summary.get('insurance_type_source', '')
62
+ duplicate_flag = '[DUP]' if summary.get('duplicate_candidate') else ''
52
63
 
53
64
  # Get the effective endpoint (confirmed > user preference > suggestion > default)
54
65
  effective_endpoint = (summary.get('confirmed_endpoint') or
@@ -61,13 +72,24 @@ def display_file_summary(index, summary):
61
72
  else:
62
73
  insurance_display = insurance_type[:3] if insurance_type else '--'
63
74
 
64
- # Displays the summary of a file.
65
- print("{:02d}. {:5} ({:<8}) {:20} {:15} {:3} {:20}".format(
75
+ # Shorten source for compact display
76
+ if insurance_source in ['DEFAULT_FALLBACK', 'DEFAULT']:
77
+ source_display = 'DEF'
78
+ elif insurance_source == 'MANUAL':
79
+ source_display = 'MAN'
80
+ elif insurance_source == 'API':
81
+ source_display = 'API'
82
+ else:
83
+ source_display = ''
84
+
85
+ print("{:02d}. {:5} ({:<8}) {:<20} {:<15} {:<3} {:<5} {:<8} {:<20}".format(
66
86
  index,
67
87
  surgery_date.strftime("%m-%d"),
68
88
  summary['patient_id'],
69
89
  summary['patient_name'][:20],
70
90
  summary['primary_insurance'][:15],
71
91
  insurance_display,
92
+ source_display,
93
+ duplicate_flag,
72
94
  effective_endpoint[:20])
73
95
  )
@@ -11,6 +11,13 @@ MediLink_ConfigLoader = get_shared_config_loader()
11
11
  import MediLink_DataMgmt
12
12
  import MediLink_Display_Utils
13
13
 
14
+ # Optional import for submission index (duplicate detection)
15
+ try:
16
+ from MediCafe.submission_index import compute_claim_key, find_by_claim_key
17
+ except Exception:
18
+ compute_claim_key = None
19
+ find_by_claim_key = None
20
+
14
21
  # Add parent directory access for MediBot import
15
22
  project_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
16
23
  if project_dir not in sys.path:
@@ -184,7 +191,7 @@ def enrich_with_insurance_type(detailed_patient_data, patient_insurance_type_map
184
191
  data['insurance_type_source'] = 'DEFAULT_FALLBACK'
185
192
 
186
193
  else:
187
- # Legacy mode (preserve existing behavior exactly)
194
+ # Legacy mode (preserve existing behavior exactly) + always set source
188
195
  MediLink_ConfigLoader.log("Using legacy insurance type enrichment", level="INFO")
189
196
  for data in detailed_patient_data:
190
197
  # FIELD NAME CLARIFICATION: Use 'patient_id' field created by extract_and_suggest_endpoint()
@@ -192,16 +199,33 @@ def enrich_with_insurance_type(detailed_patient_data, patient_insurance_type_map
192
199
  patient_id = data.get('patient_id')
193
200
  if patient_id:
194
201
  insurance_type = patient_insurance_type_mapping.get(patient_id, '12') # Default to '12' (PPO/SBR09)
202
+ data['insurance_type'] = insurance_type
203
+ # Mirror enhanced mode semantics for source
204
+ data['insurance_type_source'] = 'MANUAL' if patient_id in patient_insurance_type_mapping else 'DEFAULT'
195
205
  else:
196
206
  # Handle case where patient_id is missing or empty
197
207
  MediLink_ConfigLoader.log("No patient_id found in data record", level="WARNING")
198
208
  insurance_type = '12' # Default when no patient ID available
199
-
200
- data['insurance_type'] = insurance_type
209
+ data['insurance_type'] = insurance_type
210
+ data['insurance_type_source'] = 'DEFAULT_FALLBACK'
201
211
 
202
212
  return detailed_patient_data
203
213
 
204
214
 
215
+ def _normalize_dos_to_iso(mm_dd_yy):
216
+ """Convert date like 'MM-DD-YY' to 'YYYY-MM-DD' safely."""
217
+ try:
218
+ parts = mm_dd_yy.split('-')
219
+ if len(parts) == 3:
220
+ mm, dd, yy = parts
221
+ # Assume 20xx for YY < 50 else 19xx (adjust as needed)
222
+ century = '20' if int(yy) < 50 else '19'
223
+ return "{}-{}-{}".format(century + yy, mm.zfill(2), dd.zfill(2))
224
+ except Exception:
225
+ pass
226
+ return mm_dd_yy
227
+
228
+
205
229
  def extract_and_suggest_endpoint(file_path, config, crosswalk):
206
230
  """
207
231
  Reads a fixed-width file, extracts file details including surgery date, patient ID,
@@ -237,6 +261,13 @@ def extract_and_suggest_endpoint(file_path, config, crosswalk):
237
261
  insurance_to_id = load_insurance_data_from_mains(config)
238
262
  MediLink_ConfigLoader.log("Insurance data loaded from MAINS. {} insurance providers found.".format(len(insurance_to_id)), level="INFO")
239
263
 
264
+ # Resolve receiptsRoot for duplicate detection (optional)
265
+ try:
266
+ medi_cfg = extract_medilink_config(config)
267
+ receipts_root = medi_cfg.get('local_claims_path', None)
268
+ except Exception:
269
+ receipts_root = None
270
+
240
271
  for personal_info, insurance_info, service_info, service_info_2, service_info_3 in MediLink_DataMgmt.read_fixed_width_data(file_path):
241
272
  # Parse reserved 5-line record: 3 active lines + 2 reserved for future expansion
242
273
  try:
@@ -246,6 +277,7 @@ def extract_and_suggest_endpoint(file_path, config, crosswalk):
246
277
  parsed_data = MediLink_DataMgmt.parse_fixed_width_data(personal_info, insurance_info, service_info, service_info_2, service_info_3, cfg_for_parse)
247
278
 
248
279
  primary_insurance = parsed_data.get('INAME')
280
+ primary_procedure_code = parsed_data.get('CODEA')
249
281
 
250
282
  # Retrieve the insurance ID associated with the primary insurance
251
283
  insurance_id = insurance_to_id.get(primary_insurance)
@@ -256,7 +288,6 @@ def extract_and_suggest_endpoint(file_path, config, crosswalk):
256
288
  if insurance_id:
257
289
  for payer_id, payer_data in crosswalk.get('payer_id', {}).items():
258
290
  medisoft_ids = [str(id) for id in payer_data.get('medisoft_id', [])]
259
- # MediLink_ConfigLoader.log("Payer ID: {}, Medisoft IDs: {}".format(payer_id, medisoft_ids))
260
291
  if str(insurance_id) in medisoft_ids:
261
292
  payer_ids.append(payer_id)
262
293
  if payer_ids:
@@ -283,22 +314,52 @@ def extract_and_suggest_endpoint(file_path, config, crosswalk):
283
314
  else:
284
315
  MediLink_ConfigLoader.log("No suggested endpoint found for payer IDs: {}".format(payer_ids))
285
316
 
317
+ # Normalize DOS for keying
318
+ raw_dos = parsed_data.get('DATE')
319
+ iso_dos = _normalize_dos_to_iso(raw_dos) if raw_dos else ''
320
+
286
321
  # Enrich detailed patient data with additional information and suggested endpoint
287
322
  detailed_data = parsed_data.copy() # Copy parsed_data to avoid modifying the original dictionary
288
323
  detailed_data.update({
289
324
  'file_path': file_path,
290
- # CRITICAL FIELD MAPPING: 'CHART' field from fixed-width file becomes 'patient_id'
291
- # This is the field that enrich_with_insurance_type() will use
292
- 'patient_id': parsed_data.get('CHART'), # <- This is the key field mapping for MediLink flow
325
+ 'patient_id': parsed_data.get('CHART'),
293
326
  'surgery_date': parsed_data.get('DATE'),
327
+ 'surgery_date_iso': iso_dos,
294
328
  'patient_name': ' '.join([parsed_data.get(key, '') for key in ['FIRST', 'MIDDLE', 'LAST']]),
295
329
  'amount': parsed_data.get('AMOUNT'),
296
330
  'primary_insurance': primary_insurance,
331
+ 'primary_procedure_code': primary_procedure_code,
297
332
  'suggested_endpoint': suggested_endpoint
298
333
  })
334
+
335
+ # Compute claim_key (optional)
336
+ claim_key = None
337
+ try:
338
+ if compute_claim_key:
339
+ claim_key = compute_claim_key(
340
+ detailed_data.get('patient_id', ''),
341
+ '', # payer_id not reliably known here
342
+ detailed_data.get('primary_insurance', ''),
343
+ detailed_data.get('surgery_date_iso', ''),
344
+ detailed_data.get('primary_procedure_code', '')
345
+ )
346
+ detailed_data['claim_key'] = claim_key
347
+ except Exception:
348
+ pass
349
+
350
+ # Duplicate candidate flag (optional upstream detection)
351
+ try:
352
+ if find_by_claim_key and receipts_root and claim_key:
353
+ existing = find_by_claim_key(receipts_root, claim_key)
354
+ detailed_data['duplicate_candidate'] = bool(existing)
355
+ else:
356
+ detailed_data['duplicate_candidate'] = False
357
+ except Exception:
358
+ detailed_data['duplicate_candidate'] = False
359
+
299
360
  detailed_patient_data.append(detailed_data)
300
361
 
301
- # Return only the enriched detailed patient data, eliminating the need for a separate summary list
362
+ # Return only the enriched detailed patient data
302
363
  return detailed_patient_data
303
364
 
304
365
 
MediLink/MediLink_Up.py CHANGED
@@ -1,4 +1,15 @@
1
1
  # MediLink_Up.py
2
+ """
3
+ Notes:
4
+ - Duplicate detection relies on a JSONL index under MediLink_Config['receiptsRoot'].
5
+ If 'receiptsRoot' is missing, duplicate checks are skipped with no errors.
6
+ - The claim_key used for deconfliction is practical rather than cryptographic:
7
+ it combines (patient_id if available, else ''), (payer_id or primary_insurance), DOS, and a simple service/procedure indicator.
8
+ In this file-level flow, we approximate with primary_insurance + DOS + file basename for pre-checks.
9
+ Upstream detection now also flags duplicates per patient record using procedure code when available.
10
+ - We do NOT write to the index until a successful submission occurs.
11
+ - All I/O uses ASCII-safe defaults.
12
+ """
2
13
  from datetime import datetime
3
14
  import os, re, subprocess, traceback
4
15
  try:
@@ -22,6 +33,18 @@ try:
22
33
  except ImportError:
23
34
  api_core = None
24
35
 
36
+ # Import submission index helpers (XP-safe JSONL)
37
+ try:
38
+ from MediCafe.submission_index import (
39
+ compute_claim_key,
40
+ find_by_claim_key,
41
+ append_submission_record
42
+ )
43
+ except Exception:
44
+ compute_claim_key = None
45
+ find_by_claim_key = None
46
+ append_submission_record = None
47
+
25
48
  # Pre-compile regex patterns for better performance
26
49
  GS_PATTERN = re.compile(r'GS\*HC\*[^*]*\*[^*]*\*([0-9]{8})\*([0-9]{4})')
27
50
  SE_PATTERN = re.compile(r'SE\*\d+\*\d{4}~')
@@ -60,12 +83,10 @@ def submit_claims(detailed_patient_data_grouped_by_endpoint, config, crosswalk):
60
83
  """
61
84
  Submits claims for each endpoint, either via WinSCP or API, based on configuration settings.
62
85
 
63
- Parameters:
64
- - detailed_patient_data_grouped_by_endpoint: Dictionary with endpoints as keys and lists of patient data as values.
65
- - config: Configuration settings loaded from a JSON file.
66
-
67
- Returns:
68
- - None
86
+ Deconfliction (XP-safe):
87
+ - If JSONL index helpers are available and receiptsRoot is configured, compute a claim_key per 837p file
88
+ and skip submit if index already contains that key (duplicate protection).
89
+ - After a successful submission, append an index record.
69
90
  """
70
91
  # Normalize configuration for safe nested access
71
92
  if not isinstance(config, dict):
@@ -73,7 +94,6 @@ def submit_claims(detailed_patient_data_grouped_by_endpoint, config, crosswalk):
73
94
  config, _ = load_configuration()
74
95
  except Exception:
75
96
  config = {}
76
- # Ensure cfg is always a dict to avoid NoneType.get errors
77
97
  if isinstance(config, dict):
78
98
  cfg_candidate = config.get('MediLink_Config')
79
99
  if isinstance(cfg_candidate, dict):
@@ -83,6 +103,9 @@ def submit_claims(detailed_patient_data_grouped_by_endpoint, config, crosswalk):
83
103
  else:
84
104
  cfg = {}
85
105
 
106
+ # Resolve receipts folder for index (use same path as receipts)
107
+ receipts_root = cfg.get('local_claims_path', None)
108
+
86
109
  # Accumulate submission results
87
110
  submission_results = {}
88
111
 
@@ -113,10 +136,8 @@ def submit_claims(detailed_patient_data_grouped_by_endpoint, config, crosswalk):
113
136
  method = cfg.get('endpoints', {}).get(endpoint, {}).get('submission_method', 'winscp')
114
137
  except Exception as e:
115
138
  log("[submit_claims] Error deriving submission method for endpoint {}: {}".format(endpoint, e), level="ERROR")
116
- # Absolute fallback if cfg was unexpectedly not a dict
117
139
  method = 'winscp'
118
140
 
119
- # Attempt submission to each endpoint
120
141
  if True: #confirm_transmission({endpoint: patients_data}): # Confirm transmission to each endpoint with detailed overview
121
142
  if check_internet_connection():
122
143
  client = get_api_client()
@@ -157,13 +178,44 @@ def submit_claims(detailed_patient_data_grouped_by_endpoint, config, crosswalk):
157
178
  pass
158
179
  raise
159
180
  if converted_files:
160
- if method == 'winscp':
181
+ # Deconfliction pre-check per file if helpers available
182
+ filtered_files = []
183
+ for file_path in converted_files:
184
+ if compute_claim_key and find_by_claim_key and receipts_root:
185
+ try:
186
+ # Compute a simple service hash from file path (can be improved later)
187
+ service_hash = os.path.basename(file_path)
188
+ # Attempt to parse minimal patient_id and DOS from filename if available
189
+ # For now, rely on patient data embedded in file content via parse_837p_file
190
+ patients, _ = parse_837p_file(file_path)
191
+ # If we cannot compute a stable key, skip deconflict
192
+ if patients:
193
+ # Use first patient for keying; future improvement: per-service keys
194
+ p = patients[0]
195
+ patient_id = "" # unknown at this stage (facesheet may not contain chart)
196
+ payer_id = ""
197
+ primary_insurance = p.get('insurance_name', '')
198
+ dos = p.get('service_date', '')
199
+ claim_key = compute_claim_key(patient_id, payer_id, primary_insurance, dos, service_hash)
200
+ existing = find_by_claim_key(receipts_root, claim_key)
201
+ if existing:
202
+ print("Duplicate detected; skipping file: {}".format(file_path))
203
+ continue
204
+ except Exception:
205
+ # Fail open (do not block submission)
206
+ pass
207
+ filtered_files.append(file_path)
208
+
209
+ if not filtered_files:
210
+ print("All files skipped as duplicates for endpoint {}.".format(endpoint))
211
+ submission_results[endpoint] = {}
212
+ elif method == 'winscp':
161
213
  # Transmit files via WinSCP
162
214
  try:
163
215
  operation_type = "upload"
164
216
  endpoint_cfg = cfg.get('endpoints', {}).get(endpoint, {})
165
217
  local_claims_path = cfg.get('local_claims_path', '.')
166
- transmission_result = operate_winscp(operation_type, converted_files, endpoint_cfg, local_claims_path, config)
218
+ transmission_result = operate_winscp(operation_type, filtered_files, endpoint_cfg, local_claims_path, config)
167
219
  success_dict = handle_transmission_result(transmission_result, config, operation_type, method)
168
220
  submission_results[endpoint] = success_dict
169
221
  except FileNotFoundError as e:
@@ -179,7 +231,7 @@ def submit_claims(detailed_patient_data_grouped_by_endpoint, config, crosswalk):
179
231
  # Transmit files via API
180
232
  try:
181
233
  api_responses = []
182
- for file_path in converted_files:
234
+ for file_path in filtered_files:
183
235
  with open(file_path, 'r') as file:
184
236
  # Optimize string operations by doing replacements in one pass
185
237
  x12_request_data = file.read().replace('\n', '').replace('\r', '').strip()
@@ -217,6 +269,43 @@ def submit_claims(detailed_patient_data_grouped_by_endpoint, config, crosswalk):
217
269
 
218
270
  # Build and display receipt
219
271
  build_and_display_receipt(submission_results, config)
272
+
273
+ # Append index records for successes
274
+ try:
275
+ if append_submission_record and isinstance(submission_results, dict):
276
+ # Resolve receipts root
277
+ if isinstance(config, dict):
278
+ _cfg2 = config.get('MediLink_Config')
279
+ cfg2 = _cfg2 if isinstance(_cfg2, dict) else config
280
+ else:
281
+ cfg2 = {}
282
+ receipts_root2 = cfg2.get('local_claims_path', None)
283
+ if receipts_root2:
284
+ for endpoint, files in submission_results.items():
285
+ for file_path, result in files.items():
286
+ try:
287
+ status, message = result
288
+ if status:
289
+ patients, submitted_at = parse_837p_file(file_path)
290
+ # Take first patient for keying; improve later for per-service handling
291
+ p = patients[0] if patients else {}
292
+ claim_key = compute_claim_key("", "", p.get('insurance_name', ''), p.get('service_date', ''), os.path.basename(file_path))
293
+ record = {
294
+ 'claim_key': claim_key,
295
+ 'patient_id': "",
296
+ 'payer_id': "",
297
+ 'primary_insurance': p.get('insurance_name', ''),
298
+ 'dos': p.get('service_date', ''),
299
+ 'endpoint': endpoint,
300
+ 'submitted_at': submitted_at or datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
301
+ 'receipt_file': os.path.basename(file_path),
302
+ 'status': 'success'
303
+ }
304
+ append_submission_record(receipts_root2, record)
305
+ except Exception:
306
+ continue
307
+ except Exception:
308
+ pass
220
309
 
221
310
  print("Claim submission process completed.\n")
222
311
 
MediLink/MediLink_main.py CHANGED
@@ -54,6 +54,35 @@ if PERFORMANCE_LOGGING:
54
54
  # - XP note: default to console prompts; optional UI later.
55
55
  # This already happens when MediLink is opened.
56
56
 
57
+ def _tools_menu(config, medi):
58
+ """Low-use maintenance tools submenu."""
59
+ while True:
60
+ print("\nMaintenance Tools:")
61
+ options = [
62
+ "Rebuild submission index now",
63
+ "Back"
64
+ ]
65
+ MediLink_UI.display_menu(options)
66
+ choice = MediLink_UI.get_user_choice().strip()
67
+ if choice == '1':
68
+ receipts_root = medi.get('local_claims_path', None)
69
+ if not receipts_root:
70
+ print("No receipts folder configured (local_claims_path missing).")
71
+ continue
72
+ try:
73
+ from MediCafe.submission_index import build_initial_index
74
+ receipts_root = os.path.normpath(receipts_root)
75
+ print("Rebuilding submission index... (this may take a while)")
76
+ count = build_initial_index(receipts_root)
77
+ print("Index rebuild complete. Indexed {} records.".format(count))
78
+ except Exception as e:
79
+ print("Index rebuild error: {}".format(e))
80
+ elif choice == '2':
81
+ break
82
+ else:
83
+ MediLink_UI.display_invalid_choice()
84
+
85
+
57
86
  def main_menu():
58
87
  """
59
88
  Initializes the main menu loop and handles the overall program flow,
@@ -125,6 +154,16 @@ def main_menu():
125
154
  if PERFORMANCE_LOGGING:
126
155
  print("Path normalization completed in {:.2f} seconds".format(path_norm_end - path_norm_start))
127
156
 
157
+ # NEW: Submission index upkeep (XP-safe, inline)
158
+ try:
159
+ receipts_root = medi.get('local_claims_path', None)
160
+ if receipts_root:
161
+ from MediCafe.submission_index import ensure_submission_index
162
+ ensure_submission_index(os.path.normpath(receipts_root))
163
+ except Exception:
164
+ # Silent failure - do not block menu
165
+ pass
166
+
128
167
  # Detect files and determine if a new file is flagged.
129
168
  file_detect_start = time.time()
130
169
  if PERFORMANCE_LOGGING:
@@ -142,7 +181,7 @@ def main_menu():
142
181
 
143
182
  while True:
144
183
  # Define static menu options for consistent numbering
145
- options = ["Check for new remittances", "Submit claims", "Exit"]
184
+ options = ["Check for new remittances", "Submit claims", "Exit", "Tools"]
146
185
 
147
186
  # Display the menu options.
148
187
  menu_display_start = time.time()
@@ -193,6 +232,22 @@ def main_menu():
193
232
  elif choice == '3':
194
233
  MediLink_UI.display_exit_message()
195
234
  break
235
+ elif choice == '4':
236
+ _tools_menu(config, medi)
237
+ elif choice.lower() == 'tools:index':
238
+ # Optional maintenance: rebuild submission index now (synchronous)
239
+ try:
240
+ receipts_root = medi.get('local_claims_path', None)
241
+ if not receipts_root:
242
+ print("No receipts folder configured.")
243
+ continue
244
+ from MediCafe.submission_index import build_initial_index
245
+ receipts_root = os.path.normpath(receipts_root)
246
+ print("Rebuilding submission index... (this may take a while)")
247
+ count = build_initial_index(receipts_root)
248
+ print("Index rebuild complete. Indexed {} records.".format(count))
249
+ except Exception as e:
250
+ print("Index rebuild error: {}".format(e))
196
251
  else:
197
252
  # Display an error message if the user's choice does not match any valid option.
198
253
  MediLink_UI.display_invalid_choice()
@@ -229,6 +284,56 @@ def handle_submission(detailed_patient_data, config, crosswalk):
229
284
  # Update crosswalk reference if it was modified
230
285
  if updated_crosswalk:
231
286
  crosswalk = updated_crosswalk
287
+
288
+ # Upstream duplicate prompt: flag and allow user to exclude duplicates before submission
289
+ try:
290
+ medi_cfg = extract_medilink_config(config)
291
+ receipts_root = medi_cfg.get('local_claims_path', None)
292
+ if receipts_root:
293
+ try:
294
+ from MediCafe.submission_index import compute_claim_key, find_by_claim_key
295
+ except Exception:
296
+ compute_claim_key = None
297
+ find_by_claim_key = None
298
+ if compute_claim_key and find_by_claim_key:
299
+ for data in adjusted_data:
300
+ try:
301
+ # Use precomputed claim_key when available, else build it
302
+ claim_key = data.get('claim_key', None)
303
+ if not claim_key:
304
+ claim_key = compute_claim_key(
305
+ data.get('patient_id', ''),
306
+ '',
307
+ data.get('primary_insurance', ''),
308
+ data.get('surgery_date_iso', data.get('surgery_date', '')),
309
+ data.get('primary_procedure_code', '')
310
+ )
311
+ existing = find_by_claim_key(receipts_root, claim_key) if claim_key else None
312
+ if existing:
313
+ # Show informative prompt
314
+ print("\nPotential duplicate detected:")
315
+ print("- Patient: {} ({})".format(data.get('patient_name', ''), data.get('patient_id', '')))
316
+ print("- DOS: {} | Insurance: {} | Proc: {}".format(
317
+ data.get('surgery_date', ''),
318
+ data.get('primary_insurance', ''),
319
+ data.get('primary_procedure_code', '')
320
+ ))
321
+ print("- Prior submission: {} via {} (receipt: {})".format(
322
+ existing.get('submitted_at', 'unknown'),
323
+ existing.get('endpoint', 'unknown'),
324
+ existing.get('receipt_file', 'unknown')
325
+ ))
326
+ ans = input("Submit anyway? (Y/N): ").strip().lower()
327
+ if ans not in ['y', 'yes']:
328
+ data['exclude_from_submission'] = True
329
+ except Exception:
330
+ # Do not block flow on errors
331
+ continue
332
+ except Exception:
333
+ pass
334
+
335
+ # Filter out excluded items prior to confirmation and submission
336
+ adjusted_data = [d for d in adjusted_data if not d.get('exclude_from_submission')]
232
337
 
233
338
  # Confirm all remaining suggested endpoints.
234
339
  confirmed_data = MediLink_DataMgmt.confirm_all_suggested_endpoints(adjusted_data)
@@ -240,11 +345,6 @@ def handle_submission(detailed_patient_data, config, crosswalk):
240
345
  if MediLink_Up.check_internet_connection():
241
346
  # Submit claims if internet connectivity is confirmed.
242
347
  _ = MediLink_Up.submit_claims(organized_data, config, crosswalk)
243
- # TODO submit_claims will have a receipt return in the future.
244
- # PLAN: submit_claims should return a structure like:
245
- # {'endpoint': ep, 'files': [{'path': p, 'status': 'ok'|'error', 'receipt_id': '...', 'timestamp': ...}], 'errors': [...]}
246
- # Callers can log and optionally display the receipt IDs or open an acknowledgment view.
247
- # Backward-compatibility: if None/empty is returned, proceed as today.
248
348
  else:
249
349
  # Notify the user of an internet connection error.
250
350
  print("Internet connection error. Please ensure you're connected and try again.")