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.
@@ -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 delays in between.
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
- for attempt in range(1, retries + 1):
306
- print("Attempt {}/{} to upgrade {}...".format(attempt, retries, package))
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
- new_version = get_installed_version(package) # Get new version after upgrade
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
- if expected_version and compare_versions(new_version, expected_version) >= 0: # Compare versions
328
- if attempt == 1:
329
- print_status("Upgrade succeeded!", "SUCCESS")
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(new_version, expected_version), "WARNING")
336
- if attempt < retries:
337
- print("Retrying in {} seconds...".format(delay))
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 --no-deps.".format(attempt), "WARNING")
345
-
346
- # If --no-deps failed, try with --force-reinstall to bypass dependency issues
347
- if attempt < retries:
348
- print("Fallback this attempt: retrying with --force-reinstall...")
349
- pkg_spec = package
350
- if target_version:
351
- pkg_spec = "{}=={}".format(package, target_version)
352
-
353
- cmd = [
354
- sys.executable, '-m', 'pip', 'install', '--upgrade',
355
- '--force-reinstall', '--no-cache-dir', '--disable-pip-version-check', '-q', pkg_spec
356
- ]
357
-
358
- process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
359
- stdout, stderr = process.communicate()
360
-
361
- if process.returncode == 0:
362
- print(stdout.decode().strip())
363
- new_version = get_installed_version(package)
364
- expected_version = target_version or get_latest_version(package)
365
- if expected_version and compare_versions(new_version, expected_version) >= 0:
366
- print_status("Attempt {}: Upgrade succeeded with --force-reinstall!".format(attempt), "SUCCESS")
367
- time.sleep(delay)
368
- return True
369
- else:
370
- print_status("Upgrade incomplete. Current version: {} Expected at least: {}".format(new_version, expected_version), "WARNING")
371
- else:
372
- print(stderr.decode().strip())
373
- print_status("Attempt {}: Upgrade failed with --force-reinstall.".format(attempt), "WARNING")
374
-
375
- if attempt < retries:
376
- print("Retrying in {} seconds...".format(delay))
377
- try:
378
- time.sleep(delay + (random.random() * 0.5))
379
- except Exception:
380
- time.sleep(delay)
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
@@ -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):
@@ -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
- command = build_command(winscp_path, winscp_log_path, endpoint_config, remote_directory, operation_type, files, local_storage_path)
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
- if isinstance(filemask, list):
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
- downloaded_files = list_downloaded_files(local_storage_path)
663
- # TODO (HIGH PRIORITY - WinSCP Path Configuration Issue):
664
- # PROBLEM: WinSCP is not downloading files to the expected local_storage_path directory.
665
- # The list_downloaded_files() function is checking the wrong location.
666
- #
667
- # XP/WinSCP SUGGESTION:
668
- # - Add config override 'winscp_download_path' to explicitly set WinSCP's target directory.
669
- # - If set, prefer list_downloaded_files(config['MediLink_Config']['winscp_download_path']).
670
- # - Otherwise, parse the WinSCP log to detect the actual path and fall back to local_storage_path.
671
- #
672
- # INVESTIGATION STEPS:
673
- # 1. Check WinSCP logs to determine actual download destination:
674
- # - Look in config['MediLink_Config']['local_claims_path'] + "winscp_download.log"
675
- # - Parse log entries for "file downloaded to:" or similar patterns
676
- # 2. Compare actual WinSCP download path vs configured local_storage_path
677
- # 3. Check if WinSCP uses different path conventions (forward/backward slashes)
678
- #
679
- # IMPLEMENTATION OPTIONS:
680
- # Option A: Fix WinSCP command to use correct target directory
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
- # Ensure that the function always returns a list
731
- return downloaded_files
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
  """
@@ -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
- seen_claim_numbers.add(claim_number)
160
- GLOBAL_SEEN_CLAIM_NUMBERS.add(claim_number) # Add to cross-file set so later files also skip
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
- # unified_record.status = status_mapping.get(message_type, message_type)
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