medicafe 0.240415.1__py3-none-any.whl → 0.240517.0__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.

Files changed (42) hide show
  1. MediBot/MediBot.bat +198 -0
  2. MediBot/MediBot.py +346 -0
  3. MediBot/MediBot_Charges.py +28 -0
  4. MediBot/MediBot_Crosswalk_Library.py +280 -0
  5. MediBot/MediBot_Preprocessor.py +247 -0
  6. MediBot/MediBot_Preprocessor_lib.py +357 -0
  7. MediBot/MediBot_UI.py +240 -0
  8. MediBot/MediBot_dataformat_library.py +198 -0
  9. MediBot/MediBot_docx_decoder.py +80 -0
  10. MediBot/MediPost.py +5 -0
  11. MediBot/PDF_to_CSV_Cleaner.py +211 -0
  12. MediBot/__init__.py +0 -0
  13. MediBot/update_json.py +43 -0
  14. MediBot/update_medicafe.py +57 -0
  15. MediLink/MediLink.py +381 -0
  16. MediLink/MediLink_277_decoder.py +92 -0
  17. MediLink/MediLink_837p_encoder.py +502 -0
  18. MediLink/MediLink_837p_encoder_library.py +890 -0
  19. MediLink/MediLink_API_v2.py +174 -0
  20. MediLink/MediLink_APIs.py +137 -0
  21. MediLink/MediLink_ConfigLoader.py +81 -0
  22. MediLink/MediLink_DataMgmt.py +258 -0
  23. MediLink/MediLink_Down.py +128 -0
  24. MediLink/MediLink_ERA_decoder.py +192 -0
  25. MediLink/MediLink_Gmail.py +100 -0
  26. MediLink/MediLink_Mailer.py +7 -0
  27. MediLink/MediLink_Scheduler.py +173 -0
  28. MediLink/MediLink_StatusCheck.py +4 -0
  29. MediLink/MediLink_UI.py +118 -0
  30. MediLink/MediLink_Up.py +383 -0
  31. MediLink/MediLink_batch.bat +7 -0
  32. MediLink/Soumit_api.py +22 -0
  33. MediLink/__init__.py +0 -0
  34. MediLink/test.py +74 -0
  35. medicafe-0.240517.0.dist-info/METADATA +53 -0
  36. medicafe-0.240517.0.dist-info/RECORD +39 -0
  37. {medicafe-0.240415.1.dist-info → medicafe-0.240517.0.dist-info}/WHEEL +1 -1
  38. medicafe-0.240517.0.dist-info/top_level.txt +2 -0
  39. medicafe-0.240415.1.dist-info/METADATA +0 -17
  40. medicafe-0.240415.1.dist-info/RECORD +0 -5
  41. medicafe-0.240415.1.dist-info/top_level.txt +0 -1
  42. {medicafe-0.240415.1.dist-info → medicafe-0.240517.0.dist-info}/LICENSE +0 -0
@@ -0,0 +1,890 @@
1
+ from datetime import datetime
2
+ import sys
3
+ from MediLink import MediLink_ConfigLoader
4
+
5
+ # Add parent directory of the project to the Python path
6
+ import sys
7
+ import os
8
+ project_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
9
+ sys.path.append(project_dir)
10
+
11
+ from MediBot import MediBot_Preprocessor_lib
12
+ load_insurance_data_from_mains = MediBot_Preprocessor_lib.load_insurance_data_from_mains
13
+ from MediBot import MediBot_Crosswalk_Library
14
+ from MediLink_API_v2 import fetch_payer_name_from_api
15
+
16
+ """
17
+ - [ ] 1. Code Refactoring: Increase modularity and clarity, particularly in segment creation functions (e.g., `create_st_segment`, `create_nm1_billing_provider_segment`), for better maintenance and readability.
18
+ - [ ] 2. Endpoint Support: Extend support within segment creation for additional endpoints with attention to their unique claim submission requirements.
19
+ - [ ] 3. Payer Identification Mechanism: Refine the mechanism for dynamically identifying payers, leveraging payer mappings and integrating with external APIs like Availity for precise payer information retrieval.
20
+ - [ ] 4. Adherence to Endpoint-Specific Standards: Implement and verify the compliance of claim data formatting and inclusion based on the specific demands of each target endpoint within the segment creation logic.
21
+ - [ ] 5. De-persisting Intermediate Files.
22
+ - [ ] 6. Get an API for Optum "Entered As".
23
+ - [ ] 7. (MED) Add Authorization Number
24
+ - [X] 8. (HIGH) Interchange number should be 000HHMMSS instead of fixed constant.
25
+ - [ ] 9. Upgrade Interchange formation to consolidate the batch processor and also de-risk batch
26
+ interchange number matching for multiple .DAT.
27
+ """
28
+
29
+ # Converts date format from one format to another.
30
+ def convert_date_format(date_str):
31
+ # Parse the input date string into a datetime object using the input format
32
+ # Determine the input date format based on the length of the input string
33
+ input_format = "%m-%d-%Y" if len(date_str) == 10 else "%m-%d-%y"
34
+ date_obj = datetime.strptime(date_str, input_format)
35
+ # Format the datetime object into the desired output format and return
36
+ return date_obj.strftime("%Y%m%d")
37
+
38
+ # Formats date and time according to the specified format.
39
+ def format_datetime(dt=None, format_type='date'):
40
+ if dt is None:
41
+ dt = datetime.now()
42
+ if format_type == 'date':
43
+ return dt.strftime('%Y%m%d')
44
+ elif format_type == 'isa':
45
+ return dt.strftime('%y%m%d')
46
+ elif format_type == 'time':
47
+ return dt.strftime('%H%M')
48
+
49
+ # Constructs the ST segment for transaction set.
50
+ def create_st_segment(transaction_set_control_number):
51
+ return "ST*837*{:04d}*005010X222A1~".format(transaction_set_control_number)
52
+
53
+ # Constructs the BHT segment based on parsed data.
54
+ def create_bht_segment(parsed_data):
55
+ chart_number = parsed_data.get('CHART', 'UNKNOWN')
56
+ return "BHT*0019*00*{}*{}*{}*CH~".format(
57
+ chart_number, format_datetime(), format_datetime(format_type='time'))
58
+
59
+ # Constructs the HL segment for billing provider.
60
+ def create_hl_billing_provider_segment():
61
+ return "HL*1**20*1~"
62
+
63
+ # Constructs the NM1 segment for billing provider and includes address and Tax ID.
64
+ def create_nm1_billing_provider_segment(config, endpoint):
65
+ endpoint_config = config['endpoints'].get(endpoint.upper(), {})
66
+
67
+ # Billing provider details
68
+ billing_provider_entity_code = endpoint_config.get('billing_provider_entity_code', '85')
69
+ billing_provider_npi_qualifier = endpoint_config.get('billing_provider_npi_qualifier', 'XX')
70
+ #billing_provider_lastname = endpoint_config.get('billing_provider_lastname', config.get('default_billing_provider_name', 'DEFAULT NAME'))
71
+ #billing_provider_firstname = endpoint_config.get('billing_provider_firstname', '')
72
+ billing_provider_lastname = config.get('billing_provider_lastname', config.get('default_billing_provider_name', 'DEFAULT NAME'))
73
+ billing_provider_firstname = config.get('billing_provider_firstname', '')
74
+ billing_provider_npi = endpoint_config.get('billing_provider_npi', config.get('default_billing_provider_npi', 'DEFAULT NPI'))
75
+
76
+ # Determine billing_entity_type_qualifier based on the presence of billing_provider_firstname
77
+ billing_entity_type_qualifier = '1' if billing_provider_firstname else '2'
78
+
79
+ # Construct NM1 segment for the billing provider
80
+ nm1_segment = "NM1*{}*{}*{}*{}****{}*{}~".format(
81
+ billing_provider_entity_code,
82
+ billing_entity_type_qualifier,
83
+ billing_provider_lastname,
84
+ billing_provider_firstname,
85
+ billing_provider_npi_qualifier,
86
+ billing_provider_npi
87
+ )
88
+
89
+ # Construct address segments
90
+ address_segments = []
91
+ if config.get('billing_provider_address'):
92
+ # N3 segment for address line
93
+ address_segments.append("N3*{}~".format(config.get('billing_provider_address', 'NO ADDRESS')))
94
+ # N4 segment for City, State, ZIP
95
+ address_segments.append("N4*{}*{}*{}~".format(
96
+ config.get('billing_provider_city', 'NO CITY'),
97
+ config.get('billing_provider_state', 'NO STATE'),
98
+ config.get('billing_provider_zip', 'NO ZIP')
99
+ ))
100
+
101
+ # Assuming Tax ID is part of the same loop, otherwise move REF segment to the correct loop
102
+ ref_segment = "REF*EI*{}~".format(config.get('billing_provider_tin', 'NO TAX ID'))
103
+
104
+ # Construct PRV segment if provider taxonomy is needed, are these just for Medicaid??
105
+ #prv_segment = ""
106
+ #if config.get('billing_provider_taxonomy'):
107
+ # prv_segment = "PRV*BI*PXC*{}~".format(config.get('billing_provider_taxonomy'))
108
+
109
+ # Combine all the segments in the correct order, I think the PRV goes after the address and/or after ref
110
+ segments = [nm1_segment]
111
+ #if prv_segment:
112
+ # segments.append(prv_segment)
113
+ segments.extend(address_segments)
114
+ segments.append(ref_segment)
115
+
116
+ return segments
117
+
118
+ # Constructs the NM1 segment and accompanying details for the service facility location.
119
+ def create_service_facility_location_npi_segment(config):
120
+ """
121
+ Constructs segments for the service facility location, including the NM1 segment for identification
122
+ and accompanying N3 and N4 segments for address details.
123
+ """
124
+ facility_npi = config.get('service_facility_npi', 'DEFAULT FACILITY NPI')
125
+ facility_name = config.get('service_facility_name', 'DEFAULT FACILITY NAME')
126
+ address_line_1 = config.get('service_facility_address', 'NO ADDRESS')
127
+ city = config.get('service_facility_city', 'NO CITY')
128
+ state = config.get('service_facility_state', 'NO STATE')
129
+ zip_code = config.get('service_facility_zip', 'NO ZIP')
130
+
131
+ # NM1 segment for facility identification
132
+ nm1_segment = "NM1*77*2*{}*****XX*{}~".format(facility_name, facility_npi)
133
+ # N3 segment for facility address
134
+ n3_segment = "N3*{}~".format(address_line_1)
135
+ # N4 segment for facility city, state, and ZIP
136
+ n4_segment = "N4*{}*{}*{}~".format(city, state, zip_code)
137
+
138
+ return [nm1_segment, n3_segment, n4_segment]
139
+
140
+ # Constructs the NM1 segment for submitter name and includes PER segment for contact information.
141
+ def create_1000A_submitter_name_segment(patient_data, config, endpoint):
142
+ """
143
+ Creates the 1000A submitter name segment, including the PER segment for contact information.
144
+
145
+ Args:
146
+ patient_data (dict): Enriched patient data containing payer information.
147
+ config (dict): Configuration settings.
148
+ endpoint (str): Intended Endpoint for resolving submitter information.
149
+
150
+ Returns:
151
+ list: A list containing the NM1 segment for the submitter name and the PER segment for contact information.
152
+ """
153
+ endpoint_config = config['endpoints'].get(endpoint.upper(), {})
154
+ submitter_id_qualifier = endpoint_config.get('submitter_id_qualifier', '46') # '46' for ETIN or 'XX' for NPI
155
+ submitter_name = endpoint_config.get('nm_103_value', 'DEFAULT NAME') # Default name if not in config
156
+
157
+ # Extract payer_id from patient_data
158
+ payer_id = patient_data.get('payer_id', '')
159
+
160
+ # Check if payer_id is Florida Blue (00590 or BCBSF) and assign submitter_id accordingly
161
+ if payer_id in ['00590', 'BCBSF']:
162
+ submitter_id = endpoint_config.get('nm_109_bcbsf', 'DEFAULT BCBSF ID')
163
+ else:
164
+ submitter_id = endpoint_config.get('nm_109_value', 'DEFAULT ID') # Default ID if not specified in endpoint
165
+
166
+ # Submitter contact details
167
+ contact_name = config.get('submitter_name', 'NONE')
168
+ contact_telephone_number = config.get('submitter_tel', 'NONE')
169
+
170
+ # Construct NM1 segment for the submitter
171
+ nm1_segment = "NM1*41*2*{}*****{}*{}~".format(submitter_name, submitter_id_qualifier, submitter_id)
172
+
173
+ # Construct PER segment for the submitter's contact information
174
+ per_segment = "PER*IC*{}*TE*{}~".format(contact_name, contact_telephone_number)
175
+
176
+ return [nm1_segment, per_segment]
177
+
178
+ # Constructs the NM1 segment for the receiver (1000B).
179
+ def create_1000B_receiver_name_segment(config, endpoint):
180
+ # Retrieve endpoint specific configuration
181
+ endpoint_config = config['endpoints'].get(endpoint.upper(), {})
182
+
183
+ # Set the entity identifier code to '40' for receiver and qualifier to '46' for EDI,
184
+ # unless specified differently in the endpoint configuration.
185
+ receiver_entity_code = '40'
186
+ receiver_id_qualifier = endpoint_config.get('receiver_id_qualifier', '46')
187
+ receiver_name = endpoint_config.get('receiver_name', 'DEFAULT RECEIVER NAME')
188
+ receiver_edi = endpoint_config.get('receiver_edi', 'DEFAULT EDI')
189
+
190
+ return "NM1*{entity_code}*2*{receiver_name}*****{id_qualifier}*{receiver_edi}~".format(
191
+ entity_code=receiver_entity_code,
192
+ receiver_name=receiver_name,
193
+ id_qualifier=receiver_id_qualifier,
194
+ receiver_edi=receiver_edi
195
+ )
196
+
197
+ def payer_id_to_payer_name(parsed_data, config, endpoint):
198
+ """
199
+ Preprocesses payer information from parsed data and enriches parsed_data with the payer name and ID.
200
+
201
+ Args:
202
+ parsed_data (dict): Parsed data containing Z-dat information.
203
+ config (dict): Configuration settings.
204
+ endpoint (str): Intended Endpoint for resolving payer information.
205
+
206
+ Returns:
207
+ dict: Enriched parsed data with payer name and payer ID.
208
+ """
209
+ # Step 1: Extract insurance name from parsed data
210
+ insurance_name = parsed_data.get('INAME', '')
211
+
212
+ # Step 2: Map insurance name to payer ID
213
+ payer_id = map_insurance_name_to_payer_id(insurance_name, config)
214
+
215
+ # Step 3: Validate payer_id
216
+ if payer_id is None:
217
+ error_message = "Payer ID for '{}' cannot be None.".format(insurance_name)
218
+ MediLink_ConfigLoader.log(error_message, level="WARNING")
219
+ raise ValueError(error_message)
220
+
221
+ # Step 4: Resolve payer name using payer ID
222
+ payer_name = resolve_payer_name(payer_id, config, endpoint, insurance_name, parsed_data)
223
+
224
+ # Enrich parsed_data with payer name and payer ID
225
+ parsed_data['payer_name'] = payer_name
226
+ parsed_data['payer_id'] = payer_id
227
+
228
+ return parsed_data
229
+
230
+ # Then you can use the enriched parsed_data in your main function
231
+ def create_2010BB_payer_information_segment(parsed_data):
232
+ """
233
+ Creates the 2010BB payer information segment.
234
+
235
+ Args:
236
+ parsed_data (dict): Parsed data containing enriched payer information.
237
+
238
+ Returns:
239
+ str: The 2010BB payer information segment.
240
+ """
241
+ # Extract enriched payer name and payer ID
242
+ payer_name = parsed_data.get('payer_name')
243
+ payer_id = parsed_data.get('payer_id')
244
+
245
+ # Validate payer_name and payer_id
246
+ if not payer_name or not payer_id:
247
+ error_message = "Payer name and payer ID must be provided."
248
+ raise ValueError(error_message)
249
+
250
+ # Build NM1 segment using provided payer name and payer ID
251
+ return build_nm1_segment(payer_name, payer_id)
252
+
253
+ def resolve_payer_name(payer_id, config, primary_endpoint, insurance_name, parsed_data):
254
+ """
255
+ Resolves the payer name using the provided payer ID.
256
+
257
+ Args:
258
+ payer_id (str): The ID of the payer.
259
+ config (dict): Configuration settings.
260
+ primary_endpoint (str): The primary endpoint for resolving payer information.
261
+ insurance_name (str): The name of the insurance.
262
+ parsed_data (dict): Parsed data containing patient information.
263
+
264
+ Returns:
265
+ str: The resolved payer name.
266
+ """
267
+
268
+ # Step 1: Attempt to fetch payer name from API using primary endpoint
269
+ MediLink_ConfigLoader.log("Attempting to resolve Payer ID {} via API.".format(payer_id), level="INFO")
270
+ try:
271
+ return fetch_payer_name_from_api(payer_id, config, primary_endpoint)
272
+ except Exception as api_error:
273
+ # Step 2: Log API resolution failure and initiate user intervention
274
+ MediLink_ConfigLoader.log("API resolution failed for {}: {}. Initiating user intervention.".format(payer_id, str(api_error)), config, level="WARNING")
275
+
276
+ # Step 3: Print warning message for user intervention
277
+ print("\n\nWARNING: Unable to verify Payer ID '{}' for patient '{}'!".format(payer_id, parsed_data.get('CHART', 'unknown')))
278
+ print(" Claims for '{}' may be incorrectly\nrouted or fail without intervention.".format(insurance_name))
279
+ print("\nACTION REQUIRED: Please verify the internet connection and the Payer ID by searching for it")
280
+ print("at the expected endpoint's website or using Google.")
281
+ print("\nNote: If the Payer ID '{}' is incorrect for '{}', \nit may need to be manually corrected.".format(payer_id, insurance_name))
282
+ print("If the Payer ID appears correct, you may skip the correction and force-continue with this one.")
283
+ print("\nPlease check the Payer ID in the Crosswalk and the initial \ndata source (e.g., Carol's CSV) as needed.")
284
+ print("If unsure, llamar a Dani for guidance on manual corrections.")
285
+
286
+ # Step 4: Integrate user input logic
287
+ user_decision = input("\nType 'FORCE' to force-continue with the Medisoft name, \nor press Enter to pause processing and make corrections: ")
288
+ user_decision = user_decision.strip().lower() # Convert to lowercase and remove leading/trailing whitespace
289
+ if user_decision == 'force':
290
+ # Step 5: Fallback to truncated insurance name
291
+ truncated_name = insurance_name[:10] # Temporary fallback
292
+ MediLink_ConfigLoader.log("Using truncated insurance name '{}' as a fallback for {}".format(truncated_name, payer_id), config, level="WARNING")
293
+ return truncated_name
294
+ elif not user_decision: # Check if user pressed Enter
295
+ corrected_payer_id = prompt_user_for_payer_id(insurance_name)
296
+ if corrected_payer_id:
297
+ try:
298
+ resolved_name = fetch_payer_name_from_api(corrected_payer_id, config, primary_endpoint)
299
+ print("API resolved to insurance name: {}".format(resolved_name))
300
+ MediLink_ConfigLoader.log("API Resolved to standard insurance name: {} for corrected payer ID: {}".format(resolved_name, corrected_payer_id), config, level="INFO")
301
+
302
+ confirmation = input("Is the standard insurance name '{}' correct? (yes/no): ".format(resolved_name)).strip().lower()
303
+
304
+ if confirmation in ['yes', 'y']:
305
+ if MediBot_Crosswalk_Library.update_crosswalk_with_corrected_payer_id(payer_id, corrected_payer_id):
306
+ return resolved_name
307
+ else:
308
+ print("Failed to update crosswalk with the corrected Payer ID.")
309
+ exit(1) # probably needs a different failure direction here.
310
+ else:
311
+ print("User did not confirm the standard insurance name. Manual intervention is required.")
312
+ MediLink_ConfigLoader.log("User did not confirm the standard insurance name. Manual intervention is required.", config, level="CRITICAL")
313
+ exit(1) # probably needs a different failure direction here.
314
+ except Exception as e:
315
+ print("Failed to resolve corrected payer ID to standard insurance name: {}".format(e))
316
+ MediLink_ConfigLoader.log("Failed to resolve corrected payer ID to standard insurance name: {}".format(e), config, level="ERROR")
317
+ exit(1) # probably needs a different failure direction here.
318
+ else:
319
+ print("Exiting script. Please make the necessary corrections and retry.")
320
+ exit(1) # probably needs a different failure direction here.
321
+
322
+ def prompt_user_for_payer_id(insurance_name):
323
+ """
324
+ Prompts the user to input the payer ID manually.
325
+ """
326
+ print("Manual intervention required: No payer ID found for insurance name '{}'.".format(insurance_name))
327
+ payer_id = input("Please enter the payer ID manually: ").strip()
328
+ return payer_id
329
+
330
+ def build_nm1_segment(payer_name, payer_id):
331
+ # Step 1: Build NM1 segment using payer name and ID
332
+ return "NM1*PR*2*{}*****PI*{}~".format(payer_name, payer_id)
333
+
334
+ def map_insurance_name_to_payer_id(insurance_name, config):
335
+ """
336
+ Maps insurance name to payer ID using the crosswalk configuration.
337
+
338
+ Args:
339
+ insurance_name (str): Name of the insurance.
340
+ config (dict): Configuration settings.
341
+
342
+ Returns:
343
+ str: The payer ID corresponding to the insurance name.
344
+ """
345
+ try:
346
+ # Ensure crosswalk is initialized and 'payer_id' key is available
347
+ MediBot_Crosswalk_Library.check_and_initialize_crosswalk(config)
348
+
349
+ # Load crosswalk configuration
350
+ _, crosswalk = MediLink_ConfigLoader.load_configuration(None, config.get('crosswalkPath', 'crosswalk.json'))
351
+
352
+ # Load insurance data from MAINS to get insurance ID
353
+ insurance_to_id = load_insurance_data_from_mains(config)
354
+
355
+ # Get medisoft ID corresponding to the insurance name
356
+ medisoft_id = insurance_to_id.get(insurance_name)
357
+ if medisoft_id is None:
358
+ error_message = "No Medisoft ID found for insurance name: {}. Consider checking MAINS directly.".format(insurance_name)
359
+ MediLink_ConfigLoader.log(error_message, config, level="ERROR")
360
+ raise ValueError(error_message)
361
+
362
+ # Convert medisoft_id to string to match the JSON data type
363
+ medisoft_id_str = str(medisoft_id)
364
+
365
+ # Get payer ID corresponding to the medisoft ID
366
+ payer_id = None
367
+ for payer, payer_info in crosswalk['payer_id'].items():
368
+ if medisoft_id_str in payer_info['medisoft_id']:
369
+ payer_id = payer
370
+ break
371
+
372
+ # Handle the case where no payer ID is found
373
+ if payer_id is None:
374
+ error_message = "No payer ID found for Medisoft ID: {}".format(medisoft_id)
375
+ MediLink_ConfigLoader.log(error_message, config, level="ERROR")
376
+ print(error_message)
377
+ payer_id = handle_missing_payer_id(insurance_name, config)
378
+
379
+ return payer_id
380
+
381
+ except ValueError as e:
382
+ if "JSON" in str(e) and "decode" in str(e):
383
+ error_message = "Error decoding the crosswalk JSON file in map_insurance_name_to_payer_id"
384
+ MediLink_ConfigLoader.log(error_message, config, level="CRITICAL")
385
+ raise ValueError(error_message)
386
+ else:
387
+ error_message = "Unexpected error in map_insurance_name_to_payer_id: {}".format(e)
388
+ MediLink_ConfigLoader.log(error_message, config, level="ERROR")
389
+ raise e
390
+
391
+ def handle_missing_payer_id(insurance_name, config):
392
+ """
393
+ Handles cases where the payer ID is not found for a given Medisoft ID.
394
+
395
+ """
396
+ print("Missing Payer ID for insurance name: {}".format(insurance_name))
397
+ MediLink_ConfigLoader.log("Missing Payer ID for insurance name: {}".format(insurance_name), config, level="WARNING")
398
+
399
+ # Prompt the user for manual intervention to input the payer ID
400
+ payer_id = prompt_user_for_payer_id(insurance_name)
401
+
402
+ if not payer_id:
403
+ message = "Unable to resolve missing Payer ID. Manual intervention is required."
404
+ MediLink_ConfigLoader.log(message, config, level="CRITICAL")
405
+ return None
406
+
407
+ # Resolve the payer ID to a standard insurance name via API
408
+ try:
409
+ # primary_endpoint=None should kick to the default in the api function.
410
+ standard_insurance_name = resolve_payer_name(payer_id, config, primary_endpoint=None, insurance_name=insurance_name, parsed_data={})
411
+ message = "Resolved to standard insurance name: {} for payer ID: {}".format(standard_insurance_name, payer_id)
412
+ print(message)
413
+ MediLink_ConfigLoader.log(message, config, level="INFO")
414
+ except Exception as e:
415
+ message = "Failed to resolve payer ID to standard insurance name: {}".format(e)
416
+ print(message)
417
+ MediLink_ConfigLoader.log(message, config, level="ERROR")
418
+ return None
419
+
420
+ # Ask for user confirmation
421
+ confirmation = input("Is the standard insurance name '{}' correct? (yes/no): ".format(standard_insurance_name)).strip().lower() or 'yes'
422
+
423
+ if confirmation in ['yes', 'y']:
424
+ # Update the crosswalk with the new payer ID and insurance name mapping
425
+ MediBot_Crosswalk_Library.update_crosswalk_with_new_payer_id(insurance_name, payer_id, config)
426
+ return payer_id
427
+ else:
428
+ print("User did not confirm the standard insurance name. Manual intervention is required.")
429
+ MediLink_ConfigLoader.log("User did not confirm the standard insurance name. Manual intervention is required.", config, level="CRITICAL")
430
+ return None
431
+
432
+ def create_nm1_payto_address_segments(config):
433
+ """
434
+ Constructs the NM1 segment for the Pay-To Address, N3 for street address, and N4 for city, state, and ZIP.
435
+ This is used if the Pay-To Address is different from the Billing Provider Address.
436
+ """
437
+ payto_provider_name = config.get('payto_provider_name', 'DEFAULT PAY-TO NAME')
438
+ payto_address = config.get('payto_address', 'DEFAULT PAY-TO ADDRESS')
439
+ payto_city = config.get('payto_city', 'DEFAULT PAY-TO CITY')
440
+ payto_state = config.get('payto_state', 'DEFAULT PAY-TO STATE')
441
+ payto_zip = config.get('payto_zip', 'DEFAULT PAY-TO ZIP')
442
+
443
+ nm1_segment = "NM1*87*2*{}~".format(payto_provider_name) # '87' indicates Pay-To Provider
444
+ n3_segment = "N3*{}~".format(payto_address)
445
+ n4_segment = "N4*{}*{}*{}~".format(payto_city, payto_state, payto_zip)
446
+
447
+ return [nm1_segment, n3_segment, n4_segment]
448
+
449
+ def create_payer_address_segments(config):
450
+ """
451
+ Constructs the N3 and N4 segments for the payer's address.
452
+
453
+ """
454
+ payer_address_line_1 = config.get('payer_address_line_1', '')
455
+ payer_city = config.get('payer_city', '')
456
+ payer_state = config.get('payer_state', '')
457
+ payer_zip = config.get('payer_zip', '')
458
+
459
+ n3_segment = "N3*{}~".format(payer_address_line_1)
460
+ n4_segment = "N4*{}*{}*{}~".format(payer_city, payer_state, payer_zip)
461
+
462
+ return [n3_segment, n4_segment]
463
+
464
+ # Constructs the PRV segment for billing provider.
465
+ def create_billing_prv_segment(config, endpoint):
466
+ if endpoint.lower() == 'optumedi':
467
+ return "PRV*BI*PXC*{}~".format(config['billing_provider_taxonomy'])
468
+ return ""
469
+
470
+ # Constructs the HL segment for subscriber [hierarchical level (HL*2)]
471
+ def create_hl_subscriber_segment():
472
+ return ["HL*2*1*22*0~"]
473
+
474
+ # (2000B Loop) Constructs the SBR segment for subscriber based on parsed data and configuration.
475
+ def create_sbr_segment(config, parsed_data, endpoint):
476
+ # Determine the payer responsibility sequence number code based on the payer type
477
+ # If the payer is Medicare, use 'P' (Primary)
478
+ # If the payer is not Medicare and is primary insurance, use 'P' (Primary)
479
+ # If the payer is secondary insurance after Medicare, use 'S' (Secondary)
480
+ # Assume everything is Primary for now.
481
+ responsibility_code = 'P'
482
+
483
+ # Insurance Type Code
484
+ insurance_type_code = insurance_type_selection(parsed_data)
485
+
486
+ # Construct the SBR segment using the determined codes
487
+ sbr_segment = "SBR*{responsibility_code}*18*******{insurance_type_code}~".format(
488
+ responsibility_code=responsibility_code,
489
+ insurance_type_code=insurance_type_code
490
+ )
491
+ return sbr_segment
492
+
493
+ def insurance_type_selection(parsed_data):
494
+ """
495
+ Quick Fix: This funtion selects the insurance type for a patient based on predefined codes.
496
+
497
+ TODO (HIGH SBR09) Finish making this function.
498
+ This should eventually integrate into a menu upstream. This menu flow probably needs to be alongside the suggested endpoint flow.
499
+ For now let's make a menu here and then figure out the automated/API method to getting this per patient as there are no
500
+ useful historical patterns other than to say that the default should be PPO (12), then CI, then FI as most common options,
501
+ followed by the rest.
502
+
503
+ Present a user-selectable menu of insurance types based on predefined codes.
504
+ User needs to input the desired 2 character code for that patient or default to PPO.
505
+
506
+ Currently implements a simple text-based selection menu to choose
507
+ an insurance type for a patient. The default selection is '12' for PPO, but the user
508
+ can input other codes as per the options listed. This initial implementation uses
509
+ simple input/output functions for selection and can be replaced in the future by a
510
+ more dynamic interface or API-driven selection method.
511
+
512
+ Future Enhancements:
513
+ - Automate selection via an API that fetches patient data directly and auto-assigns the insurance type.
514
+ - Error handling to manage incorrect inputs and retry mechanisms.
515
+
516
+ Parameters:
517
+ - parsed_data (dict): Contains patient's last name under key 'LAST'.
518
+
519
+ Returns:
520
+ - str: The insurance type code selected by the user.
521
+
522
+ """
523
+ print("\nHorrible Temporary Menu: Select the insurance type for patient {}: ".format(parsed_data['LAST']))
524
+
525
+ # Define insurance options with codes and descriptions
526
+ insurance_options = {
527
+ "11": "Other Non-Federal Programs",
528
+ "12": "Preferred Provider Organization (PPO)",
529
+ "13": "Point of Service (POS)",
530
+ "14": "Exclusive Provider Organization (EPO)",
531
+ "15": "Indemnity Insurance",
532
+ "16": "Health Maintenance Organization (HMO) Medicare Risk",
533
+ "17": "Dental Maintenance Organization",
534
+ "AM": "Automobile Medical",
535
+ "BL": "Blue Cross/Blue Shield",
536
+ "CH": "Champus",
537
+ "CI": "Commercial Insurance Co.",
538
+ "DS": "Disability",
539
+ "FI": "Federal Employees Program",
540
+ "HM": "Health Maintenance Organization",
541
+ "LM": "Liability Medical",
542
+ "MA": "Medicare Part A",
543
+ "MB": "Medicare Part B",
544
+ "MC": "Medicaid",
545
+ "OF": "Other Federal Program",
546
+ "TV": "Title V",
547
+ "VA": "Veterans Affairs Plan",
548
+ "WC": "Workers Compensation Health Claim",
549
+ "ZZ": "Mutually Defined"
550
+ }
551
+
552
+ # Function to display full list of insurance options
553
+ def display_insurance_options(options):
554
+ print("Insurance Type Options:")
555
+ # Sorting the dictionary keys to ensure consistent order
556
+ sorted_keys = sorted(options.keys())
557
+ for code in sorted_keys:
558
+ description = options[code]
559
+ print("{} - {}".format(code, description))
560
+
561
+ def prompt_display_insurance_options():
562
+ # Prompt to display full list
563
+ display_full_list = input("Do you want to see the full list of insurance options? (yes/no): ").strip().lower()
564
+
565
+ # Display full list if user confirms
566
+ if display_full_list in ['yes', 'y']:
567
+ display_insurance_options(insurance_options)
568
+
569
+ # Horrible menu
570
+ prompt_display_insurance_options()
571
+
572
+ # Default selection
573
+ insurance_type_code = '12'
574
+
575
+ # User input for insurance type
576
+ user_input = input("Enter the 2-character code for the insurance type (or press Enter to default to '12' for PPO): ").strip().upper()
577
+
578
+ # Validate input and set the insurance type code
579
+ if user_input in insurance_options:
580
+ insurance_type_code = user_input
581
+ else:
582
+ print("Skipped or Input not recognized. Defaulting to Preferred Provider Organization (PPO)\n")
583
+
584
+ return insurance_type_code
585
+
586
+ # Constructs the NM1 segment for subscriber based on parsed data and configuration.
587
+ def create_nm1_subscriber_segment(config, parsed_data, endpoint):
588
+ if endpoint.lower() == 'optumedi':
589
+ entity_identifier_code = config['endpoints']['OPTUMEDI'].get('subscriber_entity_code', 'IL')
590
+ else:
591
+ entity_identifier_code = 'IL' # Default value if endpoint is not 'optumedi'
592
+
593
+ return "NM1*{entity_identifier_code}*1*{last_name}*{first_name}*{middle_name}***MI*{policy_number}~".format(
594
+ entity_identifier_code=entity_identifier_code,
595
+ last_name=parsed_data['LAST'],
596
+ first_name=parsed_data['FIRST'],
597
+ middle_name=parsed_data['MIDDLE'],
598
+ policy_number=parsed_data['IPOLICY']
599
+ )
600
+
601
+ # Constructs the N3 and N4 segments for subscriber address based on parsed data.
602
+ def create_subscriber_address_segments(parsed_data):
603
+ return [
604
+ "N3*{}~".format(parsed_data['ISTREET']),
605
+ "N4*{}*{}*{}~".format(
606
+ parsed_data['ICITY'],
607
+ parsed_data['ISTATE'],
608
+ parsed_data['IZIP'][:5]
609
+ )
610
+ ]
611
+
612
+ # Constructs the DMG segment for subscriber based on parsed data.
613
+ def create_dmg_segment(parsed_data):
614
+ return "DMG*D8*{}*{}~".format(
615
+ convert_date_format(parsed_data['BDAY']),
616
+ parsed_data['SEX'] # ensure it returns a string instead of a list if it only returns one segment
617
+ )
618
+
619
+ def create_nm1_rendering_provider_segment(config, is_rendering_provider_different=False):
620
+ """
621
+ # BUG This is getting placed incorrectly. Has this been fixed??
622
+
623
+ Constructs the NM1 segment for the rendering provider in the 2310B loop using configuration data.
624
+
625
+ Parameters:
626
+ - config: Configuration dictionary including rendering provider details.
627
+ - is_rendering_provider_different: Boolean indicating if rendering provider differs from billing provider.
628
+
629
+ Returns:
630
+ - List containing the NM1 segment for the rendering provider if required, otherwise an empty list.
631
+ """
632
+ if is_rendering_provider_different:
633
+ segments = []
634
+ rp_npi = config.get('rendering_provider_npi', '')
635
+ rp_last_name = config.get('rendering_provider_last_name', '')
636
+ rp_first_name = config.get('rendering_provider_first_name', '')
637
+
638
+ # NM1 Segment for Rendering Provider
639
+ segments.append("NM1*82*1*{0}*{1}**XX*{2}~".format(
640
+ rp_last_name,
641
+ rp_first_name,
642
+ rp_npi
643
+ ))
644
+
645
+ # PRV Segment for Rendering Provider Taxonomy
646
+ if config.get('rendering_provider_taxonomy'):
647
+ segments.append("PRV*PE*PXC*{}~".format(config['billing_provider_taxonomy']))
648
+
649
+ return segments
650
+ else:
651
+ return []
652
+
653
+ def format_claim_number(chart_number, date_of_service):
654
+ # Remove any non-alphanumeric characters from chart number and date
655
+ chart_number_alphanumeric = ''.join(filter(str.isalnum, chart_number))
656
+ date_of_service_alphanumeric = ''.join(filter(str.isalnum, date_of_service))
657
+
658
+ # Combine the alphanumeric components without spaces
659
+ formatted_claim_number = chart_number_alphanumeric + date_of_service_alphanumeric
660
+
661
+ return formatted_claim_number
662
+
663
+ # Constructs the CLM and related segments based on parsed data and configuration.
664
+ def create_clm_and_related_segments(parsed_data, config):
665
+ """
666
+ Insert the claim information (2300 loop),
667
+ ensuring that details such as claim ID, total charge amount,and service date are included.
668
+
669
+ The HI segment for Health Care Diagnosis Codes should accurately reflect the diagnosis related to the service line.
670
+
671
+ Service Line Information (2400 Loop):
672
+ Verify that the service line number (LX), professional service (SV1), and service date (DTP) segments contain
673
+ accurate information and are formatted according to the claim's details.
674
+ """
675
+
676
+ segments = []
677
+
678
+ # Format the claim number
679
+ chart_number = parsed_data.get('CHART', '')
680
+ date_of_service = parsed_data.get('DATE', '')
681
+ formatted_claim_number = format_claim_number(chart_number, date_of_service)
682
+
683
+ # CLM - Claim Information
684
+ segments.append("CLM*{}*{}***{}:B:1*Y*A*Y*Y~".format(
685
+ formatted_claim_number,
686
+ parsed_data['AMOUNT'],
687
+ parsed_data['TOS']))
688
+
689
+ # HI - Health Care Diagnosis Code
690
+ # The code value should be preceded by the qualifier "ABK" or "BK" to specify the type of
691
+ # diagnosis code being used. "ABK" typically indicates ICD-10 codes, while "BK" indicates ICD-9 codes.
692
+ segments.append("HI*ABK:H{}~".format(''.join(char for char in parsed_data['DIAG'] if char.isalnum() or char.isdigit())))
693
+
694
+ # (2310C Loop) Service Facility Location NPI and Address Information
695
+ segments.extend(create_service_facility_location_npi_segment(config))
696
+
697
+ # For future reference, SBR - (Loop 2320: OI, NM1 (2330A), N3, N4, NM1 (2330B)) - Other Subscriber Information goes here.
698
+
699
+ # LX - Service Line Number
700
+ segments.append("LX*1~")
701
+
702
+ # SV1 - Professional Service
703
+ segments.append("SV1*HC:{}:{}*{}*MJ*{}***1~".format(
704
+ parsed_data['CODEA'],
705
+ parsed_data['POS'],
706
+ parsed_data['AMOUNT'],
707
+ parsed_data['MINTUES']))
708
+
709
+ # DTP - Date
710
+ segments.append("DTP*472*D8*{}~".format(convert_date_format(parsed_data['DATE'])))
711
+
712
+ # Is there REF - Line Item Control Number missing here?
713
+
714
+ return segments
715
+
716
+ def get_endpoint_config(config, endpoint):
717
+ endpoint_config = config['endpoints'].get(endpoint.upper(), {})
718
+ if not endpoint_config:
719
+ print("Endpoint configuration for {} not found.".format(endpoint))
720
+ return None
721
+ return endpoint_config
722
+
723
+ def create_interchange_elements(config, endpoint, transaction_set_control_number):
724
+ """
725
+ Create interchange headers and trailers for an 837P document.
726
+
727
+ Parameters:
728
+ - config: Configuration settings loaded from a JSON file.
729
+ - endpoint: The endpoint for which the data is being processed.
730
+ - transaction_set_control_number: The starting transaction set control number.
731
+
732
+ Returns:
733
+ - Tuple containing (ISA header, GS header, GE trailer, IEA trailer).
734
+ """
735
+ endpoint_config = get_endpoint_config(config, endpoint)
736
+
737
+ # Get the current system time and format it as 'HHMMSS' in 24-hour clock.
738
+ current_time = datetime.now().strftime('%H%M%S')
739
+ isa13 = '000' + current_time # Format ISA13 with '000HHMMSS'.
740
+
741
+ # Check if isa13 could not be generated from the current time
742
+ if len(isa13) != 9:
743
+ # If isa13 cannot be generated from the current time, use the configured value.
744
+ isa13 = endpoint_config.get('isa_13_value', '000000001')
745
+
746
+ # Create interchange header and trailer using provided library functions.
747
+ isa_header, gs_header = create_interchange_header(config, endpoint, isa13)
748
+ ge_trailer, iea_trailer = create_interchange_trailer(config, transaction_set_control_number, isa13)
749
+
750
+ return isa_header, gs_header, ge_trailer, iea_trailer
751
+
752
+ # Generates the ISA and GS segments for the interchange header based on configuration and endpoint.
753
+ def create_interchange_header(config, endpoint, isa13):
754
+ """
755
+ Generate ISA and GS segments for the interchange header, ensuring endpoint-specific requirements are met.
756
+ Includes support for Availity, Optum, and PNT_DATA endpoints, with streamlined configuration and default handling.
757
+
758
+ Parameters:
759
+ - config: Configuration dictionary with settings and identifiers.
760
+ - endpoint: String indicating the target endpoint ('Availity', 'Optum', 'PNT_DATA').
761
+ - isa13: The ISA13 field value representing the current system time.
762
+
763
+ Returns:
764
+ - Tuple containing the ISA and GS segment strings.
765
+ """
766
+ endpoint_config = config['endpoints'].get(endpoint.upper(), {})
767
+
768
+ # Set defaults for ISA segment values
769
+ isa02 = isa04 = " " # Default value for ISA02 and ISA04
770
+ isa05 = isa07 = 'ZZ' # Default qualifier
771
+ isa15 = 'P' # 'T' for Test, 'P' for Production
772
+
773
+ # Conditional values from config
774
+ isa_sender_id = endpoint_config.get('isa_06_value', config.get('submitterId', '')).rstrip()
775
+ isa07_value = endpoint_config.get('isa_07_value', isa07)
776
+ isa_receiver_id = endpoint_config.get('isa_08_value', config.get('receiverId', '')).rstrip()
777
+ gs_sender_code = endpoint_config.get('gs_02_value', config.get('submitterEdi', ''))
778
+ gs_receiver_code = endpoint_config.get('gs_03_value', config.get('receiverEdi', ''))
779
+ isa15_value = endpoint_config.get('isa_15_value', isa15)
780
+
781
+ # ISA Segment
782
+ isa_segment = "ISA*00*{}*00*{}*{}*{}*{}*{}*{}*{}*^*00501*{}*0*{}*:~".format(
783
+ isa02, isa04, isa05, isa_sender_id.ljust(15), isa07_value, isa_receiver_id.ljust(15),
784
+ format_datetime(format_type='isa'), format_datetime(format_type='time'), isa13, isa15_value
785
+ )
786
+
787
+ # GS Segment
788
+ # GS04 YYYYMMDD
789
+ # GS06 Group Control Number, Field Length 1/9, must match GE02
790
+ # BUG probably move up a function.
791
+ gs06 = '1' # Placeholder for now?
792
+
793
+ gs_segment = "GS*HC*{}*{}*{}*{}*{}*X*005010X222A1~".format(
794
+ gs_sender_code, gs_receiver_code, format_datetime(), format_datetime(format_type='time'), gs06
795
+ )
796
+
797
+ MediLink_ConfigLoader.log("Created interchange header for endpoint: {}".format(endpoint), config, level="INFO")
798
+
799
+ return isa_segment, gs_segment
800
+
801
+ # Generates the GE and IEA segments for the interchange trailer based on the number of transactions and functional groups.
802
+ def create_interchange_trailer(config, num_transactions, isa13, num_functional_groups=1):
803
+ """
804
+ Generate GE and IEA segments for the interchange trailer.
805
+
806
+ Parameters:
807
+ - config: Configuration dictionary with settings and identifiers.
808
+ - num_transactions: The number of transactions within the functional group.
809
+ - isa13: The ISA13 field value representing the current system time.
810
+ - num_functional_groups: The number of functional groups within the interchange. Default is 1.
811
+
812
+ Returns:
813
+ - Tuple containing the GE and IEA segment strings.
814
+ """
815
+
816
+ # GE Segment: Functional Group Trailer
817
+ # Indicates the end of a functional group and provides the count of the number of transactions within it.
818
+
819
+ # GE02 Group Control Number, Field Length 1/9, must match GS06 (Header)
820
+ # TODO This gs/ge matching should probably move up a function like isa13.
821
+ ge02 = '1' #Placeholder for now?
822
+ ge_segment = "GE*{}*{}~".format(num_transactions, ge02)
823
+
824
+ # IEA Segment: Interchange Control Trailer (Note: IEA02 needs to equal ISA13)
825
+ iea_segment = "IEA*{}*{}~".format(num_functional_groups, isa13)
826
+
827
+ MediLink_ConfigLoader.log("Created interchange trailer", config, level="INFO")
828
+
829
+ return ge_segment, iea_segment
830
+
831
+ # Generates segment counts for the formatted 837P transaction and updates SE segment.
832
+ def generate_segment_counts(compiled_segments, transaction_set_control_number):
833
+ # Count the number of segments, not including the placeholder SE segment
834
+ segment_count = compiled_segments.count('~') # + 1 Including SE segment itself, but seems to be giving errors.
835
+
836
+ # Ensure transaction set control number is correctly formatted as a string
837
+ formatted_control_number = str(transaction_set_control_number).zfill(4) # Pad to ensure minimum 4 characters
838
+
839
+ # Construct the SE segment with the actual segment count and the formatted transaction set control_number
840
+ se_segment = "SE*{0}*{1}~".format(segment_count, formatted_control_number)
841
+
842
+ # Assuming the placeholder SE segment was the last segment added before compiling
843
+ # This time, we directly replace the placeholder with the correct SE segment
844
+ formatted_837p = compiled_segments.rsplit('SE**', 1)[0] + se_segment
845
+
846
+ return formatted_837p
847
+
848
+ def handle_validation_errors(transaction_set_control_number, validation_errors, config):
849
+ for error in validation_errors:
850
+ MediLink_ConfigLoader.log("Validation error for transaction set {}: {}".format(transaction_set_control_number, error), config, level="WARNING")
851
+
852
+ print("Validation errors encountered for transaction set {}. Errors: {}".format(transaction_set_control_number, validation_errors))
853
+ user_input = input("Skip this patient and continue without incrementing transaction set number? (yes/no): ")
854
+ if user_input.lower() == 'yes':
855
+ print("Skipping patient...")
856
+ MediLink_ConfigLoader.log("Skipped processing of transaction set {} due to user decision.".format(transaction_set_control_number), config, level="INFO")
857
+ return True # Skip the current patient
858
+ else:
859
+ print("Processing halted due to validation errors.")
860
+ MediLink_ConfigLoader.log("HALT: Processing halted at transaction set {} due to unresolved validation errors.".format(transaction_set_control_number), config, level="ERROR")
861
+ sys.exit() # Optionally halt further processing
862
+
863
+ def winscp_validate_output_directory(output_directory):
864
+ """
865
+ Validates the output directory path to ensure it has no spaces.
866
+ If spaces are found, prompts the user to input a new path.
867
+ If the directory doesn't exist, creates it.
868
+ """
869
+ while ' ' in output_directory:
870
+ print("\nWARNING: The output directory path contains spaces, which can cause issues with upload operations.")
871
+ print(" Current proposed path: {}".format(output_directory))
872
+ new_path = input("Please enter a new path for the output directory: ")
873
+ output_directory = new_path.strip() # Remove leading/trailing spaces
874
+
875
+ # Check if the directory exists, if not, create it
876
+ if not os.path.exists(output_directory):
877
+ os.makedirs(output_directory)
878
+ print("INFO: Created output directory: {}".format(output_directory))
879
+
880
+ return output_directory
881
+
882
+ def get_output_directory(config):
883
+ # Retrieve desired default output file path from config
884
+ output_directory = config.get('outputFilePath', '')
885
+ # BUG (Low SFTP) Add WinSCP validation because of the mishandling of spaces in paths. (This shouldn't need to exist.)
886
+ output_directory = winscp_validate_output_directory(output_directory)
887
+ if not os.path.isdir(output_directory):
888
+ print("Output directory does not exist. Please check the configuration.")
889
+ return None
890
+ return output_directory