medicafe 0.250812.6__py3-none-any.whl → 0.250813.1__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/update_medicafe.py +73 -61
- MediCafe/api_core.py +39 -12
- MediCafe/submission_index.py +44 -0
- MediLink/MediLink_DataMgmt.py +74 -43
- MediLink/MediLink_Decoder.py +38 -9
- MediLink/MediLink_Down.py +358 -21
- MediLink/MediLink_Parser.py +80 -1
- MediLink/MediLink_main.py +101 -1
- {medicafe-0.250812.6.dist-info → medicafe-0.250813.1.dist-info}/METADATA +1 -1
- {medicafe-0.250812.6.dist-info → medicafe-0.250813.1.dist-info}/RECORD +14 -14
- {medicafe-0.250812.6.dist-info → medicafe-0.250813.1.dist-info}/LICENSE +0 -0
- {medicafe-0.250812.6.dist-info → medicafe-0.250813.1.dist-info}/WHEEL +0 -0
- {medicafe-0.250812.6.dist-info → medicafe-0.250813.1.dist-info}/entry_points.txt +0 -0
- {medicafe-0.250812.6.dist-info → medicafe-0.250813.1.dist-info}/top_level.txt +0 -0
MediBot/update_medicafe.py
CHANGED
@@ -292,7 +292,7 @@ def compare_versions(version1, version2):
|
|
292
292
|
|
293
293
|
def upgrade_package(package, retries=4, delay=2, target_version=None): # Updated retries to 4
|
294
294
|
"""
|
295
|
-
Attempts to upgrade the package multiple times with
|
295
|
+
Attempts to upgrade the package multiple times with escalating techniques.
|
296
296
|
"""
|
297
297
|
if not check_internet_connection():
|
298
298
|
print_status("No internet connection detected. Please check your internet connection and try again.", "ERROR")
|
@@ -302,82 +302,94 @@ def upgrade_package(package, retries=4, delay=2, target_version=None): # Update
|
|
302
302
|
if target_version:
|
303
303
|
print("Pinned target version: {}".format(target_version))
|
304
304
|
|
305
|
-
|
306
|
-
|
305
|
+
def get_installed_version_fresh(package):
|
306
|
+
"""Get installed version using a fresh subprocess to avoid pkg_resources cache issues."""
|
307
|
+
try:
|
308
|
+
process = subprocess.Popen(
|
309
|
+
[sys.executable, '-m', 'pip', 'show', package],
|
310
|
+
stdout=subprocess.PIPE,
|
311
|
+
stderr=subprocess.PIPE
|
312
|
+
)
|
313
|
+
stdout, stderr = process.communicate()
|
314
|
+
if process.returncode == 0:
|
315
|
+
for line in stdout.decode().splitlines():
|
316
|
+
if line.startswith("Version:"):
|
317
|
+
return line.split(":", 1)[1].strip()
|
318
|
+
return None
|
319
|
+
except Exception as e:
|
320
|
+
print("Warning: Could not get fresh version: {}".format(e))
|
321
|
+
return None
|
322
|
+
|
323
|
+
def try_upgrade_with_strategy(attempt, strategy_name, cmd_args):
|
324
|
+
"""Try upgrade with specific strategy and return success status."""
|
325
|
+
print("Attempt {}/{}: Using {} strategy...".format(attempt, retries, strategy_name))
|
307
326
|
|
308
|
-
# Use a more compatible approach for Python 3.4
|
309
|
-
# Try with --no-deps first to avoid dependency resolution issues
|
310
327
|
pkg_spec = package
|
311
328
|
if target_version:
|
312
329
|
pkg_spec = "{}=={}".format(package, target_version)
|
313
330
|
|
314
|
-
cmd = [
|
315
|
-
sys.executable, '-m', 'pip', 'install', '--upgrade',
|
316
|
-
'--no-deps', '--no-cache-dir', '--disable-pip-version-check', '-q', pkg_spec
|
317
|
-
]
|
331
|
+
cmd = [sys.executable, '-m', 'pip', 'install'] + cmd_args + [pkg_spec]
|
318
332
|
|
319
|
-
print("Using pip upgrade with --no-deps and --no-cache-dir")
|
320
333
|
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
321
334
|
stdout, stderr = process.communicate()
|
322
335
|
|
323
336
|
if process.returncode == 0:
|
324
337
|
print(stdout.decode().strip())
|
325
|
-
|
338
|
+
# Add delay to allow file system to settle
|
339
|
+
time.sleep(1)
|
340
|
+
new_version = get_installed_version_fresh(package)
|
326
341
|
expected_version = target_version or get_latest_version(package)
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
else:
|
331
|
-
print_status("Attempt {}: Upgrade succeeded!".format(attempt), "SUCCESS")
|
332
|
-
time.sleep(delay)
|
342
|
+
|
343
|
+
if expected_version and new_version and compare_versions(new_version, expected_version) >= 0:
|
344
|
+
print_status("Attempt {}: Upgrade succeeded with {}!".format(attempt, strategy_name), "SUCCESS")
|
333
345
|
return True
|
334
346
|
else:
|
335
|
-
print_status("Upgrade incomplete. Current version: {} Expected at least: {}".format(
|
336
|
-
|
337
|
-
|
338
|
-
try:
|
339
|
-
time.sleep(delay + (random.random() * 0.5))
|
340
|
-
except Exception:
|
341
|
-
time.sleep(delay)
|
347
|
+
print_status("Upgrade incomplete. Current version: {} Expected at least: {}".format(
|
348
|
+
new_version or "unknown", expected_version), "WARNING")
|
349
|
+
return False
|
342
350
|
else:
|
343
351
|
print(stderr.decode().strip())
|
344
|
-
print_status("Attempt {}: Upgrade failed with
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
352
|
+
print_status("Attempt {}: Upgrade failed with {}.".format(attempt, strategy_name), "WARNING")
|
353
|
+
return False
|
354
|
+
|
355
|
+
# Define escalation strategies for each attempt
|
356
|
+
strategies = {
|
357
|
+
1: [
|
358
|
+
("Gentle Upgrade", ['--upgrade', '--no-deps', '--no-cache-dir', '--disable-pip-version-check', '-q']),
|
359
|
+
("Force Reinstall", ['--upgrade', '--force-reinstall', '--no-cache-dir', '--disable-pip-version-check', '-q'])
|
360
|
+
],
|
361
|
+
2: [
|
362
|
+
("Clean Install", ['--upgrade', '--force-reinstall', '--no-cache-dir', '--disable-pip-version-check', '--ignore-installed', '-q']),
|
363
|
+
("User Install", ['--upgrade', '--user', '--no-cache-dir', '--disable-pip-version-check', '-q'])
|
364
|
+
],
|
365
|
+
3: [
|
366
|
+
("Aggressive Clean", ['--upgrade', '--force-reinstall', '--no-cache-dir', '--disable-pip-version-check', '--ignore-installed', '--no-deps', '-q']),
|
367
|
+
("Pre-download", ['--upgrade', '--force-reinstall', '--no-cache-dir', '--disable-pip-version-check', '--pre', '-q'])
|
368
|
+
],
|
369
|
+
4: [
|
370
|
+
("Nuclear Option", ['--upgrade', '--force-reinstall', '--no-cache-dir', '--disable-pip-version-check', '--ignore-installed', '--no-deps', '--pre', '-q']),
|
371
|
+
("Last Resort", ['--upgrade', '--force-reinstall', '--no-cache-dir', '--disable-pip-version-check', '--ignore-installed', '--no-deps', '--pre', '--user', '-q'])
|
372
|
+
]
|
373
|
+
}
|
374
|
+
|
375
|
+
for attempt in range(1, retries + 1):
|
376
|
+
print("Attempt {}/{} to upgrade {}...".format(attempt, retries, package))
|
377
|
+
|
378
|
+
# Try each strategy for this attempt
|
379
|
+
for strategy_name, cmd_args in strategies[attempt]:
|
380
|
+
if try_upgrade_with_strategy(attempt, strategy_name, cmd_args):
|
381
|
+
time.sleep(delay)
|
382
|
+
return True
|
383
|
+
|
384
|
+
# If we get here, all strategies for this attempt failed
|
385
|
+
if attempt < retries:
|
386
|
+
# Escalating delays: 2s, 3s, 5s
|
387
|
+
current_delay = delay + (attempt - 1)
|
388
|
+
print("All strategies failed for attempt {}. Retrying in {} seconds...".format(attempt, current_delay))
|
389
|
+
try:
|
390
|
+
time.sleep(current_delay + (random.random() * 1.0))
|
391
|
+
except Exception:
|
392
|
+
time.sleep(current_delay)
|
381
393
|
|
382
394
|
print_status("All upgrade attempts failed.", "ERROR")
|
383
395
|
return False
|
MediCafe/api_core.py
CHANGED
@@ -983,18 +983,18 @@ def is_test_mode(client, body, endpoint_type):
|
|
983
983
|
def submit_uhc_claim(client, x12_request_data):
|
984
984
|
"""
|
985
985
|
Submits a UHC claim and retrieves the claim acknowledgement details.
|
986
|
-
|
986
|
+
|
987
987
|
This function first submits the claim using the provided x12 837p data. If the client is in Test Mode,
|
988
988
|
it returns a simulated response. If Test Mode is not enabled, it submits the claim and then retrieves
|
989
989
|
the claim acknowledgement details using the transaction ID from the initial response.
|
990
|
-
|
990
|
+
|
991
991
|
NOTE: This function uses endpoints that may not be available in the new swagger version:
|
992
992
|
- /Claims/api/claim-submission/v1 (claim submission)
|
993
993
|
- /Claims/api/claim-details/v1 (claim acknowledgement)
|
994
994
|
|
995
995
|
If these endpoints are deprecated in the new swagger, this function will need to be updated
|
996
996
|
to use the new available endpoints.
|
997
|
-
|
997
|
+
|
998
998
|
:param client: An instance of APIClient
|
999
999
|
:param x12_request_data: The x12 837p data as a string
|
1000
1000
|
:return: The final response containing the claim acknowledgement details or a dummy response if in Test Mode
|
@@ -1014,24 +1014,24 @@ def submit_uhc_claim(client, x12_request_data):
|
|
1014
1014
|
endpoints = medi.get('endpoints', {})
|
1015
1015
|
claim_submission_url = endpoints.get(endpoint_name, {}).get('additional_endpoints', {}).get('claim_submission', '')
|
1016
1016
|
claim_details_url = endpoints.get(endpoint_name, {}).get('additional_endpoints', {}).get('claim_details', '')
|
1017
|
-
|
1017
|
+
|
1018
1018
|
MediLink_ConfigLoader.log("Claim Submission URL: {}".format(claim_submission_url), level="INFO")
|
1019
1019
|
MediLink_ConfigLoader.log("Claim Details URL: {}".format(claim_details_url), level="INFO")
|
1020
|
-
|
1020
|
+
|
1021
1021
|
# Headers for the request
|
1022
1022
|
headers = {'Content-Type': 'application/json'}
|
1023
|
-
|
1023
|
+
|
1024
1024
|
# Request body for claim submission
|
1025
1025
|
claim_body = {'x12RequestData': x12_request_data}
|
1026
|
-
|
1026
|
+
|
1027
1027
|
MediLink_ConfigLoader.log("Claim Body Keys: {}".format(list(claim_body.keys())), level="INFO")
|
1028
1028
|
MediLink_ConfigLoader.log("Headers: {}".format(json.dumps(headers, indent=2)), level="INFO")
|
1029
|
-
|
1029
|
+
|
1030
1030
|
# Check if Test Mode is enabled and return simulated response if so
|
1031
1031
|
test_mode_response = is_test_mode(client, claim_body, 'claim_submission')
|
1032
1032
|
if test_mode_response:
|
1033
1033
|
return test_mode_response
|
1034
|
-
|
1034
|
+
|
1035
1035
|
# Make the API call to submit the claim
|
1036
1036
|
try:
|
1037
1037
|
MediLink_ConfigLoader.log("Making claim submission API call...", level="INFO")
|
@@ -1047,16 +1047,43 @@ def submit_uhc_claim(client, x12_request_data):
|
|
1047
1047
|
|
1048
1048
|
# Prepare the request body for the claim acknowledgement retrieval
|
1049
1049
|
acknowledgement_body = {'transactionId': transaction_id}
|
1050
|
-
|
1050
|
+
|
1051
1051
|
# Check if Test Mode is enabled and return simulated response if so
|
1052
1052
|
test_mode_response = is_test_mode(client, acknowledgement_body, 'claim_details')
|
1053
1053
|
if test_mode_response:
|
1054
1054
|
return test_mode_response
|
1055
|
-
|
1055
|
+
|
1056
1056
|
# Make the API call to retrieve the claim acknowledgement details
|
1057
1057
|
acknowledgement_response = client.make_api_call(endpoint_name, 'POST', claim_details_url, data=acknowledgement_body, headers=headers)
|
1058
|
+
|
1059
|
+
# Persist as unified ack event (best-effort)
|
1060
|
+
try:
|
1061
|
+
from MediCafe.submission_index import append_ack_event, ensure_submission_index
|
1062
|
+
cfg, _ = MediLink_ConfigLoader.load_configuration()
|
1063
|
+
receipts_root = extract_medilink_config(cfg).get('local_claims_path', None)
|
1064
|
+
if receipts_root:
|
1065
|
+
ensure_submission_index(receipts_root)
|
1066
|
+
status_text = ''
|
1067
|
+
try:
|
1068
|
+
# Attempt to pull a readable status from the response
|
1069
|
+
status_text = acknowledgement_response.get('status') or acknowledgement_response.get('message') or ''
|
1070
|
+
except Exception:
|
1071
|
+
status_text = ''
|
1072
|
+
append_ack_event(
|
1073
|
+
receipts_root,
|
1074
|
+
'', # claim_key unknown here
|
1075
|
+
status_text,
|
1076
|
+
'API-277',
|
1077
|
+
'uhcapi',
|
1078
|
+
{'transactionId': transaction_id},
|
1079
|
+
'api_ack',
|
1080
|
+
int(time.time())
|
1081
|
+
)
|
1082
|
+
except Exception:
|
1083
|
+
pass
|
1084
|
+
|
1058
1085
|
return acknowledgement_response
|
1059
|
-
|
1086
|
+
|
1060
1087
|
except Exception as e:
|
1061
1088
|
print("Error during claim processing: {}".format(e))
|
1062
1089
|
raise
|
MediCafe/submission_index.py
CHANGED
@@ -22,6 +22,9 @@ META_FILENAME = 'submission_index_meta.json'
|
|
22
22
|
INDEX_FILENAME = 'submission_index.jsonl'
|
23
23
|
LOCK_FILENAME = 'submission_index.lock'
|
24
24
|
|
25
|
+
# New: ack field keys for richer timeline entries
|
26
|
+
ACK_FIELDS = ['ack_type', 'ack_timestamp', 'control_ids', 'source', 'file_name']
|
27
|
+
|
25
28
|
|
26
29
|
def build_initial_index(receipts_root, lookback_days=200):
|
27
30
|
"""
|
@@ -123,6 +126,47 @@ def compute_claim_key(patient_id, payer_id, primary_insurance, date_of_service,
|
|
123
126
|
])
|
124
127
|
|
125
128
|
|
129
|
+
def append_ack_event(receipts_root, claim_key, status_text, ack_type, file_name, control_ids, source, ack_timestamp=None):
|
130
|
+
"""
|
131
|
+
Append a lightweight ack/timeline event to the index. XP/Py3.4/ASCII-safe.
|
132
|
+
- claim_key may be empty if unknown. Caller should pass when available.
|
133
|
+
- control_ids is a dict with optional ISA/GS/ST/TRN or transactionId.
|
134
|
+
"""
|
135
|
+
try:
|
136
|
+
_ensure_files_exist(receipts_root)
|
137
|
+
event = {
|
138
|
+
'claim_key': claim_key or '',
|
139
|
+
'patient_id': '',
|
140
|
+
'payer_id': '',
|
141
|
+
'primary_insurance': '',
|
142
|
+
'dos': '',
|
143
|
+
'endpoint': source or 'download_ack',
|
144
|
+
'submitted_at': '',
|
145
|
+
'receipt_file': file_name or '',
|
146
|
+
'status': status_text or '',
|
147
|
+
'notes': 'ack event',
|
148
|
+
}
|
149
|
+
# Attach ack fields with basic validation
|
150
|
+
try:
|
151
|
+
event['ack_type'] = ack_type or ''
|
152
|
+
event['ack_timestamp'] = ack_timestamp or int(time.time())
|
153
|
+
event['control_ids'] = control_ids or {}
|
154
|
+
event['source'] = source or ''
|
155
|
+
event['file_name'] = file_name or ''
|
156
|
+
except Exception:
|
157
|
+
pass
|
158
|
+
path = _index_path(receipts_root)
|
159
|
+
line = json.dumps(event)
|
160
|
+
f = open(path, 'a')
|
161
|
+
try:
|
162
|
+
f.write(line)
|
163
|
+
f.write("\n")
|
164
|
+
finally:
|
165
|
+
f.close()
|
166
|
+
except Exception:
|
167
|
+
pass
|
168
|
+
|
169
|
+
|
126
170
|
# ------------------------- ASCII-safe meta/lock helpers -----------------------
|
127
171
|
|
128
172
|
def _meta_path(root_dir):
|
MediLink/MediLink_DataMgmt.py
CHANGED
@@ -330,7 +330,12 @@ def operate_winscp(operation_type, files, endpoint_config, local_storage_path, c
|
|
330
330
|
local_storage_path = validate_local_storage_path(local_storage_path, config)
|
331
331
|
|
332
332
|
remote_directory = get_remote_directory(endpoint_config, operation_type)
|
333
|
-
|
333
|
+
if operation_type == "download":
|
334
|
+
# Prefer explicit ack-focused mask if not provided by endpoint
|
335
|
+
filemask = endpoint_config.get('filemask') or ['era', '277', '277ibr', '277ebr', '999']
|
336
|
+
else:
|
337
|
+
filemask = None
|
338
|
+
command = build_command(winscp_path, winscp_log_path, endpoint_config, remote_directory, operation_type, files, local_storage_path, newer_than=None, filemask=filemask)
|
334
339
|
|
335
340
|
if config.get("TestMode", True):
|
336
341
|
MediLink_ConfigLoader.log("Test mode is enabled. Simulating operation.")
|
@@ -470,6 +475,48 @@ def get_remote_directory(endpoint_config, operation_type):
|
|
470
475
|
MediLink_ConfigLoader.log("Critical Error: Endpoint config is missing key: {}".format(e))
|
471
476
|
raise RuntimeError("Configuration error: Missing required remote directory in endpoint configuration.")
|
472
477
|
|
478
|
+
def normalize_filemask(filemask):
|
479
|
+
"""
|
480
|
+
Normalize various filemask inputs into WinSCP-compatible string.
|
481
|
+
Supports list of extensions, comma-separated string, or dict with 'extensions' and other filters.
|
482
|
+
Falls back to '*' when input is invalid.
|
483
|
+
"""
|
484
|
+
try:
|
485
|
+
if not filemask:
|
486
|
+
return '*'
|
487
|
+
if isinstance(filemask, list):
|
488
|
+
parts = []
|
489
|
+
for ext in filemask:
|
490
|
+
s = str(ext).strip().lstrip('*.').lstrip('.')
|
491
|
+
if s:
|
492
|
+
parts.append('*.{}'.format(s))
|
493
|
+
return '|'.join(parts) if parts else '*'
|
494
|
+
if isinstance(filemask, dict):
|
495
|
+
exts = filemask.get('extensions', [])
|
496
|
+
other = []
|
497
|
+
for k, v in filemask.items():
|
498
|
+
if k == 'extensions':
|
499
|
+
continue
|
500
|
+
other.append(str(v))
|
501
|
+
ext_part = normalize_filemask(exts)
|
502
|
+
other_part = ';'.join(other)
|
503
|
+
if ext_part and other_part:
|
504
|
+
return '{};{}'.format(ext_part, other_part)
|
505
|
+
return ext_part or other_part or '*'
|
506
|
+
if isinstance(filemask, str):
|
507
|
+
# Support comma-separated or pipe-separated lists of extensions
|
508
|
+
raw = filemask.replace(' ', '')
|
509
|
+
if any(sep in raw for sep in [',', '|']):
|
510
|
+
tokens = raw.replace('|', ',').split(',')
|
511
|
+
return normalize_filemask([t for t in tokens if t])
|
512
|
+
# If looks like an extension, prefix
|
513
|
+
s = raw.lstrip('*.').lstrip('.')
|
514
|
+
if s and all(ch.isalnum() for ch in s):
|
515
|
+
return '*.{}'.format(s)
|
516
|
+
return raw or '*'
|
517
|
+
except Exception:
|
518
|
+
return '*'
|
519
|
+
|
473
520
|
def build_command(winscp_path, winscp_log_path, endpoint_config, remote_directory, operation_type, files, local_storage_path, newer_than=None, filemask=None):
|
474
521
|
# Log the operation type
|
475
522
|
MediLink_ConfigLoader.log("[Build Command] Building WinSCP command for operation type: {}".format(operation_type))
|
@@ -581,14 +628,7 @@ def build_command(winscp_path, winscp_log_path, endpoint_config, remote_director
|
|
581
628
|
# 5. Add validation for WinSCP-compatible patterns
|
582
629
|
# 6. Add logging for debugging filemask translations
|
583
630
|
# 7. XP QUIRK: Prefer simple masks (e.g., *.csv|*.txt) and avoid complex AND/OR until verified on XP.
|
584
|
-
|
585
|
-
filemask_str = '|'.join(['*.' + ext for ext in filemask])
|
586
|
-
elif isinstance(filemask, dict):
|
587
|
-
filemask_str = '|'.join(['*.' + ext for ext in filemask.keys()])
|
588
|
-
elif isinstance(filemask, str):
|
589
|
-
filemask_str = filemask # Assume it's already in the correct format
|
590
|
-
else:
|
591
|
-
filemask_str = '*' # Default to all files if filemask is None or unsupported type
|
631
|
+
filemask_str = normalize_filemask(filemask)
|
592
632
|
else:
|
593
633
|
filemask_str = '*' # Default to all files if filemask is None
|
594
634
|
|
@@ -659,38 +699,25 @@ def execute_winscp_command(command, operation_type, files, local_storage_path):
|
|
659
699
|
MediLink_ConfigLoader.log("WinSCP {} operation completed successfully.".format(operation_type))
|
660
700
|
|
661
701
|
if operation_type == 'download':
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
# - Update the lcd_command generation in execute_winscp_command()
|
682
|
-
# - Ensure local_storage_path is properly escaped for WinSCP
|
683
|
-
# Option B: Update list_downloaded_files() to check actual WinSCP location
|
684
|
-
# - Add function get_actual_winscp_download_path() that parses logs
|
685
|
-
# - Call list_downloaded_files(get_actual_winscp_download_path())
|
686
|
-
# Option C: Add configuration parameter for WinSCP-specific download path
|
687
|
-
# - Add 'winscp_download_path' to config
|
688
|
-
# - Default to local_storage_path if not specified
|
689
|
-
#
|
690
|
-
# RECOMMENDED: Option A (fix root cause) + Option C (explicit config)
|
691
|
-
# FILES TO MODIFY: This file (execute_winscp_command, list_downloaded_files functions)
|
692
|
-
# TESTING: Verify downloads work correctly after fix with various file types
|
693
|
-
MediLink_ConfigLoader.log("Files currently located in local_storage_path: {}".format(downloaded_files), level="DEBUG")
|
702
|
+
# Prefer configured override if present
|
703
|
+
winscp_download_path = None
|
704
|
+
try:
|
705
|
+
from MediCafe.core_utils import extract_medilink_config
|
706
|
+
config, _ = MediLink_ConfigLoader.load_configuration()
|
707
|
+
medi = extract_medilink_config(config)
|
708
|
+
winscp_download_path = medi.get('winscp_download_path')
|
709
|
+
except Exception:
|
710
|
+
winscp_download_path = None
|
711
|
+
|
712
|
+
target_dir = winscp_download_path or local_storage_path
|
713
|
+
downloaded_files = list_downloaded_files(target_dir)
|
714
|
+
MediLink_ConfigLoader.log("Files currently located in target directory ({}): {}".format(target_dir, downloaded_files), level="DEBUG")
|
715
|
+
|
716
|
+
if not downloaded_files and winscp_download_path and winscp_download_path != local_storage_path:
|
717
|
+
# Fallback to original path if override empty
|
718
|
+
fallback_files = list_downloaded_files(local_storage_path)
|
719
|
+
MediLink_ConfigLoader.log("Fallback to local_storage_path yielded: {}".format(fallback_files), level="DEBUG")
|
720
|
+
downloaded_files = fallback_files
|
694
721
|
|
695
722
|
if not downloaded_files:
|
696
723
|
MediLink_ConfigLoader.log("No files were downloaded or an error occurred during the listing process.", level="WARNING")
|
@@ -727,8 +754,12 @@ def list_downloaded_files(local_storage_path):
|
|
727
754
|
except Exception as e:
|
728
755
|
MediLink_ConfigLoader.log("Error occurred while listing files in {}: {}".format(local_storage_path, e), level="ERROR")
|
729
756
|
|
730
|
-
#
|
731
|
-
|
757
|
+
# Normalize to basenames so downstream move logic in MediLink_Down works cross-platform
|
758
|
+
try:
|
759
|
+
basenames = [os.path.basename(p) for p in downloaded_files]
|
760
|
+
return basenames
|
761
|
+
except Exception:
|
762
|
+
return downloaded_files
|
732
763
|
|
733
764
|
def detect_new_files(directory_path, file_extension='.DAT'):
|
734
765
|
"""
|
MediLink/MediLink_Decoder.py
CHANGED
@@ -20,7 +20,7 @@ else:
|
|
20
20
|
return {}, {}
|
21
21
|
def log(message, level="INFO"):
|
22
22
|
print("[{}] {}".format(level, message))
|
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
|
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, parse_999_content
|
24
24
|
|
25
25
|
# Define new_fieldnames globally
|
26
26
|
new_fieldnames = ['Claim #', 'Payer', 'Status', 'Patient', 'Proc.', 'Serv.', 'Allowed', 'Paid', 'Pt Resp', 'Charged']
|
@@ -75,7 +75,8 @@ def process_decoded_file(file_path, output_directory, return_records=False, debu
|
|
75
75
|
'277EBR': parse_277EBR_content,
|
76
76
|
'DPT': parse_dpt_content,
|
77
77
|
'EBT': parse_ebt_content,
|
78
|
-
'IBT': parse_ibt_content
|
78
|
+
'IBT': parse_ibt_content,
|
79
|
+
'999': parse_999_content
|
79
80
|
}
|
80
81
|
|
81
82
|
parse_function = parse_functions.get(file_type)
|
@@ -143,21 +144,26 @@ def format_records(records, file_type):
|
|
143
144
|
claim_number = record.get('Chart Number', '')
|
144
145
|
elif file_type == 'EBT':
|
145
146
|
claim_number = record.get('Patient Control Number', '')
|
147
|
+
elif file_type == '277':
|
148
|
+
claim_number = record.get('Claim #', '')
|
149
|
+
elif file_type == '999':
|
150
|
+
claim_number = '' # 999 lacks a direct claim number
|
146
151
|
else:
|
147
152
|
claim_number = '' # Default to empty if file type is not recognized
|
148
153
|
|
149
|
-
# Skip records without a claim number
|
150
|
-
if not claim_number:
|
154
|
+
# Skip records without a claim number, except for 999 summary/detail rows
|
155
|
+
if not claim_number and file_type != '999':
|
151
156
|
log("Record {} missing claim_number. Skipping.".format(i + 1), level="WARNING")
|
152
157
|
continue
|
153
158
|
|
154
159
|
# Check for duplicates (within this file and across files in this run)
|
155
|
-
if claim_number in seen_claim_numbers or claim_number in GLOBAL_SEEN_CLAIM_NUMBERS:
|
160
|
+
if claim_number and (claim_number in seen_claim_numbers or claim_number in GLOBAL_SEEN_CLAIM_NUMBERS):
|
156
161
|
log("Duplicate claim_number {} found at record {}. Skipping.".format(claim_number, i + 1), level="DEBUG")
|
157
162
|
continue
|
158
163
|
|
159
|
-
|
160
|
-
|
164
|
+
if claim_number:
|
165
|
+
seen_claim_numbers.add(claim_number)
|
166
|
+
GLOBAL_SEEN_CLAIM_NUMBERS.add(claim_number) # Add to cross-file set so later files also skip
|
161
167
|
|
162
168
|
unified_record = UnifiedRecord()
|
163
169
|
|
@@ -189,8 +195,7 @@ def format_records(records, file_type):
|
|
189
195
|
'A': 'Accepted',
|
190
196
|
'R': 'Rejected',
|
191
197
|
}
|
192
|
-
|
193
|
-
unified_record.status = record.get('Message', '')
|
198
|
+
unified_record.status = record.get('Message', '') or status_mapping.get(message_type, message_type)
|
194
199
|
unified_record.payer = record.get('Message Initiator', '')
|
195
200
|
unified_record.patient = record.get('Patient Name', '')
|
196
201
|
unified_record.proc_date = format_date(record.get('To Date', ''))
|
@@ -204,6 +209,30 @@ def format_records(records, file_type):
|
|
204
209
|
log("Skipped non-claim EBT Record {}: {}".format(i + 1, record), level="DEBUG")
|
205
210
|
continue
|
206
211
|
|
212
|
+
elif file_type == '277':
|
213
|
+
unified_record.claim_number = claim_number
|
214
|
+
unified_record.status = record.get('Status', '')
|
215
|
+
unified_record.patient = record.get('Patient', '')
|
216
|
+
unified_record.proc_date = format_date(record.get('Proc.', ''))
|
217
|
+
unified_record.serv_date = format_date(record.get('Serv.', ''))
|
218
|
+
unified_record.allowed = ''
|
219
|
+
unified_record.paid = record.get('Paid', '')
|
220
|
+
unified_record.pt_resp = ''
|
221
|
+
unified_record.charged = record.get('Charged', '')
|
222
|
+
|
223
|
+
elif file_type == '999':
|
224
|
+
# Show 999 summary rows; leave claim_number empty
|
225
|
+
unified_record.claim_number = ''
|
226
|
+
unified_record.status = record.get('Status', '')
|
227
|
+
unified_record.patient = ''
|
228
|
+
unified_record.payer = record.get('Functional ID', '')
|
229
|
+
unified_record.proc_date = ''
|
230
|
+
unified_record.serv_date = ''
|
231
|
+
unified_record.allowed = ''
|
232
|
+
unified_record.paid = ''
|
233
|
+
unified_record.pt_resp = ''
|
234
|
+
unified_record.charged = ''
|
235
|
+
|
207
236
|
# Append the unified record to the list
|
208
237
|
formatted_records.append(unified_record)
|
209
238
|
|