medicafe 0.250711.1__py3-none-any.whl → 0.250720.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.
Potentially problematic release.
This version of medicafe might be problematic. Click here for more details.
- MediBot/MediBot_Crosswalk_Library.py +109 -1
- MediLink/MediLink_837p_cob_library.py +832 -0
- MediLink/MediLink_837p_encoder.py +40 -0
- MediLink/MediLink_837p_encoder_library.py +78 -123
- MediLink/MediLink_837p_utilities.py +264 -0
- MediLink/MediLink_ClaimStatus.py +4 -3
- MediLink/MediLink_Deductible.py +401 -108
- MediLink/MediLink_Deductible_Validator.py +484 -0
- MediLink/test_cob_library.py +436 -0
- MediLink/test_validation.py +127 -0
- {medicafe-0.250711.1.dist-info → medicafe-0.250720.1.dist-info}/METADATA +1 -1
- {medicafe-0.250711.1.dist-info → medicafe-0.250720.1.dist-info}/RECORD +15 -10
- {medicafe-0.250711.1.dist-info → medicafe-0.250720.1.dist-info}/LICENSE +0 -0
- {medicafe-0.250711.1.dist-info → medicafe-0.250720.1.dist-info}/WHEEL +0 -0
- {medicafe-0.250711.1.dist-info → medicafe-0.250720.1.dist-info}/top_level.txt +0 -0
|
@@ -4,6 +4,8 @@ from datetime import datetime
|
|
|
4
4
|
import MediLink_ConfigLoader
|
|
5
5
|
from MediLink_DataMgmt import parse_fixed_width_data, read_fixed_width_data
|
|
6
6
|
import MediLink_837p_encoder_library
|
|
7
|
+
# TODO (COB ENHANCEMENT): Import COB library when implementing Medicare and secondary claim support
|
|
8
|
+
# import MediLink_837p_cob_library
|
|
7
9
|
#from tqdm import tqdm
|
|
8
10
|
|
|
9
11
|
def format_single_claim(patient_data, config, endpoint, transaction_set_control_number, crosswalk, client):
|
|
@@ -46,7 +48,18 @@ def format_single_claim(patient_data, config, endpoint, transaction_set_control_
|
|
|
46
48
|
|
|
47
49
|
# Subscriber information, possibly including endpoint-specific logic
|
|
48
50
|
segments.extend(MediLink_837p_encoder_library.create_hl_subscriber_segment())
|
|
51
|
+
|
|
52
|
+
# TODO (COB ENHANCEMENT): Enhanced SBR segment creation for COB scenarios
|
|
53
|
+
# Replace the standard SBR segment creation with enhanced COB-aware version
|
|
54
|
+
# Current: segments.append(MediLink_837p_encoder_library.create_sbr_segment(config, patient_data, endpoint))
|
|
55
|
+
# Enhanced:
|
|
56
|
+
# if patient_data.get('claim_type') == 'secondary':
|
|
57
|
+
# enhanced_sbr = MediLink_837p_cob_library.create_enhanced_sbr_segment(patient_data, config, crosswalk)
|
|
58
|
+
# segments.append(enhanced_sbr)
|
|
59
|
+
# else:
|
|
60
|
+
# segments.append(MediLink_837p_encoder_library.create_sbr_segment(config, patient_data, endpoint))
|
|
49
61
|
segments.append(MediLink_837p_encoder_library.create_sbr_segment(config, patient_data, endpoint))
|
|
62
|
+
|
|
50
63
|
segments.append(MediLink_837p_encoder_library.create_nm1_subscriber_segment(config, patient_data, endpoint))
|
|
51
64
|
segments.extend(MediLink_837p_encoder_library.create_subscriber_address_segments(patient_data))
|
|
52
65
|
segments.append(MediLink_837p_encoder_library.create_dmg_segment(patient_data))
|
|
@@ -58,9 +71,36 @@ def format_single_claim(patient_data, config, endpoint, transaction_set_control_
|
|
|
58
71
|
segments.extend([MediLink_837p_encoder_library.create_2010BB_payer_information_segment(patient_data)])
|
|
59
72
|
#segments.extend(MediLink_837p_encoder_library.create_payer_address_segments(config)) OMITTED
|
|
60
73
|
|
|
74
|
+
# TODO (COB ENHANCEMENT): COB processing for secondary claims and Medicare adjudication
|
|
75
|
+
# See MediLink_837p_cob_library.process_cob_claim() for complete COB implementation
|
|
76
|
+
# This would include:
|
|
77
|
+
# - Enhanced SBR segments with Medicare payer types (MB/MA)
|
|
78
|
+
# - 2320 loop for other subscriber information
|
|
79
|
+
# - 2330B loop for prior payer (Medicare) information
|
|
80
|
+
# - 2330C loop when patient != subscriber
|
|
81
|
+
# - 2430 loop for service-level COB adjudication
|
|
82
|
+
# - PWK segments for attachment references
|
|
83
|
+
#
|
|
84
|
+
# Integration point for COB processing:
|
|
85
|
+
# if patient_data.get('claim_type') == 'secondary' or patient_data.get('requires_cob_processing'):
|
|
86
|
+
# cob_segments = MediLink_837p_cob_library.process_cob_claim(patient_data, config, crosswalk, client)
|
|
87
|
+
# segments.extend(cob_segments)
|
|
88
|
+
|
|
61
89
|
# Rendering Provider (2310B Loop)
|
|
62
90
|
segments.extend(MediLink_837p_encoder_library.create_nm1_rendering_provider_segment(config))
|
|
63
91
|
|
|
92
|
+
# TODO (COB ENHANCEMENT): Enhanced CLM segment creation for COB claims
|
|
93
|
+
# Replace standard CLM creation with COB-aware version
|
|
94
|
+
# Current: segments.extend(MediLink_837p_encoder_library.create_clm_and_related_segments(patient_data, config, crosswalk))
|
|
95
|
+
# Enhanced:
|
|
96
|
+
# if patient_data.get('claim_type') == 'secondary':
|
|
97
|
+
# enhanced_clm = MediLink_837p_cob_library.create_enhanced_clm_segment(patient_data, config, crosswalk)
|
|
98
|
+
# segments.append(enhanced_clm)
|
|
99
|
+
# # Add service line segments separately for COB
|
|
100
|
+
# segments.extend(MediLink_837p_encoder_library.create_service_line_segments(patient_data, config, crosswalk))
|
|
101
|
+
# else:
|
|
102
|
+
# segments.extend(MediLink_837p_encoder_library.create_clm_and_related_segments(patient_data, config, crosswalk))
|
|
103
|
+
|
|
64
104
|
# Claim information 2300, 2310C Service Facility and 2400 loop segments
|
|
65
105
|
segments.extend(MediLink_837p_encoder_library.create_clm_and_related_segments(patient_data, config, crosswalk))
|
|
66
106
|
|
|
@@ -10,27 +10,22 @@ if project_dir not in sys.path:
|
|
|
10
10
|
from MediBot import MediBot_Preprocessor_lib
|
|
11
11
|
load_insurance_data_from_mains = MediBot_Preprocessor_lib.load_insurance_data_from_mains
|
|
12
12
|
from MediBot import MediBot_Crosswalk_Library
|
|
13
|
-
from MediLink_API_v3 import fetch_payer_name_from_api
|
|
14
|
-
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return dt.strftime('%Y%m%d')
|
|
30
|
-
elif format_type == 'isa':
|
|
31
|
-
return dt.strftime('%y%m%d')
|
|
32
|
-
elif format_type == 'time':
|
|
33
|
-
return dt.strftime('%H%M')
|
|
13
|
+
from .MediLink_API_v3 import fetch_payer_name_from_api
|
|
14
|
+
|
|
15
|
+
# Import utility functions from utilities module
|
|
16
|
+
from .MediLink_837p_utilities import (
|
|
17
|
+
convert_date_format,
|
|
18
|
+
format_datetime,
|
|
19
|
+
get_user_confirmation,
|
|
20
|
+
prompt_user_for_payer_id,
|
|
21
|
+
format_claim_number,
|
|
22
|
+
generate_segment_counts,
|
|
23
|
+
handle_validation_errors,
|
|
24
|
+
get_output_directory,
|
|
25
|
+
winscp_validate_output_directory
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
34
29
|
|
|
35
30
|
# Constructs the ST segment for transaction set.
|
|
36
31
|
def create_st_segment(transaction_set_control_number):
|
|
@@ -152,14 +147,18 @@ def create_1000A_submitter_name_segment(patient_data, config, endpoint):
|
|
|
152
147
|
# Submitter contact details
|
|
153
148
|
contact_name = config.get('submitter_name', 'NONE')
|
|
154
149
|
contact_telephone_number = config.get('submitter_tel', 'NONE')
|
|
155
|
-
|
|
150
|
+
|
|
151
|
+
# Get submitter first name to determine entity type qualifier
|
|
152
|
+
submitter_first_name = config.get('submitter_first_name', '')
|
|
153
|
+
# Determine entity_type_qualifier: '1' for individual (with first name), '2' for organization
|
|
154
|
+
entity_type_qualifier = '1' if submitter_first_name else '2' # Make sure that this is correct. Original default was 2.
|
|
156
155
|
|
|
157
156
|
# Construct NM1 segment for the submitter
|
|
158
157
|
nm1_segment = "NM1*41*{}*{}*****{}*{}~".format(entity_type_qualifier, submitter_name, submitter_id_qualifier, submitter_id) # BUG - need to check submitter_name because this is written as fixed ****** which implies a single entry and not a first and last name. This is weird.
|
|
159
158
|
|
|
160
159
|
# Construct PER segment for the submitter's contact information
|
|
161
160
|
per_segment = "PER*IC*{}*TE*{}~".format(contact_name, contact_telephone_number)
|
|
162
|
-
|
|
161
|
+
|
|
163
162
|
return [nm1_segment, per_segment]
|
|
164
163
|
|
|
165
164
|
# Constructs the NM1 segment for the receiver (1000B).
|
|
@@ -237,15 +236,7 @@ def create_2010BB_payer_information_segment(parsed_data):
|
|
|
237
236
|
# Build NM1 segment using provided payer name and payer ID
|
|
238
237
|
return build_nm1_segment(payer_name, payer_id)
|
|
239
238
|
|
|
240
|
-
|
|
241
|
-
while True:
|
|
242
|
-
response = input(prompt_message).strip().lower()
|
|
243
|
-
if response in ['yes', 'y']:
|
|
244
|
-
return True
|
|
245
|
-
elif response in ['no', 'n']:
|
|
246
|
-
return False
|
|
247
|
-
else:
|
|
248
|
-
print("Please respond with 'yes' or 'no'.")
|
|
239
|
+
|
|
249
240
|
|
|
250
241
|
def resolve_payer_name(payer_id, config, primary_endpoint, insurance_name, parsed_data, crosswalk, client):
|
|
251
242
|
# Check if the payer_id is in the crosswalk with a name already attached to it.
|
|
@@ -405,18 +396,7 @@ def handle_missing_payer_id(insurance_name, config, crosswalk, client):
|
|
|
405
396
|
MediLink_ConfigLoader.log("User did not confirm the standard insurance name. Manual intervention is required.", config, level="CRITICAL")
|
|
406
397
|
return None
|
|
407
398
|
|
|
408
|
-
|
|
409
|
-
"""
|
|
410
|
-
Prompts the user to input the payer ID manually and ensures that a valid alphanumeric ID is provided.
|
|
411
|
-
"""
|
|
412
|
-
while True:
|
|
413
|
-
print("Manual intervention required: No payer ID found for insurance name '{}'.".format(insurance_name))
|
|
414
|
-
payer_id = input("Please enter the payer ID manually: ").strip()
|
|
415
|
-
|
|
416
|
-
if payer_id.isalnum():
|
|
417
|
-
return payer_id
|
|
418
|
-
else:
|
|
419
|
-
print("Error: Payer ID must be alphanumeric. Please try again.")
|
|
399
|
+
|
|
420
400
|
|
|
421
401
|
def build_nm1_segment(payer_name, payer_id):
|
|
422
402
|
# Step 1: Build NM1 segment using payer name and ID
|
|
@@ -539,6 +519,12 @@ def create_sbr_segment(config, parsed_data, endpoint):
|
|
|
539
519
|
# Insurance Type Code
|
|
540
520
|
insurance_type_code = insurance_type_selection(parsed_data)
|
|
541
521
|
|
|
522
|
+
# TODO (COB ENHANCEMENT): Enhanced SBR segment for Medicare and COB support
|
|
523
|
+
# For Medicare Part B: SBR09 = "MB"
|
|
524
|
+
# For Medicare Advantage: SBR09 = "MA"
|
|
525
|
+
# For secondary claims: SBR01 = "S"
|
|
526
|
+
# See MediLink_837p_cob_library.create_enhanced_sbr_segment() for implementation
|
|
527
|
+
|
|
542
528
|
# Construct the SBR segment using the determined codes
|
|
543
529
|
sbr_segment = "SBR*{responsibility_code}*18*******{insurance_type_code}~".format(
|
|
544
530
|
responsibility_code=responsibility_code,
|
|
@@ -591,6 +577,12 @@ def insurance_type_selection(parsed_data):
|
|
|
591
577
|
# Retrieve insurance options with codes and descriptions
|
|
592
578
|
config, _ = MediLink_ConfigLoader.load_configuration()
|
|
593
579
|
insurance_options = config['MediLink_Config'].get('insurance_options')
|
|
580
|
+
|
|
581
|
+
# TODO (COB ENHANCEMENT): Enhanced insurance options for Medicare support
|
|
582
|
+
# See MediLink_837p_cob_library.get_enhanced_insurance_options() for Medicare codes:
|
|
583
|
+
# - MB: Medicare Part B
|
|
584
|
+
# - MA: Medicare Advantage
|
|
585
|
+
# - MC: Medicare Part C
|
|
594
586
|
|
|
595
587
|
def prompt_display_insurance_options():
|
|
596
588
|
# Prompt to display full list
|
|
@@ -684,15 +676,7 @@ def create_nm1_rendering_provider_segment(config, is_rendering_provider_differen
|
|
|
684
676
|
else:
|
|
685
677
|
return []
|
|
686
678
|
|
|
687
|
-
|
|
688
|
-
# Remove any non-alphanumeric characters from chart number and date
|
|
689
|
-
chart_number_alphanumeric = ''.join(filter(str.isalnum, chart_number))
|
|
690
|
-
date_of_service_alphanumeric = ''.join(filter(str.isalnum, date_of_service))
|
|
691
|
-
|
|
692
|
-
# Combine the alphanumeric components without spaces
|
|
693
|
-
formatted_claim_number = chart_number_alphanumeric + date_of_service_alphanumeric
|
|
694
|
-
|
|
695
|
-
return formatted_claim_number
|
|
679
|
+
|
|
696
680
|
|
|
697
681
|
# Constructs the CLM and related segments based on parsed data and configuration.
|
|
698
682
|
def create_clm_and_related_segments(parsed_data, config, crosswalk):
|
|
@@ -705,6 +689,14 @@ def create_clm_and_related_segments(parsed_data, config, crosswalk):
|
|
|
705
689
|
Service Line Information (2400 Loop):
|
|
706
690
|
Verify that the service line number (LX), professional service (SV1), and service date (DTP) segments contain
|
|
707
691
|
accurate information and are formatted according to the claim's details.
|
|
692
|
+
|
|
693
|
+
TODO (COB ENHANCEMENT): This function needs enhancement for COB scenarios:
|
|
694
|
+
1. Enhanced CLM segment with proper claim frequency (CLM05-3)
|
|
695
|
+
2. Support for service-level COB adjudication (2430 loop)
|
|
696
|
+
3. Integration with 835-derived adjudication data
|
|
697
|
+
4. PWK segment support for attachments
|
|
698
|
+
|
|
699
|
+
See MediLink_837p_cob_library.create_enhanced_clm_segment() for enhanced implementation.
|
|
708
700
|
"""
|
|
709
701
|
|
|
710
702
|
segments = []
|
|
@@ -715,15 +707,22 @@ def create_clm_and_related_segments(parsed_data, config, crosswalk):
|
|
|
715
707
|
formatted_claim_number = format_claim_number(chart_number, date_of_service)
|
|
716
708
|
|
|
717
709
|
# CLM - Claim Information
|
|
718
|
-
|
|
710
|
+
# TODO (COB ENHANCEMENT): Enhanced claim frequency handling
|
|
711
|
+
# For COB claims, CLM05-3 should be "1" (original) unless replacement logic applies
|
|
712
|
+
claim_frequency = "1" # Default to original
|
|
713
|
+
# if parsed_data.get('claim_type') == 'secondary':
|
|
714
|
+
# claim_frequency = "1" # Original for secondary claims
|
|
715
|
+
|
|
716
|
+
segments.append("CLM*{}*{}***{}:B:{}*Y*A*Y*Y~".format(
|
|
719
717
|
formatted_claim_number,
|
|
720
718
|
parsed_data['AMOUNT'],
|
|
721
|
-
parsed_data['TOS']
|
|
719
|
+
parsed_data['TOS'],
|
|
720
|
+
claim_frequency))
|
|
722
721
|
|
|
723
722
|
# HI - Health Care Diagnosis Code
|
|
724
723
|
# Hardcoding "ABK" for ICD-10 codes as they are the only ones used now.
|
|
725
724
|
medisoft_code = ''.join(filter(str.isalnum, parsed_data['DIAG']))
|
|
726
|
-
diagnosis_code = next((key for key, value in crosswalk.get('diagnosis_to_medisoft', {}).items() if value == medisoft_code), None)
|
|
725
|
+
diagnosis_code = next((key for key, value in crosswalk.get('diagnosis_to_medisoft', {}).items() if value == medisoft_code), None)
|
|
727
726
|
|
|
728
727
|
if diagnosis_code is None:
|
|
729
728
|
error_message = "Diagnosis code is empty for chart number: {}. Please verify. Medisoft code is {}".format(chart_number, medisoft_code)
|
|
@@ -743,8 +742,21 @@ def create_clm_and_related_segments(parsed_data, config, crosswalk):
|
|
|
743
742
|
segments.extend(create_service_facility_location_npi_segment(config))
|
|
744
743
|
|
|
745
744
|
# For future reference, SBR - (Loop 2320: OI, NM1 (2330A), N3, N4, NM1 (2330B)) - Other Subscriber Information goes here.
|
|
746
|
-
|
|
747
|
-
#
|
|
745
|
+
# TODO (COB ENHANCEMENT): COB loops for secondary claims and Medicare adjudication
|
|
746
|
+
# See MediLink_837p_cob_library for implementation of:
|
|
747
|
+
# - create_2320_other_subscriber_segments() for secondary payer info
|
|
748
|
+
# - create_2330B_prior_payer_segments() for Medicare prior payer
|
|
749
|
+
# - create_2430_service_line_cob_segments() for service-level adjudication
|
|
750
|
+
# - create_2330C_other_subscriber_name_segments() when patient != subscriber
|
|
751
|
+
# TODO (COB ENHANCEMENT): Optional attachment references (PWK) for non-electronic EOB handling
|
|
752
|
+
# See MediLink_837p_cob_library.create_pwk_attachment_segment() for implementation
|
|
753
|
+
# Example: PWK*EB*FX*123456~ for attachment control number
|
|
754
|
+
# if parsed_data.get('requires_attachment'):
|
|
755
|
+
# pwk_segment = MediLink_837p_cob_library.create_pwk_attachment_segment(parsed_data, config)
|
|
756
|
+
# if pwk_segment:
|
|
757
|
+
# segments.append(pwk_segment)
|
|
758
|
+
|
|
759
|
+
# LX - Service Line Counter
|
|
748
760
|
segments.append("LX*1~")
|
|
749
761
|
|
|
750
762
|
# SV1 - Professional Service
|
|
@@ -762,6 +774,14 @@ def create_clm_and_related_segments(parsed_data, config, crosswalk):
|
|
|
762
774
|
# 6R - Provider Control Number (Number assigned by information provider company for tracking and billing purposes)
|
|
763
775
|
# 1 - Reference information as defined for a particular Transaction Set or as specified by the Reference Identification Qualifier
|
|
764
776
|
|
|
777
|
+
|
|
778
|
+
# TODO (COB ENHANCEMENT): Service-level COB adjudication (2430 loop)
|
|
779
|
+
# See MediLink_837p_cob_library.create_2430_service_line_cob_segments() for implementation
|
|
780
|
+
# This would include SVD, CAS, and DTP*573 segments for service-level adjudication
|
|
781
|
+
# if parsed_data.get('service_adjudications'):
|
|
782
|
+
# cob_segments = MediLink_837p_cob_library.create_2430_service_line_cob_segments(parsed_data, config, crosswalk)
|
|
783
|
+
# segments.extend(cob_segments)
|
|
784
|
+
|
|
765
785
|
return segments
|
|
766
786
|
|
|
767
787
|
def get_endpoint_config(config, endpoint):
|
|
@@ -892,70 +912,5 @@ def create_interchange_trailer(config, num_transactions, isa13, num_functional_g
|
|
|
892
912
|
|
|
893
913
|
return ge_segment, iea_segment
|
|
894
914
|
|
|
895
|
-
# Generates segment counts for the formatted 837P transaction and updates SE segment.
|
|
896
|
-
def generate_segment_counts(compiled_segments, transaction_set_control_number):
|
|
897
|
-
# Count the number of segments, not including the placeholder SE segment
|
|
898
|
-
segment_count = compiled_segments.count('~') # + 1 Including SE segment itself, but seems to be giving errors.
|
|
899
|
-
|
|
900
|
-
# Ensure transaction set control number is correctly formatted as a string
|
|
901
|
-
formatted_control_number = str(transaction_set_control_number).zfill(4) # Pad to ensure minimum 4 characters
|
|
902
915
|
|
|
903
|
-
# Construct the SE segment with the actual segment count and the formatted transaction set control_number
|
|
904
|
-
se_segment = "SE*{0}*{1}~".format(segment_count, formatted_control_number)
|
|
905
916
|
|
|
906
|
-
# Assuming the placeholder SE segment was the last segment added before compiling
|
|
907
|
-
# This time, we directly replace the placeholder with the correct SE segment
|
|
908
|
-
formatted_837p = compiled_segments.rsplit('SE**', 1)[0] + se_segment
|
|
909
|
-
|
|
910
|
-
return formatted_837p
|
|
911
|
-
|
|
912
|
-
def handle_validation_errors(transaction_set_control_number, validation_errors, config):
|
|
913
|
-
for error in validation_errors:
|
|
914
|
-
MediLink_ConfigLoader.log("Validation error for transaction set {}: {}".format(transaction_set_control_number, error), config, level="WARNING")
|
|
915
|
-
|
|
916
|
-
print("Validation errors encountered for transaction set {}. Errors: {}".format(transaction_set_control_number, validation_errors))
|
|
917
|
-
user_input = input("Skip this patient and continue without incrementing transaction set number? (yes/no): ")
|
|
918
|
-
if user_input.lower() == 'yes':
|
|
919
|
-
print("Skipping patient...")
|
|
920
|
-
MediLink_ConfigLoader.log("Skipped processing of transaction set {} due to user decision.".format(transaction_set_control_number), config, level="INFO")
|
|
921
|
-
return True # Skip the current patient
|
|
922
|
-
else:
|
|
923
|
-
print("Processing halted due to validation errors.")
|
|
924
|
-
MediLink_ConfigLoader.log("HALT: Processing halted at transaction set {} due to unresolved validation errors.".format(transaction_set_control_number), config, level="ERROR")
|
|
925
|
-
sys.exit() # Optionally halt further processing
|
|
926
|
-
|
|
927
|
-
def winscp_validate_output_directory(output_directory):
|
|
928
|
-
"""
|
|
929
|
-
Validates the output directory path to ensure it has no spaces.
|
|
930
|
-
If spaces are found, prompts the user to input a new path.
|
|
931
|
-
If the directory doesn't exist, creates it.
|
|
932
|
-
"""
|
|
933
|
-
while ' ' in output_directory:
|
|
934
|
-
print("\nWARNING: The output directory path contains spaces, which can cause issues with upload operations.")
|
|
935
|
-
print(" Current proposed path: {}".format(output_directory))
|
|
936
|
-
new_path = input("Please enter a new path for the output directory: ")
|
|
937
|
-
output_directory = new_path.strip() # Remove leading/trailing spaces
|
|
938
|
-
|
|
939
|
-
# Check if the directory exists, if not, create it
|
|
940
|
-
if not os.path.exists(output_directory):
|
|
941
|
-
os.makedirs(output_directory)
|
|
942
|
-
print("INFO: Created output directory: {}".format(output_directory))
|
|
943
|
-
|
|
944
|
-
return output_directory
|
|
945
|
-
|
|
946
|
-
def get_output_directory(config):
|
|
947
|
-
# Retrieve desired default output file path from config
|
|
948
|
-
output_directory = config.get('outputFilePath', '').strip()
|
|
949
|
-
# BUG (Low SFTP) Add WinSCP validation because of the mishandling of spaces in paths. (This shouldn't need to exist.)
|
|
950
|
-
if not output_directory:
|
|
951
|
-
print("Output file path is not specified in the configuration.")
|
|
952
|
-
output_directory = input("Please enter a valid output directory path: ").strip()
|
|
953
|
-
|
|
954
|
-
# Validate the directory path (checks for spaces and existence)
|
|
955
|
-
output_directory = winscp_validate_output_directory(output_directory)
|
|
956
|
-
|
|
957
|
-
if not os.path.isdir(output_directory):
|
|
958
|
-
print("Output directory does not exist or is not accessible. Please check the configuration.")
|
|
959
|
-
return None
|
|
960
|
-
|
|
961
|
-
return output_directory
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# MediLink_837p_utilities.py
|
|
2
|
+
"""
|
|
3
|
+
837P Encoder Utility Functions
|
|
4
|
+
|
|
5
|
+
This module contains utility functions extracted from MediLink_837p_encoder_library.py
|
|
6
|
+
to reduce the size and complexity of the main encoder library while avoiding circular imports.
|
|
7
|
+
|
|
8
|
+
Functions included:
|
|
9
|
+
- Date/time formatting utilities
|
|
10
|
+
- User interaction utilities
|
|
11
|
+
- File/path handling utilities
|
|
12
|
+
- Processing utilities
|
|
13
|
+
- Validation utilities
|
|
14
|
+
|
|
15
|
+
Import Strategy:
|
|
16
|
+
This module only imports base Python modules and MediLink_ConfigLoader to avoid
|
|
17
|
+
circular dependencies. Other modules import from this utilities module.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
import sys
|
|
22
|
+
import os
|
|
23
|
+
import re
|
|
24
|
+
|
|
25
|
+
# Import MediLink_ConfigLoader for logging functionality
|
|
26
|
+
try:
|
|
27
|
+
from MediLink import MediLink_ConfigLoader
|
|
28
|
+
except ImportError:
|
|
29
|
+
import MediLink_ConfigLoader
|
|
30
|
+
|
|
31
|
+
# =============================================================================
|
|
32
|
+
# DATE/TIME UTILITIES
|
|
33
|
+
# =============================================================================
|
|
34
|
+
|
|
35
|
+
def convert_date_format(date_str):
|
|
36
|
+
"""
|
|
37
|
+
Converts date format from one format to another.
|
|
38
|
+
|
|
39
|
+
Parameters:
|
|
40
|
+
- date_str: Date string in MM-DD-YYYY or MM-DD-YY format
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
- Date string in YYYYMMDD format
|
|
44
|
+
"""
|
|
45
|
+
# Parse the input date string into a datetime object using the input format
|
|
46
|
+
# Determine the input date format based on the length of the input string
|
|
47
|
+
input_format = "%m-%d-%Y" if len(date_str) == 10 else "%m-%d-%y"
|
|
48
|
+
date_obj = datetime.strptime(date_str, input_format)
|
|
49
|
+
# Format the datetime object into the desired output format and return
|
|
50
|
+
return date_obj.strftime("%Y%m%d")
|
|
51
|
+
|
|
52
|
+
def format_datetime(dt=None, format_type='date'):
|
|
53
|
+
"""
|
|
54
|
+
Formats date and time according to the specified format.
|
|
55
|
+
|
|
56
|
+
Parameters:
|
|
57
|
+
- dt: datetime object (defaults to current datetime if None)
|
|
58
|
+
- format_type: 'date', 'isa', or 'time'
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
- Formatted date/time string
|
|
62
|
+
"""
|
|
63
|
+
if dt is None:
|
|
64
|
+
dt = datetime.now()
|
|
65
|
+
if format_type == 'date':
|
|
66
|
+
return dt.strftime('%Y%m%d')
|
|
67
|
+
elif format_type == 'isa':
|
|
68
|
+
return dt.strftime('%y%m%d')
|
|
69
|
+
elif format_type == 'time':
|
|
70
|
+
return dt.strftime('%H%M')
|
|
71
|
+
|
|
72
|
+
# =============================================================================
|
|
73
|
+
# USER INTERACTION UTILITIES
|
|
74
|
+
# =============================================================================
|
|
75
|
+
|
|
76
|
+
def get_user_confirmation(prompt_message):
|
|
77
|
+
"""
|
|
78
|
+
Prompts user for yes/no confirmation with validation.
|
|
79
|
+
|
|
80
|
+
Parameters:
|
|
81
|
+
- prompt_message: Message to display to user
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
- Boolean: True for yes, False for no
|
|
85
|
+
"""
|
|
86
|
+
while True:
|
|
87
|
+
response = input(prompt_message).strip().lower()
|
|
88
|
+
if response in ['yes', 'y']:
|
|
89
|
+
return True
|
|
90
|
+
elif response in ['no', 'n']:
|
|
91
|
+
return False
|
|
92
|
+
else:
|
|
93
|
+
print("Please respond with 'yes' or 'no'.")
|
|
94
|
+
|
|
95
|
+
def prompt_user_for_payer_id(insurance_name):
|
|
96
|
+
"""
|
|
97
|
+
Prompts the user to input the payer ID manually and ensures that a valid alphanumeric ID is provided.
|
|
98
|
+
|
|
99
|
+
Parameters:
|
|
100
|
+
- insurance_name: Name of the insurance for context
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
- Valid alphanumeric payer ID
|
|
104
|
+
"""
|
|
105
|
+
while True:
|
|
106
|
+
print("Manual intervention required: No payer ID found for insurance name '{}'.".format(insurance_name))
|
|
107
|
+
payer_id = input("Please enter the payer ID manually: ").strip()
|
|
108
|
+
|
|
109
|
+
if payer_id.isalnum():
|
|
110
|
+
return payer_id
|
|
111
|
+
else:
|
|
112
|
+
print("Error: Payer ID must be alphanumeric. Please try again.")
|
|
113
|
+
|
|
114
|
+
# =============================================================================
|
|
115
|
+
# FILE/PATH UTILITIES
|
|
116
|
+
# =============================================================================
|
|
117
|
+
|
|
118
|
+
def format_claim_number(chart_number, date_of_service):
|
|
119
|
+
"""
|
|
120
|
+
Formats claim number by combining chart number and date of service.
|
|
121
|
+
|
|
122
|
+
Parameters:
|
|
123
|
+
- chart_number: Patient chart number
|
|
124
|
+
- date_of_service: Date of service
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
- Formatted claim number (alphanumeric only)
|
|
128
|
+
"""
|
|
129
|
+
# Remove any non-alphanumeric characters from chart number and date
|
|
130
|
+
chart_number_alphanumeric = ''.join(filter(str.isalnum, chart_number))
|
|
131
|
+
date_of_service_alphanumeric = ''.join(filter(str.isalnum, date_of_service))
|
|
132
|
+
|
|
133
|
+
# Combine the alphanumeric components without spaces
|
|
134
|
+
formatted_claim_number = chart_number_alphanumeric + date_of_service_alphanumeric
|
|
135
|
+
|
|
136
|
+
return formatted_claim_number
|
|
137
|
+
|
|
138
|
+
def winscp_validate_output_directory(output_directory):
|
|
139
|
+
"""
|
|
140
|
+
Validates the output directory path to ensure it has no spaces.
|
|
141
|
+
If spaces are found, prompts the user to input a new path.
|
|
142
|
+
If the directory doesn't exist, creates it.
|
|
143
|
+
|
|
144
|
+
Parameters:
|
|
145
|
+
- output_directory: Directory path to validate
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
- Validated directory path
|
|
149
|
+
"""
|
|
150
|
+
while ' ' in output_directory:
|
|
151
|
+
print("\nWARNING: The output directory path contains spaces, which can cause issues with upload operations.")
|
|
152
|
+
print(" Current proposed path: {}".format(output_directory))
|
|
153
|
+
new_path = input("Please enter a new path for the output directory: ")
|
|
154
|
+
output_directory = new_path.strip() # Remove leading/trailing spaces
|
|
155
|
+
|
|
156
|
+
# Check if the directory exists, if not, create it
|
|
157
|
+
if not os.path.exists(output_directory):
|
|
158
|
+
os.makedirs(output_directory)
|
|
159
|
+
print("INFO: Created output directory: {}".format(output_directory))
|
|
160
|
+
|
|
161
|
+
return output_directory
|
|
162
|
+
|
|
163
|
+
def get_output_directory(config):
|
|
164
|
+
"""
|
|
165
|
+
Retrieves and validates output directory from configuration.
|
|
166
|
+
|
|
167
|
+
Parameters:
|
|
168
|
+
- config: Configuration dictionary
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
- Valid output directory path or None if invalid
|
|
172
|
+
"""
|
|
173
|
+
# Retrieve desired default output file path from config
|
|
174
|
+
output_directory = config.get('outputFilePath', '').strip()
|
|
175
|
+
# BUG (Low SFTP) Add WinSCP validation because of the mishandling of spaces in paths. (This shouldn't need to exist.)
|
|
176
|
+
if not output_directory:
|
|
177
|
+
print("Output file path is not specified in the configuration.")
|
|
178
|
+
output_directory = input("Please enter a valid output directory path: ").strip()
|
|
179
|
+
|
|
180
|
+
# Validate the directory path (checks for spaces and existence)
|
|
181
|
+
output_directory = winscp_validate_output_directory(output_directory)
|
|
182
|
+
|
|
183
|
+
if not os.path.isdir(output_directory):
|
|
184
|
+
print("Output directory does not exist or is not accessible. Please check the configuration.")
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
return output_directory
|
|
188
|
+
|
|
189
|
+
# =============================================================================
|
|
190
|
+
# PROCESSING UTILITIES
|
|
191
|
+
# =============================================================================
|
|
192
|
+
|
|
193
|
+
def generate_segment_counts(compiled_segments, transaction_set_control_number):
|
|
194
|
+
"""
|
|
195
|
+
Generates segment counts for the formatted 837P transaction and updates SE segment.
|
|
196
|
+
|
|
197
|
+
Parameters:
|
|
198
|
+
- compiled_segments: String containing compiled 837P segments
|
|
199
|
+
- transaction_set_control_number: Transaction set control number
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
- Formatted 837P string with correct SE segment
|
|
203
|
+
"""
|
|
204
|
+
# Count the number of segments, not including the placeholder SE segment
|
|
205
|
+
segment_count = compiled_segments.count('~') # + 1 Including SE segment itself, but seems to be giving errors.
|
|
206
|
+
|
|
207
|
+
# Ensure transaction set control number is correctly formatted as a string
|
|
208
|
+
formatted_control_number = str(transaction_set_control_number).zfill(4) # Pad to ensure minimum 4 characters
|
|
209
|
+
|
|
210
|
+
# Construct the SE segment with the actual segment count and the formatted transaction set control_number
|
|
211
|
+
se_segment = "SE*{0}*{1}~".format(segment_count, formatted_control_number)
|
|
212
|
+
|
|
213
|
+
# Assuming the placeholder SE segment was the last segment added before compiling
|
|
214
|
+
# This time, we directly replace the placeholder with the correct SE segment
|
|
215
|
+
formatted_837p = compiled_segments.rsplit('SE**', 1)[0] + se_segment
|
|
216
|
+
|
|
217
|
+
return formatted_837p
|
|
218
|
+
|
|
219
|
+
# =============================================================================
|
|
220
|
+
# VALIDATION UTILITIES
|
|
221
|
+
# =============================================================================
|
|
222
|
+
|
|
223
|
+
def handle_validation_errors(transaction_set_control_number, validation_errors, config):
|
|
224
|
+
"""
|
|
225
|
+
Handles validation errors with user interaction for decision making.
|
|
226
|
+
|
|
227
|
+
Parameters:
|
|
228
|
+
- transaction_set_control_number: Current transaction set control number
|
|
229
|
+
- validation_errors: List of validation errors
|
|
230
|
+
- config: Configuration for logging
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
- Boolean: True to skip patient, False to halt processing
|
|
234
|
+
"""
|
|
235
|
+
for error in validation_errors:
|
|
236
|
+
MediLink_ConfigLoader.log("Validation error for transaction set {}: {}".format(transaction_set_control_number, error), config, level="WARNING")
|
|
237
|
+
|
|
238
|
+
print("Validation errors encountered for transaction set {}. Errors: {}".format(transaction_set_control_number, validation_errors))
|
|
239
|
+
user_input = input("Skip this patient and continue without incrementing transaction set number? (yes/no): ")
|
|
240
|
+
if user_input.lower() == 'yes':
|
|
241
|
+
print("Skipping patient...")
|
|
242
|
+
MediLink_ConfigLoader.log("Skipped processing of transaction set {} due to user decision.".format(transaction_set_control_number), config, level="INFO")
|
|
243
|
+
return True # Skip the current patient
|
|
244
|
+
else:
|
|
245
|
+
print("Processing halted due to validation errors.")
|
|
246
|
+
MediLink_ConfigLoader.log("HALT: Processing halted at transaction set {} due to unresolved validation errors.".format(transaction_set_control_number), config, level="ERROR")
|
|
247
|
+
sys.exit() # Optionally halt further processing
|
|
248
|
+
|
|
249
|
+
# =============================================================================
|
|
250
|
+
# UTILITY FUNCTION REGISTRY
|
|
251
|
+
# =============================================================================
|
|
252
|
+
|
|
253
|
+
# Export all utility functions for easy importing
|
|
254
|
+
__all__ = [
|
|
255
|
+
'convert_date_format',
|
|
256
|
+
'format_datetime',
|
|
257
|
+
'get_user_confirmation',
|
|
258
|
+
'prompt_user_for_payer_id',
|
|
259
|
+
'format_claim_number',
|
|
260
|
+
'winscp_validate_output_directory',
|
|
261
|
+
'get_output_directory',
|
|
262
|
+
'generate_segment_counts',
|
|
263
|
+
'handle_validation_errors'
|
|
264
|
+
]
|
MediLink/MediLink_ClaimStatus.py
CHANGED
|
@@ -21,9 +21,10 @@ start_date_str = start_date.strftime('%m/%d/%Y')
|
|
|
21
21
|
billing_provider_tin = config['MediLink_Config'].get('billing_provider_tin')
|
|
22
22
|
|
|
23
23
|
# Define the list of payer_id's to iterate over
|
|
24
|
-
payer_ids = ['87726']
|
|
25
|
-
# Allowed payer id's for UHC
|
|
26
|
-
# payer_ids = ['87726', '03432', '96385', '95467', '86050', '86047', '95378', '
|
|
24
|
+
payer_ids = ['87726'] # Default Value
|
|
25
|
+
# Allowed payer id's for UHC 87726, 03432, 96385, 95467, 86050, 86047, 95378, 37602. This api does not support payerId 06111.
|
|
26
|
+
# payer_ids = ['87726', '03432', '96385', '95467', '86050', '86047', '95378', '37602']
|
|
27
|
+
# Oddly enough, the API is completely ignoring the payerId parameter and returning the exact same dataset for all payer IDs.
|
|
27
28
|
|
|
28
29
|
# Initialize the API client
|
|
29
30
|
client = MediLink_API_v3.APIClient()
|