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.
- MediBot/MediBot.py +35 -1
- MediBot/MediBot_Crosswalk_Utils.py +1 -4
- MediBot/MediBot_Preprocessor.py +88 -14
- MediBot/MediBot_Preprocessor_lib.py +97 -0
- MediBot/__init__.py +1 -1
- MediCafe/MediLink_ConfigLoader.py +6 -0
- MediCafe/__init__.py +1 -1
- MediLink/MediLink_837p_encoder.py +1 -1
- MediLink/MediLink_837p_encoder_library.py +582 -318
- MediLink/MediLink_837p_utilities.py +1 -4
- MediLink/MediLink_API_Generator.py +1 -1
- MediLink/MediLink_DataMgmt.py +2 -2
- MediLink/MediLink_Down.py +1 -1
- MediLink/MediLink_Up.py +45 -4
- MediLink/MediLink_main.py +12 -45
- MediLink/__init__.py +1 -1
- MediLink/gmail_oauth_utils.py +1 -4
- {medicafe-0.250818.0.dist-info → medicafe-0.250819.1.dist-info}/METADATA +1 -1
- {medicafe-0.250818.0.dist-info → medicafe-0.250819.1.dist-info}/RECORD +23 -23
- {medicafe-0.250818.0.dist-info → medicafe-0.250819.1.dist-info}/LICENSE +0 -0
- {medicafe-0.250818.0.dist-info → medicafe-0.250819.1.dist-info}/WHEEL +0 -0
- {medicafe-0.250818.0.dist-info → medicafe-0.250819.1.dist-info}/entry_points.txt +0 -0
- {medicafe-0.250818.0.dist-info → medicafe-0.250819.1.dist-info}/top_level.txt +0 -0
@@ -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
|
-
#
|
173
|
-
|
174
|
-
|
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 =
|
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(
|
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
|
-
|
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
|
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
|
-
#
|
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
|
227
|
-
facility_name = config
|
228
|
-
address_line_1 = config
|
229
|
-
city = config
|
230
|
-
state = config
|
231
|
-
zip_code = config
|
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
|
-
|
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
|
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
|
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
|
270
|
-
contact_telephone_number = config
|
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
|
306
|
-
receiver_edi = endpoint_config
|
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
|
-
#
|
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
|
-
#
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
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
|
-
#
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
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
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
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
|
-
|
1233
|
-
|
1234
|
-
|
1235
|
-
|
1236
|
-
|
1237
|
-
|
1238
|
-
|
1239
|
-
|
1240
|
-
|
1241
|
-
|
1242
|
-
|
1243
|
-
|
1244
|
-
|
1245
|
-
|
1246
|
-
|
1247
|
-
|
1248
|
-
print("
|
1249
|
-
|
1250
|
-
|
1251
|
-
|
1252
|
-
|
1253
|
-
|
1254
|
-
|
1255
|
-
|
1256
|
-
|
1257
|
-
|
1258
|
-
|
1259
|
-
|
1260
|
-
|
1261
|
-
|
1262
|
-
|
1263
|
-
|
1264
|
-
|
1265
|
-
if
|
1266
|
-
|
1267
|
-
|
1268
|
-
|
1269
|
-
|
1270
|
-
|
1271
|
-
|
1272
|
-
|
1273
|
-
|
1274
|
-
|
1275
|
-
|
1276
|
-
|
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
|
-
#
|
1280
|
-
#
|
1281
|
-
#
|
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
|
-
#
|
1307
|
-
# 1.
|
1308
|
-
# 2.
|
1309
|
-
# 3.
|
1310
|
-
# 4.
|
1311
|
-
# 5.
|
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
|
-
#
|
1314
|
-
# -
|
1315
|
-
# -
|
1316
|
-
# -
|
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)
|