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.
- MediBot/MediBot.bat +35 -1
- MediBot/MediBot.py +52 -6
- MediCafe/api_core.py +3 -0
- MediCafe/graphql_utils.py +63 -4
- MediCafe/submission_index.py +288 -0
- MediLink/InsuranceTypeService.py +57 -0
- MediLink/MediLink_DataMgmt.py +7 -2
- MediLink/MediLink_Display_Utils.py +28 -6
- MediLink/MediLink_PatientProcessor.py +69 -8
- MediLink/MediLink_Up.py +101 -12
- MediLink/MediLink_main.py +106 -6
- medicafe-0.250812.5.dist-info/METADATA +95 -0
- {medicafe-0.250812.4.dist-info → medicafe-0.250812.5.dist-info}/RECORD +17 -15
- medicafe-0.250812.4.dist-info/METADATA +0 -138
- {medicafe-0.250812.4.dist-info → medicafe-0.250812.5.dist-info}/LICENSE +0 -0
- {medicafe-0.250812.4.dist-info → medicafe-0.250812.5.dist-info}/WHEEL +0 -0
- {medicafe-0.250812.4.dist-info → medicafe-0.250812.5.dist-info}/entry_points.txt +0 -0
- {medicafe-0.250812.4.dist-info → medicafe-0.250812.5.dist-info}/top_level.txt +0 -0
@@ -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
|
MediLink/MediLink_DataMgmt.py
CHANGED
@@ -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
|
-
|
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} {
|
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("-"*
|
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
|
-
#
|
65
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
64
|
-
-
|
65
|
-
|
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
|
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,
|
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
|
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.")
|