medicafe 0.250818.0__py3-none-any.whl → 0.250819.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.
@@ -147,6 +147,27 @@ if MediLink_837p_utilities:
147
147
  prompt_for_insurance_selection = MediLink_837p_utilities.prompt_for_insurance_selection
148
148
  build_nm1_segment = MediLink_837p_utilities.build_nm1_segment
149
149
 
150
+ # -----------------------------------------------------------------------------
151
+ # Test Mode Utilities
152
+ # -----------------------------------------------------------------------------
153
+ def _is_test_mode(config):
154
+ """
155
+ Determine if test mode is enabled. Accepts either the full config
156
+ (with 'MediLink_Config') or the MediLink subset config.
157
+ """
158
+ try:
159
+ if isinstance(config, dict):
160
+ # Prefer direct MediLink subset key
161
+ if config.get('TestMode', False):
162
+ return True
163
+ # Fallback: full config structure
164
+ medi = config.get('MediLink_Config', {})
165
+ if isinstance(medi, dict) and medi.get('TestMode', False):
166
+ return True
167
+ except Exception:
168
+ pass
169
+ return False
170
+
150
171
 
151
172
  # Constructs the ST segment for transaction set.
152
173
  def create_st_segment(transaction_set_control_number):
@@ -162,6 +183,14 @@ def create_bht_segment(parsed_data):
162
183
  def create_hl_billing_provider_segment():
163
184
  return "HL*1**20*1~"
164
185
 
186
+ # Constructs the HL segment for subscriber [hierarchical level (HL*2)]
187
+ def create_hl_subscriber_segment():
188
+ """
189
+ Returns the subscriber HL segment. Kept for backward compatibility with
190
+ MediLink_837p_encoder.py which expects this function.
191
+ """
192
+ return ["HL*2*1*22*0~"]
193
+
165
194
  # Constructs the NM1 segment for billing provider and includes address and Tax ID.
166
195
  def create_nm1_billing_provider_segment(config, endpoint):
167
196
  endpoint_config = config.get('endpoints', {}).get(endpoint.upper(), {})
@@ -169,11 +198,24 @@ def create_nm1_billing_provider_segment(config, endpoint):
169
198
  # Billing provider details
170
199
  billing_provider_entity_code = endpoint_config.get('billing_provider_entity_code', '85')
171
200
  billing_provider_npi_qualifier = endpoint_config.get('billing_provider_npi_qualifier', 'XX')
172
- #billing_provider_lastname = endpoint_config.get('billing_provider_lastname', config.get('default_billing_provider_name', 'DEFAULT NAME'))
173
- #billing_provider_firstname = endpoint_config.get('billing_provider_firstname', '')
174
- billing_provider_lastname = config.get('billing_provider_lastname', config.get('default_billing_provider_name', 'DEFAULT NAME'))
201
+ # Resolve required values with TestMode-aware enforcement
202
+ billing_provider_lastname = legacy_require_config_value(
203
+ [config, endpoint_config],
204
+ 'billing_provider_lastname',
205
+ config.get('default_billing_provider_name', 'DEFAULT NAME'),
206
+ '2010AA Billing Provider Last Name',
207
+ config,
208
+ endpoint
209
+ )
175
210
  billing_provider_firstname = config.get('billing_provider_firstname', '')
176
- billing_provider_npi = endpoint_config.get('billing_provider_npi', config.get('default_billing_provider_npi', 'DEFAULT NPI'))
211
+ billing_provider_npi = legacy_require_config_value(
212
+ [endpoint_config, config],
213
+ 'billing_provider_npi',
214
+ config.get('default_billing_provider_npi', 'DEFAULT NPI'),
215
+ '2010AA Billing Provider NPI',
216
+ config,
217
+ endpoint
218
+ )
177
219
 
178
220
  # Determine billing_entity_type_qualifier based on the presence of billing_provider_firstname
179
221
  billing_entity_type_qualifier = '1' if billing_provider_firstname else '2'
@@ -191,27 +233,27 @@ def create_nm1_billing_provider_segment(config, endpoint):
191
233
  # Construct address segments
192
234
  address_segments = []
193
235
  if config.get('billing_provider_address'):
236
+ addr = legacy_require_config_value([config], 'billing_provider_address', 'NO ADDRESS', '2010AA Billing Address', config, endpoint)
237
+ city = legacy_require_config_value([config], 'billing_provider_city', 'NO CITY', '2010AA Billing City', config, endpoint)
238
+ state = legacy_require_config_value([config], 'billing_provider_state', 'NO STATE', '2010AA Billing State', config, endpoint)
239
+ zip_code = legacy_require_config_value([config], 'billing_provider_zip', 'NO ZIP', '2010AA Billing ZIP', config, endpoint)
194
240
  # N3 segment for address line
195
- address_segments.append("N3*{}~".format(config.get('billing_provider_address', 'NO ADDRESS')))
241
+ address_segments.append("N3*{}~".format(addr))
196
242
  # N4 segment for City, State, ZIP
197
- address_segments.append("N4*{}*{}*{}~".format(
198
- config.get('billing_provider_city', 'NO CITY'),
199
- config.get('billing_provider_state', 'NO STATE'),
200
- config.get('billing_provider_zip', 'NO ZIP')
201
- ))
243
+ address_segments.append("N4*{}*{}*{}~".format(city, state, zip_code))
202
244
 
203
245
  # Assuming Tax ID is part of the same loop, otherwise move REF segment to the correct loop
204
- ref_segment = "REF*EI*{}~".format(config.get('billing_provider_tin', 'NO TAX ID'))
246
+ billing_tin = legacy_require_config_value([config], 'billing_provider_tin', 'NO TAX ID', '2010AA Billing TIN', config, endpoint)
247
+ ref_segment = "REF*EI*{}~".format(billing_tin)
205
248
 
206
- # Construct PRV segment if provider taxonomy is needed, are these just for Medicaid??
249
+ # Construct PRV segment if provider taxonomy is needed
207
250
  #prv_segment = ""
208
251
  #if config.get('billing_provider_taxonomy'):
209
252
  # prv_segment = "PRV*BI*PXC*{}~".format(config.get('billing_provider_taxonomy'))
210
253
 
211
- # Combine all the segments in the correct order, I think the PRV goes after the address and/or after ref
212
254
  segments = [nm1_segment]
213
- #if prv_segment:
214
- # segments.append(prv_segment)
255
+ # if prv_segment:
256
+ # segments.append(prv_segment)
215
257
  segments.extend(address_segments)
216
258
  segments.append(ref_segment)
217
259
 
@@ -223,12 +265,12 @@ def create_service_facility_location_npi_segment(config):
223
265
  Constructs segments for the service facility location, including the NM1 segment for identification
224
266
  and accompanying N3 and N4 segments for address details.
225
267
  """
226
- facility_npi = config.get('service_facility_npi', 'DEFAULT FACILITY NPI')
227
- facility_name = config.get('service_facility_name', 'DEFAULT FACILITY NAME')
228
- address_line_1 = config.get('service_facility_address', 'NO ADDRESS')
229
- city = config.get('service_facility_city', 'NO CITY')
230
- state = config.get('service_facility_state', 'NO STATE')
231
- zip_code = config.get('service_facility_zip', 'NO ZIP')
268
+ facility_npi = legacy_require_config_value([config], 'service_facility_npi', 'DEFAULT FACILITY NPI', '2310C Service Facility NPI', config)
269
+ facility_name = legacy_require_config_value([config], 'service_facility_name', 'DEFAULT FACILITY NAME', '2310C Service Facility Name', config)
270
+ address_line_1 = legacy_require_config_value([config], 'service_facility_address', 'NO ADDRESS', '2310C Service Facility Address', config)
271
+ city = legacy_require_config_value([config], 'service_facility_city', 'NO CITY', '2310C Service Facility City', config)
272
+ state = legacy_require_config_value([config], 'service_facility_state', 'NO STATE', '2310C Service Facility State', config)
273
+ zip_code = legacy_require_config_value([config], 'service_facility_zip', 'NO ZIP', '2310C Service Facility ZIP', config)
232
274
 
233
275
  # NM1 segment for facility identification
234
276
  nm1_segment = "NM1*77*2*{}*****XX*{}~".format(facility_name, facility_npi)
@@ -243,31 +285,25 @@ def create_service_facility_location_npi_segment(config):
243
285
  def create_1000A_submitter_name_segment(patient_data, config, endpoint):
244
286
  """
245
287
  Creates the 1000A submitter name segment, including the PER segment for contact information.
246
-
247
- Args:
248
- patient_data (dict): Enriched patient data containing payer information.
249
- config (dict): Configuration settings.
250
- endpoint (str): Intended Endpoint for resolving submitter information.
251
-
252
- Returns:
253
- list: A list containing the NM1 segment for the submitter name and the PER segment for contact information.
254
288
  """
255
289
  endpoint_config = config.get('endpoints', {}).get(endpoint.upper(), {})
256
290
  submitter_id_qualifier = endpoint_config.get('submitter_id_qualifier', '46') # '46' for ETIN or 'XX' for NPI
257
- submitter_name = endpoint_config.get('nm_103_value', 'DEFAULT NAME') # Default name if not in config
291
+
292
+ # Required submitter name
293
+ submitter_name = legacy_require_config_value([endpoint_config, config], 'nm_103_value', 'DEFAULT NAME', '1000A Submitter Name', config, endpoint)
258
294
 
259
295
  # Extract payer_id from patient_data
260
296
  payer_id = patient_data.get('payer_id', '')
261
297
 
262
298
  # Check if payer_id is Florida Blue (00590 or BCBSF) and assign submitter_id accordingly
263
299
  if payer_id in ['00590', 'BCBSF']:
264
- submitter_id = endpoint_config.get('nm_109_bcbs', 'DEFAULT BCBSF ID')
300
+ submitter_id = legacy_require_config_value([endpoint_config], 'nm_109_bcbsf', 'DEFAULT BCBSF ID', '1000A Submitter ID (BCBSF)', config, endpoint)
265
301
  else:
266
- submitter_id = endpoint_config.get('nm_109_value', 'DEFAULT ID') # Default ID if not specified in endpoint
302
+ submitter_id = legacy_require_config_value([endpoint_config], 'nm_109_value', 'DEFAULT ID', '1000A Submitter ID', config, endpoint)
267
303
 
268
- # Submitter contact details
269
- contact_name = config.get('submitter_name', 'NONE')
270
- contact_telephone_number = config.get('submitter_tel', 'NONE')
304
+ # Submitter contact details (required)
305
+ contact_name = legacy_require_config_value([config], 'submitter_name', 'NONE', '1000A Submitter Contact Name', config, endpoint)
306
+ contact_telephone_number = legacy_require_config_value([config], 'submitter_tel', 'NONE', '1000A Submitter Contact Phone', config, endpoint)
271
307
 
272
308
  # Get submitter first name to determine entity type qualifier
273
309
  submitter_first_name = config.get('submitter_first_name', '')
@@ -290,21 +326,15 @@ def create_1000A_submitter_name_segment(patient_data, config, endpoint):
290
326
 
291
327
  # Construct PER segment for the submitter's contact information
292
328
  per_segment = "PER*IC*{}*TE*{}~".format(contact_name, contact_telephone_number)
293
-
294
329
  return [nm1_segment, per_segment]
295
330
 
296
331
  # Constructs the NM1 segment for the receiver (1000B).
297
332
  def create_1000B_receiver_name_segment(config, endpoint):
298
- # Retrieve endpoint specific configuration
299
333
  endpoint_config = config.get('endpoints', {}).get(endpoint.upper(), {})
300
-
301
- # Set the entity identifier code to '40' for receiver and qualifier to '46' for EDI,
302
- # unless specified differently in the endpoint configuration.
303
334
  receiver_entity_code = '40'
304
335
  receiver_id_qualifier = endpoint_config.get('receiver_id_qualifier', '46')
305
- receiver_name = endpoint_config.get('receiver_name', 'DEFAULT RECEIVER NAME')
306
- receiver_edi = endpoint_config.get('receiver_edi', 'DEFAULT EDI')
307
-
336
+ receiver_name = legacy_require_config_value([endpoint_config], 'receiver_name', 'DEFAULT RECEIVER NAME', '1000B Receiver Name', config, endpoint)
337
+ receiver_edi = legacy_require_config_value([endpoint_config], 'receiver_edi', 'DEFAULT EDI', '1000B Receiver EDI', config, endpoint)
308
338
  return "NM1*{entity_code}*2*{receiver_name}*****{id_qualifier}*{receiver_edi}~".format(
309
339
  entity_code=receiver_entity_code,
310
340
  receiver_name=receiver_name,
@@ -312,6 +342,180 @@ def create_1000B_receiver_name_segment(config, endpoint):
312
342
  receiver_edi=receiver_edi
313
343
  )
314
344
 
345
+ def create_nm1_payto_address_segments(config):
346
+ """
347
+ Constructs the NM1 segment for the Pay-To Address, N3 for street address, and N4 for city, state, and ZIP.
348
+ This is used if the Pay-To Address is different from the Billing Provider Address.
349
+ """
350
+ payto_provider_name = legacy_require_config_value([config], 'payto_provider_name', 'DEFAULT PAY-TO NAME', '2010AB Pay-To Name', config)
351
+ payto_address = legacy_require_config_value([config], 'payto_address', 'DEFAULT PAY-TO ADDRESS', '2010AB Pay-To Address', config)
352
+ payto_city = legacy_require_config_value([config], 'payto_city', 'DEFAULT PAY-TO CITY', '2010AB Pay-To City', config)
353
+ payto_state = legacy_require_config_value([config], 'payto_state', 'DEFAULT PAY-TO STATE', '2010AB Pay-To State', config)
354
+ payto_zip = legacy_require_config_value([config], 'payto_zip', 'DEFAULT PAY-TO ZIP', '2010AB Pay-To ZIP', config)
355
+
356
+ nm1_segment = "NM1*87*2*{}~".format(payto_provider_name)
357
+ n3_segment = "N3*{}~".format(payto_address)
358
+ n4_segment = "N4*{}*{}*{}~".format(payto_city, payto_state, payto_zip)
359
+ return [nm1_segment, n3_segment, n4_segment]
360
+
361
+ # Constructs the N3 and N4 segments for the payer's address.
362
+ def create_payer_address_segments(config):
363
+ """
364
+ Constructs the N3 and N4 segments for the payer's address.
365
+
366
+ """
367
+ payer_address_line_1 = config.get('payer_address_line_1', '')
368
+ payer_city = config.get('payer_city', '')
369
+ payer_state = config.get('payer_state', '')
370
+ payer_zip = config.get('payer_zip', '')
371
+
372
+ n3_segment = "N3*{}~".format(payer_address_line_1)
373
+ n4_segment = "N4*{}*{}*{}~".format(payer_city, payer_state, payer_zip)
374
+
375
+ return [n3_segment, n4_segment]
376
+
377
+ # Constructs the PRV segment for billing provider.
378
+ def create_billing_prv_segment(config, endpoint):
379
+ if endpoint.lower() == 'optumedi':
380
+ return "PRV*BI*PXC*{}~".format(config['billing_provider_taxonomy'])
381
+ return ""
382
+
383
+ # (2000B Loop) Constructs the SBR segment for subscriber based on parsed data and configuration.
384
+ def create_sbr_segment(config, parsed_data, endpoint):
385
+ # Determine the payer responsibility sequence number code based on the payer type
386
+ # If the payer is Medicare, use 'P' (Primary)
387
+ # If the payer is not Medicare and is primary insurance, use 'P' (Primary)
388
+ # If the payer is secondary insurance after Medicare, use 'S' (Secondary)
389
+ # Assume everything is Primary for now.
390
+ responsibility_code = 'S' if parsed_data.get('claim_type') == 'secondary' else 'P'
391
+
392
+ # Insurance Type Code (SBR09)
393
+ insurance_type_code = insurance_type_selection(parsed_data)
394
+
395
+ # Prefer Medicare-specific type codes when determinable and COB helpers are available
396
+ if COB is not None:
397
+ try:
398
+ medicare_type = COB.determine_medicare_payer_type(parsed_data, config)
399
+ if medicare_type:
400
+ insurance_type_code = medicare_type # MB or MA
401
+ except Exception as e:
402
+ MediLink_ConfigLoader.log("COB determine_medicare_payer_type error: {}".format(str(e)), level="WARNING")
403
+
404
+ # Construct the SBR segment using the determined codes
405
+ sbr_segment = "SBR*{responsibility_code}*18*******{insurance_type_code}~".format(
406
+ responsibility_code=responsibility_code,
407
+ insurance_type_code=insurance_type_code
408
+ )
409
+ return sbr_segment
410
+
411
+ def insurance_type_selection(parsed_data):
412
+ """
413
+ Enhanced insurance type selection with optional API integration and safe fallbacks.
414
+ Maintains exact same signature as existing implementation.
415
+
416
+ TODO (HIGH SBR09) Finish making this function.
417
+ This should eventually integrate into a menu upstream. This menu flow probably needs to be alongside the suggested endpoint flow.
418
+ 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
419
+ useful historical patterns other than to say that the default should be PPO (12), then CI, then FI as most common options,
420
+ followed by the rest.
421
+
422
+ Present a user-selectable menu of insurance types based on predefined codes.
423
+ User needs to input the desired 2 character code for that patient or default to PPO.
424
+
425
+ Currently implements a simple text-based selection menu to choose
426
+ an insurance type for a patient. The default selection is '12' for PPO, but the user
427
+ can input other codes as per the options listed. This initial implementation uses
428
+ simple input/output functions for selection and can be replaced in the future by a
429
+ more dynamic interface or API-driven selection method.
430
+ """
431
+ MediLink_ConfigLoader.log("insurance_type_selection(parsed_data): {}".format(parsed_data), level="DEBUG")
432
+
433
+ # Try enhanced selection with safe fallback
434
+ if safe_insurance_type_selection:
435
+ try:
436
+ return safe_insurance_type_selection(parsed_data, _original_insurance_type_selection_logic)
437
+ except Exception as e:
438
+ MediLink_ConfigLoader.log("Error in enhanced insurance selection: {}. Using original logic".format(str(e)), level="ERROR")
439
+ return _original_insurance_type_selection_logic(parsed_data)
440
+ else:
441
+ MediLink_ConfigLoader.log("Enhanced insurance selection not available. Using original logic", level="INFO")
442
+ return _original_insurance_type_selection_logic(parsed_data)
443
+
444
+ def _original_insurance_type_selection_logic(parsed_data):
445
+ """
446
+ Original insurance type selection logic extracted to preserve exact behavior.
447
+ This ensures backward compatibility when enhanced features are not available.
448
+ """
449
+ # Check if insurance type is already assigned and is valid
450
+ insurance_type_code = parsed_data.get('insurance_type')
451
+ if insurance_type_code and len(insurance_type_code) == 2 and insurance_type_code.isalnum():
452
+ MediLink_ConfigLoader.log("Insurance type already assigned: {}".format(insurance_type_code), level="DEBUG")
453
+ return insurance_type_code
454
+ elif insurance_type_code:
455
+ MediLink_ConfigLoader.log("Invalid insurance type: {}".format(insurance_type_code), level="WARNING")
456
+
457
+ print("\nInsurance Type Validation Error: Select the insurance type for patient {}: ".format(parsed_data['LAST']))
458
+
459
+ # Retrieve insurance options with codes and descriptions
460
+ config, _ = MediLink_ConfigLoader.load_configuration()
461
+ try:
462
+ from MediCafe.core_utils import extract_medilink_config
463
+ medi = extract_medilink_config(config)
464
+ insurance_options = medi.get('insurance_options', {})
465
+ except Exception:
466
+ insurance_options = {}
467
+
468
+ # If COB library is available, augment options with Medicare codes (MB/MA/MC)
469
+ if COB is not None:
470
+ try:
471
+ insurance_options = COB.get_enhanced_insurance_options(config)
472
+ except Exception as e:
473
+ MediLink_ConfigLoader.log("COB get_enhanced_insurance_options error: {}".format(str(e)), level="WARNING")
474
+
475
+ # TODO (COB ENHANCEMENT): Enhanced insurance options for Medicare support
476
+ # See MediLink_837p_cob_library.get_enhanced_insurance_options() for Medicare codes:
477
+ # - MB: Medicare Part B
478
+ # - MA: Medicare Advantage
479
+ # - MC: Medicare Part C
480
+
481
+ def prompt_display_insurance_options():
482
+ # Prompt to display full list
483
+ display_full_list = input("Do you want to see the full list of insurance options? (yes/no): ").strip().lower()
484
+
485
+ # Display full list if user confirms
486
+ if display_full_list in ['yes', 'y'] and MediLink_Display_Utils:
487
+ MediLink_Display_Utils.display_insurance_options(insurance_options)
488
+
489
+ # Horrible menu
490
+ prompt_display_insurance_options()
491
+
492
+ # Default selection
493
+ insurance_type_code = '12'
494
+
495
+ # User input for insurance type
496
+ user_input = input("Enter the 2-character code for the insurance type (or press Enter to default to '12' for PPO): ").strip().upper()
497
+
498
+ # Input validation and assignment
499
+ if user_input:
500
+ # Basic format validation
501
+ if len(user_input) > 3 or not user_input.isalnum():
502
+ print("Invalid format: Insurance codes should be 1-3 alphanumeric characters. Defaulting to PPO.")
503
+ elif user_input in insurance_options:
504
+ insurance_type_code = user_input
505
+ print("Selected: {} - {}".format(user_input, insurance_options[user_input]))
506
+ else:
507
+ # User wants to use a code not in options - confirm with them
508
+ confirm = input("Code '{}' not found in options. Use it anyway? (y/n): ".format(user_input)).strip().lower()
509
+ if confirm in ['y', 'yes']:
510
+ insurance_type_code = user_input
511
+ print("Using code: {}".format(user_input))
512
+ else:
513
+ print("Defaulting to PPO (Preferred Provider Organization)")
514
+ else:
515
+ print("Using default: PPO (Preferred Provider Organization)")
516
+
517
+ return insurance_type_code
518
+
315
519
  def payer_id_to_payer_name(parsed_data, config, endpoint, crosswalk, client):
316
520
  """
317
521
  Preprocesses payer information from parsed data and enriches parsed_data with the payer name and ID.
@@ -393,6 +597,19 @@ def resolve_payer_name(payer_id, config, primary_endpoint, insurance_name, parse
393
597
  print("If the Payer ID appears correct, you may skip the correction and force-continue with this one.")
394
598
  print("\nPlease check the Payer ID in the Crosswalk and the initial \ndata source (e.g., Carol's CSV) as needed.")
395
599
  print("If unsure, llamar a Dani for guidance on manual corrections.")
600
+
601
+ # In Test Mode, avoid blocking prompts and proceed with a placeholder name after API failure
602
+ if _is_test_mode(config):
603
+ try:
604
+ if payer_id in crosswalk.get('payer_id', {}):
605
+ _nm = crosswalk['payer_id'][payer_id].get('name', 'TEST INSURANCE')
606
+ print("TEST MODE: Using crosswalk payer name '{}' after API failure".format(_nm))
607
+ return _nm
608
+ except Exception:
609
+ pass
610
+ MediLink_ConfigLoader.log("[TEST MODE] API resolution failed; using placeholder payer name", config, level="WARNING")
611
+ print("TEST MODE: Using placeholder payer name 'TEST INSURANCE' after API failure")
612
+ return 'TEST INSURANCE'
396
613
 
397
614
  # Step 4: Integrate user input logic
398
615
  user_decision = input("\nType 'FORCE' to force-continue with the Medisoft name, or press Enter to pause processing and make corrections: ").strip().lower()
@@ -436,7 +653,7 @@ def resolve_payer_name(payer_id, config, primary_endpoint, insurance_name, parse
436
653
  print("User did not confirm the standard insurance name. Manual intervention is required.")
437
654
  MediLink_ConfigLoader.log("User did not confirm the standard insurance name. Manual intervention is required.", config, level="CRITICAL")
438
655
 
439
- #FIXED: CRITICAL ISSUE - Implemented recovery path instead of exit(1)
656
+ # FIXED: CRITICAL ISSUE - Implemented recovery path instead of exit(1)
440
657
  # The insurance name confirmation is primarily a sanity check to verify the API recognizes the payer ID,
441
658
  # not a critical validation for claim processing. The payer name is not used in the crosswalk or in the
442
659
  # actual claims once they are built. This implementation provides a recovery path instead of halting.
@@ -568,6 +785,10 @@ def map_insurance_name_to_payer_id(insurance_name, config, client, crosswalk):
568
785
 
569
786
  # Ensure crosswalk is initialized and 'payer_id' key is available
570
787
  if 'payer_id' not in crosswalk:
788
+ if _is_test_mode(config):
789
+ MediLink_ConfigLoader.log("[TEST MODE] Crosswalk 'payer_id' missing. Using hardcoded fallback 'TEST01'", config, level="WARNING")
790
+ print("TEST MODE: Crosswalk 'payer_id' missing. Using fallback payer ID 'TEST01'")
791
+ return 'TEST01'
571
792
  raise ValueError("Crosswalk 'payer_id' not found. Please run MediBot_Preprocessor.py with the --update-crosswalk argument.")
572
793
 
573
794
  # Load insurance data from MAINS to get insurance ID
@@ -580,31 +801,71 @@ def map_insurance_name_to_payer_id(insurance_name, config, client, crosswalk):
580
801
  closest_matches = find_closest_insurance_matches(insurance_name, insurance_to_id)
581
802
 
582
803
  if closest_matches:
583
- # Prompt user to select from closest matches
584
- selected_insurance_name = prompt_for_insurance_selection(insurance_name, closest_matches, config)
585
-
586
- if selected_insurance_name:
587
- # Use the selected insurance name
588
- medisoft_id = insurance_to_id.get(selected_insurance_name)
589
- MediLink_ConfigLoader.log("Using selected insurance name '{}' for original '{}'".format(selected_insurance_name, insurance_name), config, level="INFO")
804
+ # In Test Mode, auto-select the best match to avoid interaction
805
+ if _is_test_mode(config):
806
+ try:
807
+ auto_name = closest_matches[0][0]
808
+ medisoft_id = insurance_to_id.get(auto_name)
809
+ MediLink_ConfigLoader.log("[TEST MODE] Auto-selected closest insurance match '{}' for original '{}'".format(auto_name, insurance_name), config, level="WARNING")
810
+ except Exception:
811
+ medisoft_id = None
590
812
  else:
591
- # User chose manual intervention
592
- error_message = "CLAIM SUBMISSION CANCELLED: Insurance name '{}' not found in MAINS and user chose manual intervention.".format(insurance_name)
593
- MediLink_ConfigLoader.log(error_message, config, level="WARNING")
594
- print("\n" + "="*80)
595
- print("CLAIM SUBMISSION CANCELLED: MANUAL INTERVENTION REQUIRED")
596
- print("="*80)
597
- print("\nThe system cannot automatically process this claim because:")
598
- print("- Insurance name '{}' was not found in the MAINS database".format(insurance_name))
599
- print("- Without a valid insurance mapping, the claim cannot be submitted")
600
- print("\nTo proceed with this claim, you need to:")
601
- print("1. Verify the correct insurance company name")
602
- print("2. Ensure the insurance company is in your Medisoft system")
603
- print("3. Restart the claim submission process once the insurance is properly configured")
604
- print("="*80)
605
- raise ValueError(error_message)
813
+ # Prompt user to select from closest matches
814
+ selected_insurance_name = prompt_for_insurance_selection(insurance_name, closest_matches, config)
815
+
816
+ if selected_insurance_name:
817
+ # Use the selected insurance name
818
+ medisoft_id = insurance_to_id.get(selected_insurance_name)
819
+ MediLink_ConfigLoader.log("Using selected insurance name '{}' for original '{}'".format(selected_insurance_name, insurance_name), config, level="INFO")
820
+ else:
821
+ # Test Mode: fallback to a safe placeholder payer ID and continue
822
+ if _is_test_mode(config):
823
+ try:
824
+ first_key = None
825
+ for _k in crosswalk.get('payer_id', {}).keys():
826
+ first_key = _k
827
+ break
828
+ if first_key:
829
+ MediLink_ConfigLoader.log("[TEST MODE] Using fallback payer ID '{}' for insurance '{}'".format(first_key, insurance_name), config, level="WARNING")
830
+ print("TEST MODE: Using fallback payer ID '{}' for insurance '{}'".format(first_key, insurance_name))
831
+ return first_key
832
+ except Exception:
833
+ pass
834
+ MediLink_ConfigLoader.log("[TEST MODE] Using hardcoded fallback payer ID 'TEST01' for insurance '{}'".format(insurance_name), config, level="WARNING")
835
+ print("TEST MODE: Using hardcoded fallback payer ID 'TEST01' for insurance '{}'".format(insurance_name))
836
+ return 'TEST01'
837
+ # User chose manual intervention
838
+ error_message = "CLAIM SUBMISSION CANCELLED: Insurance name '{}' not found in MAINS and user chose manual intervention.".format(insurance_name)
839
+ MediLink_ConfigLoader.log(error_message, config, level="WARNING")
840
+ print("\n" + "="*80)
841
+ print("CLAIM SUBMISSION CANCELLED: MANUAL INTERVENTION REQUIRED")
842
+ print("="*80)
843
+ print("\nThe system cannot automatically process this claim because:")
844
+ print("- Insurance name '{}' was not found in the MAINS database".format(insurance_name))
845
+ print("- Without a valid insurance mapping, the claim cannot be submitted")
846
+ print("\nTo proceed with this claim, you need to:")
847
+ print("1. Verify the correct insurance company name")
848
+ print("2. Ensure the insurance company is in your Medisoft system")
849
+ print("3. Restart the claim submission process once the insurance is properly configured")
850
+ print("="*80)
851
+ raise ValueError(error_message)
606
852
  else:
607
853
  # No matches found in MAINS
854
+ if _is_test_mode(config):
855
+ try:
856
+ first_key = None
857
+ for _k in crosswalk.get('payer_id', {}).keys():
858
+ first_key = _k
859
+ break
860
+ if first_key:
861
+ MediLink_ConfigLoader.log("[TEST MODE] No MAINS match. Using fallback payer ID '{}' for '{}'".format(first_key, insurance_name), config, level="WARNING")
862
+ print("TEST MODE: No MAINS match. Using fallback payer ID '{}' for '{}'".format(first_key, insurance_name))
863
+ return first_key
864
+ except Exception:
865
+ pass
866
+ MediLink_ConfigLoader.log("[TEST MODE] No MAINS match. Using hardcoded fallback payer ID 'TEST01' for '{}'".format(insurance_name), config, level="WARNING")
867
+ print("TEST MODE: No MAINS match. Using hardcoded fallback payer ID 'TEST01' for '{}'".format(insurance_name))
868
+ return 'TEST01'
608
869
  error_message = "CLAIM SUBMISSION FAILED: Cannot find Medisoft ID for insurance name: '{}'. No similar matches found in MAINS database.".format(insurance_name)
609
870
  MediLink_ConfigLoader.log(error_message, config, level="ERROR")
610
871
  print("\n" + "="*80)
@@ -641,10 +902,25 @@ def map_insurance_name_to_payer_id(insurance_name, config, client, crosswalk):
641
902
 
642
903
  # Handle the case where no payer ID is found
643
904
  if payer_id is None:
644
- error_message = "No payer ID found for Medisoft ID: {}.".format(medisoft_id)
645
- MediLink_ConfigLoader.log(error_message, config, level="ERROR")
646
- print(error_message)
647
- payer_id = handle_missing_payer_id(insurance_name, config, crosswalk, client)
905
+ if _is_test_mode(config):
906
+ try:
907
+ first_key = None
908
+ for _k in crosswalk.get('payer_id', {}).keys():
909
+ first_key = _k
910
+ break
911
+ if first_key:
912
+ MediLink_ConfigLoader.log("[TEST MODE] No payer ID found for Medisoft ID {}. Using fallback '{}'".format(medisoft_id, first_key), config, level="WARNING")
913
+ payer_id = first_key
914
+ else:
915
+ MediLink_ConfigLoader.log("[TEST MODE] No payer ID found for Medisoft ID {}. Using hardcoded 'TEST01'".format(medisoft_id), config, level="WARNING")
916
+ payer_id = 'TEST01'
917
+ except Exception:
918
+ payer_id = 'TEST01'
919
+ else:
920
+ error_message = "No payer ID found for Medisoft ID: {}.".format(medisoft_id)
921
+ MediLink_ConfigLoader.log(error_message, config, level="ERROR")
922
+ print(error_message)
923
+ payer_id = handle_missing_payer_id(insurance_name, config, crosswalk, client)
648
924
 
649
925
  if payer_id is None:
650
926
  raise ValueError("Payer ID cannot be None after all checks.")
@@ -661,195 +937,6 @@ def map_insurance_name_to_payer_id(insurance_name, config, client, crosswalk):
661
937
  MediLink_ConfigLoader.log(error_message, config, level="ERROR")
662
938
  raise e
663
939
 
664
- def create_nm1_payto_address_segments(config):
665
- """
666
- Constructs the NM1 segment for the Pay-To Address, N3 for street address, and N4 for city, state, and ZIP.
667
- This is used if the Pay-To Address is different from the Billing Provider Address.
668
- """
669
- payto_provider_name = config.get('payto_provider_name', 'DEFAULT PAY-TO NAME')
670
- payto_address = config.get('payto_address', 'DEFAULT PAY-TO ADDRESS')
671
- payto_city = config.get('payto_city', 'DEFAULT PAY-TO CITY')
672
- payto_state = config.get('payto_state', 'DEFAULT PAY-TO STATE')
673
- payto_zip = config.get('payto_zip', 'DEFAULT PAY-TO ZIP')
674
-
675
- nm1_segment = "NM1*87*2*{}~".format(payto_provider_name) # '87' indicates Pay-To Provider
676
- n3_segment = "N3*{}~".format(payto_address)
677
- n4_segment = "N4*{}*{}*{}~".format(payto_city, payto_state, payto_zip)
678
-
679
- return [nm1_segment, n3_segment, n4_segment]
680
-
681
- def create_payer_address_segments(config):
682
- """
683
- Constructs the N3 and N4 segments for the payer's address.
684
-
685
- """
686
- payer_address_line_1 = config.get('payer_address_line_1', '')
687
- payer_city = config.get('payer_city', '')
688
- payer_state = config.get('payer_state', '')
689
- payer_zip = config.get('payer_zip', '')
690
-
691
- n3_segment = "N3*{}~".format(payer_address_line_1)
692
- n4_segment = "N4*{}*{}*{}~".format(payer_city, payer_state, payer_zip)
693
-
694
- return [n3_segment, n4_segment]
695
-
696
- # Constructs the PRV segment for billing provider.
697
- def create_billing_prv_segment(config, endpoint):
698
- if endpoint.lower() == 'optumedi':
699
- return "PRV*BI*PXC*{}~".format(config['billing_provider_taxonomy'])
700
- return ""
701
-
702
- # Constructs the HL segment for subscriber [hierarchical level (HL*2)]
703
- def create_hl_subscriber_segment():
704
- return ["HL*2*1*22*0~"]
705
-
706
- # (2000B Loop) Constructs the SBR segment for subscriber based on parsed data and configuration.
707
- def create_sbr_segment(config, parsed_data, endpoint):
708
- # Determine the payer responsibility sequence number code based on the payer type
709
- # If the payer is Medicare, use 'P' (Primary)
710
- # If the payer is not Medicare and is primary insurance, use 'P' (Primary)
711
- # If the payer is secondary insurance after Medicare, use 'S' (Secondary)
712
- # Assume everything is Primary for now.
713
- responsibility_code = 'S' if parsed_data.get('claim_type') == 'secondary' else 'P'
714
-
715
- # Insurance Type Code (SBR09)
716
- insurance_type_code = insurance_type_selection(parsed_data)
717
-
718
- # Prefer Medicare-specific type codes when determinable and COB helpers are available
719
- if COB is not None:
720
- try:
721
- medicare_type = COB.determine_medicare_payer_type(parsed_data, config)
722
- if medicare_type:
723
- insurance_type_code = medicare_type # MB or MA
724
- except Exception as e:
725
- MediLink_ConfigLoader.log("COB determine_medicare_payer_type error: {}".format(str(e)), level="WARNING")
726
-
727
- # Construct the SBR segment using the determined codes
728
- sbr_segment = "SBR*{responsibility_code}*18*******{insurance_type_code}~".format(
729
- responsibility_code=responsibility_code,
730
- insurance_type_code=insurance_type_code
731
- )
732
- return sbr_segment
733
-
734
- def insurance_type_selection(parsed_data):
735
- """
736
- Enhanced insurance type selection with optional API integration and safe fallbacks.
737
- Maintains exact same signature as existing implementation.
738
-
739
- TODO (HIGH SBR09) Finish making this function.
740
- This should eventually integrate into a menu upstream. This menu flow probably needs to be alongside the suggested endpoint flow.
741
- 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
742
- useful historical patterns other than to say that the default should be PPO (12), then CI, then FI as most common options,
743
- followed by the rest.
744
-
745
- Present a user-selectable menu of insurance types based on predefined codes.
746
- User needs to input the desired 2 character code for that patient or default to PPO.
747
-
748
- Currently implements a simple text-based selection menu to choose
749
- an insurance type for a patient. The default selection is '12' for PPO, but the user
750
- can input other codes as per the options listed. This initial implementation uses
751
- simple input/output functions for selection and can be replaced in the future by a
752
- more dynamic interface or API-driven selection method.
753
-
754
- Future Enhancements:
755
- - Automate selection via an API that fetches patient data directly and auto-assigns the insurance type.
756
- - Error handling to manage incorrect inputs and retry mechanisms.
757
-
758
- Parameters:
759
- - parsed_data (dict): Contains patient's last name under key 'LAST'.
760
-
761
- Returns:
762
- - str: The insurance type code selected by the user.
763
-
764
- """
765
- MediLink_ConfigLoader.log("insurance_type_selection(parsed_data): {}".format(parsed_data), level="DEBUG")
766
-
767
- # Try enhanced selection with safe fallback
768
- if safe_insurance_type_selection:
769
- try:
770
- return safe_insurance_type_selection(parsed_data, _original_insurance_type_selection_logic)
771
- except Exception as e:
772
- MediLink_ConfigLoader.log("Error in enhanced insurance selection: {}. Using original logic".format(str(e)), level="ERROR")
773
- return _original_insurance_type_selection_logic(parsed_data)
774
- else:
775
- MediLink_ConfigLoader.log("Enhanced insurance selection not available. Using original logic", level="INFO")
776
- return _original_insurance_type_selection_logic(parsed_data)
777
-
778
- def _original_insurance_type_selection_logic(parsed_data):
779
- """
780
- Original insurance type selection logic extracted to preserve exact behavior.
781
- This ensures backward compatibility when enhanced features are not available.
782
- """
783
- # Check if insurance type is already assigned and is valid
784
- insurance_type_code = parsed_data.get('insurance_type')
785
- if insurance_type_code and len(insurance_type_code) == 2 and insurance_type_code.isalnum():
786
- MediLink_ConfigLoader.log("Insurance type already assigned: {}".format(insurance_type_code), level="DEBUG")
787
- return insurance_type_code
788
- elif insurance_type_code:
789
- MediLink_ConfigLoader.log("Invalid insurance type: {}".format(insurance_type_code), level="WARNING")
790
-
791
- print("\nInsurance Type Validation Error: Select the insurance type for patient {}: ".format(parsed_data['LAST']))
792
-
793
- # Retrieve insurance options with codes and descriptions
794
- config, _ = MediLink_ConfigLoader.load_configuration()
795
- try:
796
- from MediCafe.core_utils import extract_medilink_config
797
- medi = extract_medilink_config(config)
798
- insurance_options = medi.get('insurance_options', {})
799
- except Exception:
800
- insurance_options = {}
801
-
802
- # If COB library is available, augment options with Medicare codes (MB/MA/MC)
803
- if COB is not None:
804
- try:
805
- insurance_options = COB.get_enhanced_insurance_options(config)
806
- except Exception as e:
807
- MediLink_ConfigLoader.log("COB get_enhanced_insurance_options error: {}".format(str(e)), level="WARNING")
808
-
809
- # TODO (COB ENHANCEMENT): Enhanced insurance options for Medicare support
810
- # See MediLink_837p_cob_library.get_enhanced_insurance_options() for Medicare codes:
811
- # - MB: Medicare Part B
812
- # - MA: Medicare Advantage
813
- # - MC: Medicare Part C
814
-
815
- def prompt_display_insurance_options():
816
- # Prompt to display full list
817
- display_full_list = input("Do you want to see the full list of insurance options? (yes/no): ").strip().lower()
818
-
819
- # Display full list if user confirms
820
- if display_full_list in ['yes', 'y'] and MediLink_Display_Utils:
821
- MediLink_Display_Utils.display_insurance_options(insurance_options)
822
-
823
- # Horrible menu
824
- prompt_display_insurance_options()
825
-
826
- # Default selection
827
- insurance_type_code = '12'
828
-
829
- # User input for insurance type
830
- user_input = input("Enter the 2-character code for the insurance type (or press Enter to default to '12' for PPO): ").strip().upper()
831
-
832
- # Input validation and assignment
833
- if user_input:
834
- # Basic format validation
835
- if len(user_input) > 3 or not user_input.isalnum():
836
- print("Invalid format: Insurance codes should be 1-3 alphanumeric characters. Defaulting to PPO.")
837
- elif user_input in insurance_options:
838
- insurance_type_code = user_input
839
- print("Selected: {} - {}".format(user_input, insurance_options[user_input]))
840
- else:
841
- # User wants to use a code not in options - confirm with them
842
- confirm = input("Code '{}' not found in options. Use it anyway? (y/n): ".format(user_input)).strip().lower()
843
- if confirm in ['y', 'yes']:
844
- insurance_type_code = user_input
845
- print("Using code: {}".format(user_input))
846
- else:
847
- print("Defaulting to PPO (Preferred Provider Organization)")
848
- else:
849
- print("Using default: PPO (Preferred Provider Organization)")
850
-
851
- return insurance_type_code
852
-
853
940
  # Constructs the NM1 segment for subscriber based on parsed data and configuration.
854
941
  def create_nm1_subscriber_segment(config, parsed_data, endpoint):
855
942
  if endpoint.lower() == 'optumedi':
@@ -1145,6 +1232,25 @@ def create_interchange_header(config, endpoint, isa13):
1145
1232
  gs_receiver_code = endpoint_config.get('gs_03_value', config.get('receiverEdi', ''))
1146
1233
  isa15_value = endpoint_config.get('isa_15_value', isa15)
1147
1234
 
1235
+ # Log warnings for empty sender/receiver codes (fallback detection)
1236
+ config_warnings = []
1237
+ if not isa_sender_id or isa_sender_id.strip() == '':
1238
+ config_warnings.append("ISA06 Sender ID is empty - using fallback configuration")
1239
+ if not gs_sender_code or gs_sender_code.strip() == '':
1240
+ config_warnings.append("GS02 Sender Code is empty - using fallback configuration")
1241
+ if not isa_receiver_id or isa_receiver_id.strip() == '':
1242
+ config_warnings.append("ISA08 Receiver ID is empty - using fallback configuration")
1243
+ if not gs_receiver_code or gs_receiver_code.strip() == '':
1244
+ config_warnings.append("GS03 Receiver Code is empty - using fallback configuration")
1245
+
1246
+ if config_warnings:
1247
+ warning_msg = "CONFIG FALLBACK DETECTED for endpoint {}: {}".format(endpoint, '; '.join(config_warnings))
1248
+ MediLink_ConfigLoader.log(warning_msg, config, level="WARNING")
1249
+ # Also log to console for immediate visibility during header creation
1250
+ print("WARNING: Configuration fallbacks detected during 837P header creation:")
1251
+ for warning in config_warnings:
1252
+ print(" - {}".format(warning))
1253
+
1148
1254
  # ISA Segment
1149
1255
  isa_segment = "ISA*00*{}*00*{}*{}*{}*{}*{}*{}*{}*^*00501*{}*0*{}*:~".format(
1150
1256
  isa02, isa04, isa05, isa_sender_id.ljust(15), isa07_value, isa_receiver_id.ljust(15),
@@ -1229,56 +1335,68 @@ def validate_claim_data_for_837p(parsed_data, config, crosswalk):
1229
1335
  diagnosis_code = next((key for key, value in crosswalk.get('diagnosis_to_medisoft', {}).items() if value == medisoft_code), None)
1230
1336
 
1231
1337
  if not diagnosis_code:
1232
- # Log the error condition with detailed context
1233
- error_message = "Diagnosis code is empty for chart number: {}. Please verify. Medisoft code is {}".format(chart_number, medisoft_code)
1234
- MediLink_ConfigLoader.log(error_message, config, level="CRITICAL")
1235
-
1236
- print("\n{}".format("="*80))
1237
- print("CRITICAL: Missing diagnosis code mapping for patient {}".format(chart_number))
1238
- print("{}".format("="*80))
1239
- print("Medisoft code: '{}'".format(medisoft_code))
1240
- print("Patient: {}, {}".format(parsed_data.get('LAST', 'Unknown'), parsed_data.get('FIRST', 'Unknown')))
1241
- print("Service Date: {}".format(parsed_data.get('DATE', 'Unknown')))
1242
- print("\nThis diagnosis code needs to be added to the crosswalk.json file.")
1243
- print("\nCurrent diagnosis_to_medisoft mapping format:")
1244
-
1245
- # Show example entries from the crosswalk
1246
- diagnosis_examples = list(crosswalk.get('diagnosis_to_medisoft', {}).items())[:3]
1247
- for full_code, medisoft_short in diagnosis_examples:
1248
- print(" '{}': '{}'".format(full_code, medisoft_short))
1249
-
1250
- print("\nPlease enter the complete ICD-10 diagnosis code (e.g., H25.10):")
1251
- diagnosis_code = input("> ").strip()
1252
-
1253
- if not diagnosis_code:
1254
- raise ValueError("Cannot proceed without diagnosis code for patient {}".format(chart_number))
1255
-
1256
- # Update the crosswalk dictionary with the new pairing of diagnosis_code and medisoft_code
1257
- crosswalk['diagnosis_to_medisoft'][diagnosis_code] = medisoft_code
1258
- MediLink_ConfigLoader.log("Updated crosswalk with new diagnosis code: {}, for Medisoft code {}".format(diagnosis_code, medisoft_code), config, level="INFO")
1259
- print("\n[SUCCESS] Added '{}' -> '{}' to crosswalk".format(diagnosis_code, medisoft_code))
1260
-
1261
- # Fix: Automatically persist the crosswalk changes
1262
- try:
1263
- # Import the existing save function from MediBot_Crosswalk_Utils
1264
- from MediBot.MediBot_Crosswalk_Utils import save_crosswalk
1265
- if save_crosswalk(None, config, crosswalk, skip_api_operations=True):
1266
- print("Crosswalk changes saved successfully.")
1267
- MediLink_ConfigLoader.log("Diagnosis code mapping persisted to crosswalk file", config, level="INFO")
1268
- else:
1269
- print("Warning: Failed to save crosswalk changes - they may be lost on restart.")
1270
- MediLink_ConfigLoader.log("Failed to persist diagnosis code mapping", config, level="WARNING")
1271
- except ImportError:
1272
- print("Warning: Could not import save_crosswalk function - changes not persisted.")
1273
- MediLink_ConfigLoader.log("Could not import save_crosswalk for diagnosis persistence", config, level="ERROR")
1274
- except Exception as e:
1275
- print("Warning: Error saving crosswalk changes: {}".format(e))
1276
- MediLink_ConfigLoader.log("Error persisting diagnosis code mapping: {}".format(e), config, level="ERROR")
1338
+ if _is_test_mode(config):
1339
+ # Use a safe placeholder ICD-10 code and update mapping in-memory only
1340
+ placeholder_icd = 'R69' # Unknown and unspecified causes of morbidity
1341
+ try:
1342
+ if 'diagnosis_to_medisoft' not in crosswalk or not isinstance(crosswalk.get('diagnosis_to_medisoft'), dict):
1343
+ crosswalk['diagnosis_to_medisoft'] = {}
1344
+ except Exception:
1345
+ pass
1346
+ crosswalk['diagnosis_to_medisoft'][placeholder_icd] = medisoft_code or 'R69'
1347
+ MediLink_ConfigLoader.log("[TEST MODE] Using placeholder ICD '{}' and updating in-memory mapping".format(placeholder_icd), config, level="WARNING")
1348
+ print("TEST MODE: Using placeholder ICD '{}' for chart '{}' (in-memory only)".format(placeholder_icd, chart_number))
1349
+ else:
1350
+ # Log the error condition with detailed context and prompt user
1351
+ error_message = "Diagnosis code is empty for chart number: {}. Please verify. Medisoft code is {}".format(chart_number, medisoft_code)
1352
+ MediLink_ConfigLoader.log(error_message, config, level="CRITICAL")
1353
+
1354
+ print("\n{}".format("="*80))
1355
+ print("CRITICAL: Missing diagnosis code mapping for patient {}".format(chart_number))
1356
+ print("{}".format("="*80))
1357
+ print("Medisoft code: '{}'".format(medisoft_code))
1358
+ print("Patient: {}, {}".format(parsed_data.get('LAST', 'Unknown'), parsed_data.get('FIRST', 'Unknown')))
1359
+ print("Service Date: {}".format(parsed_data.get('DATE', 'Unknown')))
1360
+ print("\nThis diagnosis code needs to be added to the crosswalk.json file.")
1361
+ print("\nCurrent diagnosis_to_medisoft mapping format:")
1362
+
1363
+ # Show example entries from the crosswalk
1364
+ diagnosis_examples = list(crosswalk.get('diagnosis_to_medisoft', {}).items())[:3]
1365
+ for full_code, medisoft_short in diagnosis_examples:
1366
+ print(" '{}': '{}'".format(full_code, medisoft_short))
1367
+
1368
+ print("\nPlease enter the complete ICD-10 diagnosis code (e.g., H25.10):")
1369
+ diagnosis_code = input("> ").strip()
1370
+
1371
+ if not diagnosis_code:
1372
+ raise ValueError("Cannot proceed without diagnosis code for patient {}".format(chart_number))
1373
+
1374
+ # Update the crosswalk dictionary with the new pairing of diagnosis_code and medisoft_code
1375
+ crosswalk['diagnosis_to_medisoft'][diagnosis_code] = medisoft_code
1376
+ MediLink_ConfigLoader.log("Updated crosswalk with new diagnosis code: {}, for Medisoft code {}".format(diagnosis_code, medisoft_code), config, level="INFO")
1377
+ print("\n[SUCCESS] Added '{}' -> '{}' to crosswalk".format(diagnosis_code, medisoft_code))
1378
+
1379
+ # Fix: Automatically persist the crosswalk changes
1380
+ try:
1381
+ # Import the existing save function from MediBot_Crosswalk_Utils
1382
+ from MediBot.MediBot_Crosswalk_Utils import save_crosswalk
1383
+ if save_crosswalk(None, config, crosswalk, skip_api_operations=True):
1384
+ print("Crosswalk changes saved successfully.")
1385
+ MediLink_ConfigLoader.log("Diagnosis code mapping persisted to crosswalk file", config, level="INFO")
1386
+ else:
1387
+ print("Warning: Failed to save crosswalk changes - they may be lost on restart.")
1388
+ MediLink_ConfigLoader.log("Failed to persist diagnosis code mapping", config, level="WARNING")
1389
+ except ImportError:
1390
+ print("Warning: Could not import save_crosswalk function - changes not persisted.")
1391
+ MediLink_ConfigLoader.log("Could not import save_crosswalk for diagnosis persistence", config, level="ERROR")
1392
+ except Exception as e:
1393
+ print("Warning: Error saving crosswalk changes: {}".format(e))
1394
+ MediLink_ConfigLoader.log("Error persisting diagnosis code mapping: {}".format(e), config, level="ERROR")
1277
1395
 
1278
1396
  # TODO (HIGH PRIORITY - Crosswalk Data Persistence and Validation):
1279
- #FIXED: Diagnosis codes are now automatically persisted to file
1280
- #FIXED: Manual save requirement has been removed
1281
- #FIXED: Error handling and logging added for file save failures
1397
+ # FIXED: Diagnosis codes are now automatically persisted to file
1398
+ # FIXED: Manual save requirement has been removed
1399
+ # FIXED: Error handling and logging added for file save failures
1282
1400
  #
1283
1401
  # REMAINING WORKFLOW ISSUES:
1284
1402
  # 1. Validation still happens during encoding (too late in the process) - FUTURE ENHANCEMENT
@@ -1303,17 +1421,163 @@ def validate_claim_data_for_837p(parsed_data, config, crosswalk):
1303
1421
  # 3. Add crosswalk versioning and change tracking
1304
1422
  # 4. Implement crosswalk sharing across multiple users/systems
1305
1423
  #
1306
- #IMPLEMENTED:
1307
- # 1.Found existing crosswalk persistence function: save_crosswalk()
1308
- # 2.Added call here: save_crosswalk(None, config, crosswalk, skip_api_operations=True)
1309
- # 3.Removed manual save message
1310
- # 4.Added error handling for file save failures
1311
- # 5.Added logging for successful crosswalk updates
1424
+ # IMPLEMENTED:
1425
+ # 1. Found existing crosswalk persistence function: save_crosswalk()
1426
+ # 2. Added call here: save_crosswalk(None, config, crosswalk, skip_api_operations=True)
1427
+ # 3. Removed manual save message
1428
+ # 4. Added error handling for file save failures
1429
+ # 5. Added logging for successful crosswalk updates
1312
1430
  #
1313
- #TESTING COMPLETED:
1314
- # -Crosswalk changes persist across application restarts
1315
- # -Error handling tested for file save failures
1316
- # - Logging verified for successful crosswalk updates
1431
+ # TESTING COMPLETED:
1432
+ # - Crosswalk changes persist across application restarts
1433
+ # - Error handling tested for file save failures
1434
+ # - Logging verified for successful crosswalk updates
1317
1435
 
1318
1436
  # Always return the validated (or minimally normalized) data structure
1319
- return validated_data
1437
+ return validated_data
1438
+
1439
+
1440
+ def validate_config_sender_codes(config, endpoint):
1441
+ """
1442
+ Validates sender/receiver identification codes in configuration before claim submission.
1443
+
1444
+ This function specifically checks for missing or empty sender and receiver
1445
+ identification codes that are critical for valid 837P interchange headers.
1446
+ This is separate from claim data validation and focuses on configuration issues.
1447
+
1448
+ IMPORTANT: This function validates the config that has already been loaded by the
1449
+ centralized MediCafe config loader. It does NOT reload configuration from disk.
1450
+
1451
+ Compatible with: Python 3.4.4, Windows XP SP3, ASCII-only output
1452
+
1453
+ Parameters:
1454
+ - config: Configuration dictionary (already loaded by centralized loader)
1455
+ - endpoint: Endpoint name being processed
1456
+
1457
+ Returns:
1458
+ list: List of critical configuration issues that would prevent successful submission
1459
+ """
1460
+ issues = []
1461
+
1462
+ # Extract MediLink config safely
1463
+ medi_config = {}
1464
+ if isinstance(config, dict):
1465
+ medi_config = config.get('MediLink_Config', {})
1466
+ if not isinstance(medi_config, dict):
1467
+ medi_config = config # Fallback to root config
1468
+
1469
+ # Check if we're using default fallback config by examining key characteristics
1470
+ # The centralized loader provides a default config when JSON files are missing
1471
+ default_characteristics = [
1472
+ medi_config.get('local_storage_path') == '.',
1473
+ medi_config.get('receiptsRoot') == './receipts',
1474
+ not medi_config.get('submitterId', '').strip(),
1475
+ not medi_config.get('submitterEdi', '').strip()
1476
+ ]
1477
+
1478
+ # If most default characteristics match, likely using fallback config
1479
+ if sum(default_characteristics) >= 3:
1480
+ issues.append("Using default fallback configuration - JSON config file may be missing or inaccessible")
1481
+
1482
+ # Check critical sender identification
1483
+ submitter_id = medi_config.get('submitterId', '').strip() if isinstance(medi_config.get('submitterId'), str) else ''
1484
+ submitter_edi = medi_config.get('submitterEdi', '').strip() if isinstance(medi_config.get('submitterEdi'), str) else ''
1485
+
1486
+ if not submitter_id:
1487
+ issues.append("submitterId is empty - ISA06 sender ID field will be blank")
1488
+ if not submitter_edi:
1489
+ issues.append("submitterEdi is empty - GS02 sender code field will be blank")
1490
+
1491
+ # Check endpoint-specific overrides
1492
+ endpoints_config = medi_config.get('endpoints', {})
1493
+ if isinstance(endpoints_config, dict):
1494
+ endpoint_config = endpoints_config.get(endpoint, {})
1495
+ if isinstance(endpoint_config, dict):
1496
+ isa_06 = endpoint_config.get('isa_06_value', '').strip() if isinstance(endpoint_config.get('isa_06_value'), str) else ''
1497
+ gs_02 = endpoint_config.get('gs_02_value', '').strip() if isinstance(endpoint_config.get('gs_02_value'), str) else ''
1498
+
1499
+ if not isa_06 and not submitter_id:
1500
+ issues.append("No ISA06 sender ID configured for endpoint '{}'".format(endpoint))
1501
+ if not gs_02 and not submitter_edi:
1502
+ issues.append("No GS02 sender code configured for endpoint '{}'".format(endpoint))
1503
+ else:
1504
+ # No endpoints configuration found
1505
+ if not submitter_id:
1506
+ issues.append("No endpoints configuration found and submitterId is empty")
1507
+ if not submitter_edi:
1508
+ issues.append("No endpoints configuration found and submitterEdi is empty")
1509
+
1510
+ # Check receiver identification
1511
+ receiver_id = medi_config.get('receiverId', '').strip() if isinstance(medi_config.get('receiverId'), str) else ''
1512
+ receiver_edi = medi_config.get('receiverEdi', '').strip() if isinstance(medi_config.get('receiverEdi'), str) else ''
1513
+
1514
+ if not receiver_id:
1515
+ issues.append("receiverId is empty - ISA08 receiver ID field will be blank")
1516
+ if not receiver_edi:
1517
+ issues.append("receiverEdi is empty - GS03 receiver code field will be blank")
1518
+
1519
+ return issues
1520
+
1521
+ # Helper: resolve config value with TestMode-only defaults enforcement
1522
+ # Defaults/placeholders are allowed ONLY in TestMode; otherwise we raise to avoid malformed 837p
1523
+ DEFAULT_PLACEHOLDER_VALUES = set([
1524
+ 'DEFAULT NAME', 'DEFAULT ID', 'DEFAULT BCBSF ID', 'DEFAULT RECEIVER NAME',
1525
+ 'DEFAULT EDI', 'DEFAULT NPI', 'DEFAULT FACILITY NAME', 'DEFAULT FACILITY NPI',
1526
+ 'DEFAULT PAY-TO NAME', 'DEFAULT PAY-TO ADDRESS', 'DEFAULT PAY-TO CITY',
1527
+ 'DEFAULT PAY-TO STATE', 'DEFAULT PAY-TO ZIP', 'NO ADDRESS', 'NO CITY',
1528
+ 'NO STATE', 'NO ZIP', 'NO TAX ID', 'NONE'
1529
+ ])
1530
+
1531
+ def _get_value_from_sources(source_dicts, key):
1532
+ for d in source_dicts:
1533
+ try:
1534
+ if isinstance(d, dict) and key in d:
1535
+ val = d.get(key)
1536
+ if val not in [None, '']:
1537
+ return val
1538
+ except Exception:
1539
+ pass
1540
+ return None
1541
+
1542
+ def legacy_require_config_value(source_dicts, key, default_value, context_label, config, endpoint=None, allow_default_in_test=True):
1543
+ """
1544
+ Fetch a configuration value from one or more dicts.
1545
+ - If found and equals a known placeholder, allow only in TestMode.
1546
+ - If missing, allow default only in TestMode (when allow_default_in_test is True).
1547
+ - Otherwise raise ValueError to prevent generating malformed 837p.
1548
+
1549
+ TODO Eventually we should get this functionality into configloader in MediCafe so that we can use it in other places.
1550
+ """
1551
+ value = _get_value_from_sources(source_dicts, key)
1552
+ # Found a value
1553
+ if value not in [None, '']:
1554
+ if isinstance(value, str) and value in DEFAULT_PLACEHOLDER_VALUES:
1555
+ if _is_test_mode(config):
1556
+ try:
1557
+ MediLink_ConfigLoader.log("TEST MODE: Placeholder used for {} -> {}".format(key, value), level="WARNING")
1558
+ except Exception:
1559
+ pass
1560
+ print("TEST MODE: Using placeholder '{}' for {} ({})".format(value, key, context_label))
1561
+ return value
1562
+ else:
1563
+ msg = "Missing real value for '{}' in {}. Found placeholder '{}'. Endpoint: {}".format(key, context_label, value, (endpoint or ''))
1564
+ try:
1565
+ MediLink_ConfigLoader.log(msg, level="CRITICAL")
1566
+ except Exception:
1567
+ pass
1568
+ raise ValueError(msg)
1569
+ return value
1570
+ # Missing value entirely
1571
+ if allow_default_in_test and _is_test_mode(config):
1572
+ try:
1573
+ MediLink_ConfigLoader.log("TEST MODE: Default used for {} -> {}".format(key, default_value), level="WARNING")
1574
+ except Exception:
1575
+ pass
1576
+ print("TEST MODE: Using default '{}' for {} ({})".format(default_value, key, context_label))
1577
+ return default_value
1578
+ msg = "Required configuration '{}' missing for {}. Endpoint: {}".format(key, context_label, (endpoint or ''))
1579
+ try:
1580
+ MediLink_ConfigLoader.log(msg, level="CRITICAL")
1581
+ except Exception:
1582
+ pass
1583
+ raise ValueError(msg)