medicafe 0.240415.1__py3-none-any.whl → 0.240419.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of medicafe might be problematic. Click here for more details.

@@ -0,0 +1,679 @@
1
+ from datetime import datetime
2
+ import json
3
+ import requests
4
+ import time
5
+ import sys
6
+ import MediLink_ConfigLoader
7
+
8
+ """
9
+ 1. Code Refactoring: Increase modularity and clarity, particularly in segment creation functions (e.g., `create_st_segment`, `create_nm1_billing_provider_segment`), for better maintenance and readability.
10
+ 2. Endpoint Support: Extend support within segment creation for additional endpoints with attention to their unique claim submission requirements.
11
+ 3. Payer Identification Mechanism: Refine the mechanism for dynamically identifying payers, leveraging payer mappings and integrating with external APIs like Availity for precise payer information retrieval.
12
+ 4. Adherence to Endpoint-Specific Standards: Implement and verify the compliance of claim data formatting and inclusion based on the specific demands of each target endpoint within the segment creation logic.
13
+ 5. De-persisting Intermediate Files.
14
+ 6. Get an API for Optum "Entered As".
15
+ """
16
+
17
+ # Logs messages with optional error type and claim data.
18
+ def log(message, config='', level="INFO", error_type=None, claim=None):
19
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
20
+ level = level.upper() # Ensure the log level is in uppercase for consistency
21
+ claim_data = "- Claim Data: {}\n".format(claim) if claim else ""
22
+ error_info = "- Error Type: {}\n".format(error_type) if error_type else ""
23
+ full_message = "[{}] {} - {} {} {}\n".format(timestamp, level, message, claim_data, error_info).strip()
24
+ log_file_path = 'process_log.txt' if config == '' else config.get('logFilePath', 'process_log.txt')
25
+ with open(log_file_path, 'a') as log_file:
26
+ log_file.write(full_message)
27
+
28
+ # Converts date format from one format to another.
29
+ def convert_date_format(date_str):
30
+ # Parse the input date string into a datetime object using the input format
31
+ # Determine the input date format based on the length of the input string
32
+ input_format = "%m-%d-%Y" if len(date_str) == 10 else "%m-%d-%y"
33
+ date_obj = datetime.strptime(date_str, input_format)
34
+ # Format the datetime object into the desired output format and return
35
+ return date_obj.strftime("%Y%m%d")
36
+
37
+ # Formats date and time according to the specified format.
38
+ def format_datetime(dt=None, format_type='date'):
39
+ if dt is None:
40
+ dt = datetime.now()
41
+ if format_type == 'date':
42
+ return dt.strftime('%Y%m%d')
43
+ elif format_type == 'isa':
44
+ return dt.strftime('%y%m%d')
45
+ elif format_type == 'time':
46
+ return dt.strftime('%H%M')
47
+
48
+ # Constructs the ST segment for transaction set.
49
+ def create_st_segment(transaction_set_control_number):
50
+ return "ST*837*{:04d}*005010X222A1~".format(transaction_set_control_number)
51
+
52
+ # Constructs the BHT segment based on parsed data.
53
+ def create_bht_segment(parsed_data):
54
+ chart_number = parsed_data.get('CHART', 'UNKNOWN')
55
+ return "BHT*0019*00*{}*{}*{}*CH~".format(
56
+ chart_number, format_datetime(), format_datetime(format_type='time'))
57
+
58
+ # Constructs the HL segment for billing provider.
59
+ def create_hl_billing_provider_segment():
60
+ return "HL*1**20*1~"
61
+
62
+ # Constructs the NM1 segment for billing provider and includes address and Tax ID.
63
+ def create_nm1_billing_provider_segment(config, endpoint):
64
+ endpoint_config = config['endpoints'].get(endpoint.upper(), {})
65
+
66
+ # Billing provider details
67
+ billing_provider_entity_code = endpoint_config.get('billing_provider_entity_code', '85')
68
+ billing_provider_npi_qualifier = endpoint_config.get('billing_provider_npi_qualifier', 'XX')
69
+ #billing_provider_lastname = endpoint_config.get('billing_provider_lastname', config.get('default_billing_provider_name', 'DEFAULT NAME'))
70
+ #billing_provider_firstname = endpoint_config.get('billing_provider_firstname', '')
71
+ billing_provider_lastname = config.get('billing_provider_lastname', config.get('default_billing_provider_name', 'DEFAULT NAME'))
72
+ billing_provider_firstname = config.get('billing_provider_firstname', '')
73
+ billing_provider_npi = endpoint_config.get('billing_provider_npi', config.get('default_billing_provider_npi', 'DEFAULT NPI'))
74
+
75
+ # Determine billing_entity_type_qualifier based on the presence of billing_provider_firstname
76
+ billing_entity_type_qualifier = '1' if billing_provider_firstname else '2'
77
+
78
+ # Construct NM1 segment for the billing provider
79
+ nm1_segment = "NM1*{}*{}*{}*{}****{}*{}~".format(
80
+ billing_provider_entity_code,
81
+ billing_entity_type_qualifier,
82
+ billing_provider_lastname,
83
+ billing_provider_firstname,
84
+ billing_provider_npi_qualifier,
85
+ billing_provider_npi
86
+ )
87
+
88
+ # Construct address segments
89
+ address_segments = []
90
+ if config.get('billing_provider_address'):
91
+ # N3 segment for address line
92
+ address_segments.append("N3*{}~".format(config.get('billing_provider_address', 'NO ADDRESS')))
93
+ # N4 segment for City, State, ZIP
94
+ address_segments.append("N4*{}*{}*{}~".format(
95
+ config.get('billing_provider_city', 'NO CITY'),
96
+ config.get('billing_provider_state', 'NO STATE'),
97
+ config.get('billing_provider_zip', 'NO ZIP')
98
+ ))
99
+
100
+ # Assuming Tax ID is part of the same loop, otherwise move REF segment to the correct loop
101
+ ref_segment = "REF*EI*{}~".format(config.get('billing_provider_tin', 'NO TAX ID'))
102
+
103
+ # Construct PRV segment if provider taxonomy is needed, are these just for Medicaid??
104
+ #prv_segment = ""
105
+ #if config.get('billing_provider_taxonomy'):
106
+ # prv_segment = "PRV*BI*PXC*{}~".format(config.get('billing_provider_taxonomy'))
107
+
108
+ # Combine all the segments in the correct order, I think the PRV goes after the address and/or after ref
109
+ segments = [nm1_segment]
110
+ #if prv_segment:
111
+ # segments.append(prv_segment)
112
+ segments.extend(address_segments)
113
+ segments.append(ref_segment)
114
+
115
+ return segments
116
+
117
+ # Constructs the NM1 segment and accompanying details for the service facility location.
118
+ def create_service_facility_location_npi_segment(config):
119
+ """
120
+ Constructs segments for the service facility location, including the NM1 segment for identification
121
+ and accompanying N3 and N4 segments for address details.
122
+ """
123
+ facility_npi = config.get('service_facility_npi', 'DEFAULT FACILITY NPI')
124
+ facility_name = config.get('service_facility_name', 'DEFAULT FACILITY NAME')
125
+ address_line_1 = config.get('service_facility_address', 'NO ADDRESS')
126
+ city = config.get('service_facility_city', 'NO CITY')
127
+ state = config.get('service_facility_state', 'NO STATE')
128
+ zip_code = config.get('service_facility_zip', 'NO ZIP')
129
+
130
+ # NM1 segment for facility identification
131
+ nm1_segment = "NM1*77*2*{}*****XX*{}~".format(facility_name, facility_npi)
132
+ # N3 segment for facility address
133
+ n3_segment = "N3*{}~".format(address_line_1)
134
+ # N4 segment for facility city, state, and ZIP
135
+ n4_segment = "N4*{}*{}*{}~".format(city, state, zip_code)
136
+
137
+ return [nm1_segment, n3_segment, n4_segment]
138
+
139
+ # Constructs the NM1 segment for submitter name and includes PER segment for contact information.
140
+ def create_1000A_submitter_name_segment(config, endpoint):
141
+ endpoint_config = config['endpoints'].get(endpoint.upper(), {})
142
+ submitter_id_qualifier = endpoint_config.get('submitter_id_qualifier', '46') # '46' for ETIN or 'XX' for NPI
143
+ submitter_name = endpoint_config.get('nm_103_value', 'DEFAULT NAME') # Default name if not in config
144
+ submitter_id = endpoint_config.get('nm_109_value', 'DEFAULT ID') # Default ID if not specified in endpoint
145
+
146
+ # Submitter contact details
147
+ contact_name = config.get('submitter_contact_name', 'RAFAEL OLIVERVIDAUD')
148
+ contact_telephone_number = config.get('submitter_contact_tel', '9543821782')
149
+
150
+ # Construct NM1 segment for the submitter
151
+ nm1_segment = "NM1*41*2*{}*****{}*{}~".format(submitter_name, submitter_id_qualifier, submitter_id)
152
+
153
+ # Construct PER segment for the submitter's contact information
154
+ per_segment = "PER*IC*{}*TE*{}~".format(contact_name, contact_telephone_number)
155
+
156
+ return [nm1_segment, per_segment]
157
+
158
+ # Constructs the NM1 segment for the receiver (1000B).
159
+ def create_1000B_receiver_name_segment(config, endpoint):
160
+ # Retrieve endpoint specific configuration
161
+ endpoint_config = config['endpoints'].get(endpoint.upper(), {})
162
+
163
+ # Set the entity identifier code to '40' for receiver and qualifier to '46' for EDI,
164
+ # unless specified differently in the endpoint configuration.
165
+ receiver_entity_code = '40'
166
+ receiver_id_qualifier = endpoint_config.get('receiver_id_qualifier', '46')
167
+ receiver_name = endpoint_config.get('receiver_name', 'DEFAULT RECEIVER NAME')
168
+ receiver_edi = endpoint_config.get('receiver_edi', 'DEFAULT EDI')
169
+
170
+ return "NM1*{entity_code}*2*{receiver_name}*****{id_qualifier}*{receiver_edi}~".format(
171
+ entity_code=receiver_entity_code,
172
+ receiver_name=receiver_name,
173
+ id_qualifier=receiver_id_qualifier,
174
+ receiver_edi=receiver_edi
175
+ )
176
+
177
+ def map_insurance_name_to_payer_id(insurance_name, config):
178
+ """
179
+ Maps the insurance name provided by Medisoft to the corresponding payer ID by referencing
180
+ a crosswalk mapping stored in the "crosswalk" JSON file. This file must be located at a path
181
+ specified in the config under the key 'crosswalk_path'.
182
+
183
+ Inputs:
184
+ - insurance_name: The name of the insurance as provided by Medisoft.
185
+
186
+ Outputs:
187
+ - payer_id: The unique identifier (ID) corresponding to the insurance name, if the mapping
188
+ is successful. It will be a 5-character alphanumeric code.
189
+
190
+ Functionality:
191
+ - This function retrieves the mapping from a "crosswalk" JSON file.
192
+ - If the mapping is successful, it returns the payer ID.
193
+ - If the mapping fails or the payer ID cannot be retrieved, it raises an error indicating
194
+ the inability to extract the payer ID.
195
+ """
196
+ _, crosswalk = MediLink_ConfigLoader.load_configuration(None, config.get('crosswalkPath', 'crosswalk.json'))
197
+
198
+ try:
199
+ payer_id = crosswalk['medisoft_insurance_to_payer_id'].get(insurance_name)
200
+ if not payer_id:
201
+ raise ValueError("No payer ID found for insurance name: {}".format(insurance_name))
202
+ return payer_id
203
+ except json.JSONDecodeError:
204
+ raise ValueError("Error decoding the crosswalk JSON file")
205
+
206
+ def fetch_payer_name_from_crosswalk(payer_id, config, endpoint):
207
+ """
208
+ Retrieves the payer name corresponding to a given payer ID from a crosswalk mapping.
209
+
210
+ Parameters:
211
+ - payer_id: The unique identifier for the payer whose name is to be resolved.
212
+ - config: Configuration dictionary including the path to the crosswalk JSON.
213
+
214
+ Returns:
215
+ - The payer name corresponding to the payer ID.
216
+
217
+ Raises:
218
+ - ValueError if the payer ID is not found in the crosswalk mapping.
219
+ """
220
+ # Ensure the 'crosswalkPath' key exists in config and contains the path to the crosswalk JSON file
221
+ if 'crosswalkPath' not in config:
222
+ raise ValueError("Crosswalk path not defined in configuration.")
223
+
224
+ crosswalk_path = config.get('crosswalkPath')
225
+ try:
226
+ with open(crosswalk_path, 'r') as file:
227
+ crosswalk_mappings = json.load(file)
228
+ # Select the endpoint-specific section from the mappings
229
+ endpoint_mappings = crosswalk_mappings['payer_id_to_name'].get(endpoint, {})
230
+ payer_name = endpoint_mappings.get(payer_id)
231
+
232
+ if not payer_name:
233
+ raise ValueError("Payer name not found for payer ID: {} in {} mappings.".format(payer_id, endpoint))
234
+
235
+ return payer_name
236
+ except FileNotFoundError:
237
+ raise FileNotFoundError("Crosswalk file not found at {}".format(crosswalk_path))
238
+ except json.JSONDecodeError as e:
239
+ raise ValueError("Error decoding the crosswalk JSON file: {}".format(e))
240
+
241
+ def create_2010BB_payer_information_segment(parsed_data, config, endpoint):
242
+ """
243
+ Dynamically generates the NM1 segment for the payer in the 2010BB loop of an 837P transaction.
244
+ This process involves mapping the insurance name to the correct payer ID and then resolving the payer name.
245
+ The function prioritizes the Availity API for payer name resolution (currently the only API integrated)
246
+ and falls back to crosswalk mapping if necessary or when additional APIs are not available.
247
+
248
+ Parameters:
249
+ - parsed_data: Dictionary containing parsed claim data, including the insurance name.
250
+ - config: Configuration dictionary including endpoint-specific settings and crosswalk paths.
251
+ - endpoint: Target endpoint for submission, e.g., 'Availity'. Future integrations may include 'Optum', 'PNT_DATA'.
252
+
253
+ Returns:
254
+ - A formatted NM1*PR segment string for the 2010BB loop, correctly identifying the payer with its name and ID.
255
+
256
+ Process:
257
+ 1. Maps the insurance name to a payer ID using a crosswalk mapping or configuration.
258
+ 2. Attempts to resolve the payer name using the Availity API based on the payer ID.
259
+ 3. Falls back to crosswalk mapping for payer name resolution if the API call fails or is not applicable.
260
+
261
+ Raises an error if the payer ID extraction or payer name resolution fails, ensuring transaction integrity.
262
+
263
+ A reference for a payer ID to payer name mapping for Optum can be found here:
264
+ https://iedi.optum.com/iedi/enspublic/Download/Payerlists/Medicalpayerlist.pdf
265
+ This or similar resources could be used to populate the initial configuration mapping.
266
+ """
267
+ # Step 1: Map insurance name to Payer ID
268
+ insurance_name = parsed_data.get('INAME', '')
269
+ payer_id = map_insurance_name_to_payer_id(insurance_name, config)
270
+
271
+ payer_name = ''
272
+ # Step 2: Attempt to Retrieve Payer Name from Availity API (current API integration)
273
+ endpoint = 'availity' # Force availity API because none of the other ones are connected BUG
274
+ if True: #endpoint.lower() == 'availity':
275
+ try:
276
+ payer_name = fetch_payer_name_from_api(payer_id, config, endpoint)
277
+ log("Resolved payer name {} via {} API for Payer ID: {}".format(payer_name, endpoint, payer_id), config, level="INFO")
278
+ except Exception as api_error:
279
+ print("{} API call failed for Payer ID '{}': {}".format(endpoint, payer_id, api_error))
280
+
281
+ # Placeholder for future API integrations with other endpoints
282
+ # elif endpoint.lower() == 'optum':
283
+ # payer_name = fetch_payer_name_from_optum_api(payer_id, config)
284
+ # elif endpoint.lower() == 'pnt_data':
285
+ # payer_name = fetch_payer_name_from_pnt_data_api(payer_id, config)
286
+
287
+ # Step 3: Fallback to Crosswalk Mapping if API resolution fails or is not applicable
288
+ if not payer_name:
289
+ payer_name = fetch_payer_name_from_crosswalk(payer_id, config, endpoint)
290
+
291
+ # Construct and return the NM1*PR segment for the 2010BB loop with the payer name and ID
292
+ # Is this supposed to be PI instead of PR?
293
+ return "NM1*PR*2*{}*****PI*{}~".format(payer_name, payer_id)
294
+
295
+ def create_nm1_payto_address_segments(config):
296
+ """
297
+ Constructs the NM1 segment for the Pay-To Address, N3 for street address, and N4 for city, state, and ZIP.
298
+ This is used if the Pay-To Address is different from the Billing Provider Address.
299
+ """
300
+ payto_provider_name = config.get('payto_provider_name', 'DEFAULT PAY-TO NAME')
301
+ payto_address = config.get('payto_address', 'DEFAULT PAY-TO ADDRESS')
302
+ payto_city = config.get('payto_city', 'DEFAULT PAY-TO CITY')
303
+ payto_state = config.get('payto_state', 'DEFAULT PAY-TO STATE')
304
+ payto_zip = config.get('payto_zip', 'DEFAULT PAY-TO ZIP')
305
+
306
+ nm1_segment = "NM1*87*2*{}~".format(payto_provider_name) # '87' indicates Pay-To Provider
307
+ n3_segment = "N3*{}~".format(payto_address)
308
+ n4_segment = "N4*{}*{}*{}~".format(payto_city, payto_state, payto_zip)
309
+
310
+ return [nm1_segment, n3_segment, n4_segment]
311
+
312
+ def create_payer_address_segments(config):
313
+ """
314
+ Constructs the N3 and N4 segments for the payer's address.
315
+
316
+ """
317
+ payer_address_line_1 = config.get('payer_address_line_1', '')
318
+ payer_city = config.get('payer_city', '')
319
+ payer_state = config.get('payer_state', '')
320
+ payer_zip = config.get('payer_zip', '')
321
+
322
+ n3_segment = "N3*{}~".format(payer_address_line_1)
323
+ n4_segment = "N4*{}*{}*{}~".format(payer_city, payer_state, payer_zip)
324
+
325
+ return [n3_segment, n4_segment]
326
+
327
+ # Fetches the payer name from API based on the payer ID.
328
+ # Initialize a global dictionary to store the access token and its expiry time
329
+ # BUG This will need to get setup for each endpoint separately.
330
+ token_cache = {
331
+ 'access_token': None,
332
+ 'expires_at': 0 # Timestamp of when the token expires
333
+ }
334
+
335
+ def get_access_token(endpoint_config):
336
+ current_time = time.time()
337
+
338
+ # Check if the cached token is still valid
339
+ if token_cache['access_token'] and token_cache['expires_at'] > current_time:
340
+ return token_cache['access_token']
341
+
342
+ # Validate endpoint configuration
343
+ if not endpoint_config or not all(k in endpoint_config for k in ['client_id', 'client_secret', 'token_url']):
344
+ raise ValueError("Endpoint configuration is incomplete or missing necessary fields.")
345
+
346
+ # Extract credentials and URL from the config
347
+ CLIENT_ID = endpoint_config.get("client_id")
348
+ CLIENT_SECRET = endpoint_config.get("client_secret")
349
+ url = endpoint_config.get("token_url")
350
+
351
+ # Setup the data payload and headers for the HTTP request
352
+ data = {
353
+ 'grant_type': 'client_credentials',
354
+ 'client_id': CLIENT_ID,
355
+ 'client_secret': CLIENT_SECRET,
356
+ 'scope': 'hipaa'
357
+ }
358
+ headers = {
359
+ 'Content-Type': 'application/x-www-form-urlencoded'
360
+ }
361
+
362
+ try:
363
+ # Perform the HTTP request to get the access token
364
+ response = requests.post(url, headers=headers, data=data)
365
+ response.raise_for_status() # This will raise an exception for HTTP error statuses
366
+ json_response = response.json()
367
+ access_token = json_response.get('access_token')
368
+ expires_in = json_response.get('expires_in', 3600) # Default to 3600 seconds if not provided
369
+
370
+ if not access_token:
371
+ raise ValueError("No access token returned by the server.")
372
+
373
+ # Store the access token and calculate the expiry time
374
+ token_cache['access_token'] = access_token
375
+ token_cache['expires_at'] = current_time + expires_in - 120 # Subtracting 120 seconds to provide buffer
376
+
377
+ return access_token
378
+ except requests.RequestException as e:
379
+ # Handle HTTP errors (e.g., network problems, invalid response)
380
+ error_msg = "Failed to retrieve access token: {0}. Response status: {1}".format(str(e), response.status_code if response else 'No response')
381
+ raise Exception(error_msg)
382
+ except ValueError as e:
383
+ # Handle specific errors like missing access token
384
+ raise Exception("Configuration or server response error: {0}".format(str(e)))
385
+
386
+ def fetch_payer_name_from_api(payer_id, config, endpoint):
387
+
388
+ endpoint_config = config['endpoints'].get(endpoint.upper(), {})
389
+ # Currently configured for Availity
390
+ token = get_access_token(endpoint_config) # Get the access token using the function above
391
+
392
+ api_url = endpoint_config.get("api_url", "")
393
+ headers = {
394
+ 'Authorization': 'Bearer {0}'.format(token),
395
+ 'Accept': 'application/json'
396
+ }
397
+ params = {'payerId': payer_id}
398
+ # print(params) DEBUG
399
+
400
+ try:
401
+ response = requests.get(api_url, headers=headers, params=params)
402
+ response.raise_for_status() # This will raise an HTTPError if the HTTP request returned an unsuccessful status code
403
+ data = response.json()
404
+ # print(data) DEBUG
405
+
406
+ # Check if 'payers' key exists and has at least one item
407
+ if 'payers' in data and data['payers']:
408
+ payer = data['payers'][0]
409
+ payer_name = payer.get('displayName') or payer.get('name', 'No name available')
410
+ return payer_name
411
+ else:
412
+ raise ValueError("No payer found for ID: {0}".format(payer_id))
413
+ except requests.RequestException as e:
414
+ print("Error fetching payer name: {0}".format(e))
415
+ raise
416
+
417
+ # Test Case for API fetch
418
+ #payer_id = "11347"
419
+ #config = load_configuration()
420
+ #payer_name = fetch_payer_name_from_api(payer_id, config, endpoint='AVAILITY')
421
+ #print(payer_id, payer_name)
422
+
423
+ # Constructs the PRV segment for billing provider.
424
+ def create_billing_prv_segment(config, endpoint):
425
+ if endpoint.lower() == 'optumedi':
426
+ return "PRV*BI*PXC*{}~".format(config['billing_provider_taxonomy'])
427
+ return ""
428
+
429
+ # Constructs the HL segment for subscriber [hierarchical level (HL*2)]
430
+ def create_hl_subscriber_segment():
431
+ return ["HL*2*1*22*0~"]
432
+
433
+ # Constructs the SBR segment for subscriber based on parsed data and configuration.
434
+ def create_sbr_segment(config, parsed_data, endpoint):
435
+ # Determine the payer responsibility sequence number code based on the payer type
436
+ # If the payer is Medicare, use 'P' (Primary)
437
+ # If the payer is not Medicare and is primary insurance, use 'P' (Primary)
438
+ # If the payer is secondary insurance after Medicare, use 'S' (Secondary)
439
+ # For simplicity, we're going to assume everything is Primary for now.
440
+ responsibility_code = 'P'
441
+
442
+ # Determine the insurance type code based on the payer
443
+ # Map different payers to their respective insurance type codes
444
+ # For simplicity, 'CI' (Commercial Insurance) as a default
445
+ insurance_type_code = 'CI'
446
+
447
+ # Construct the SBR segment using the determined codes
448
+ sbr_segment = "SBR*{responsibility_code}*18*******{insurance_type_code}~".format(
449
+ responsibility_code=responsibility_code,
450
+ insurance_type_code=insurance_type_code
451
+ )
452
+
453
+ return sbr_segment
454
+
455
+ # Constructs the NM1 segment for subscriber based on parsed data and configuration.
456
+ def create_nm1_subscriber_segment(config, parsed_data, endpoint):
457
+ if endpoint.lower() == 'optumedi':
458
+ entity_identifier_code = config['endpoints']['OPTUMEDI'].get('subscriber_entity_code', 'IL')
459
+ else:
460
+ entity_identifier_code = 'IL' # Default value if endpoint is not 'optumedi'
461
+
462
+ return "NM1*{entity_identifier_code}*1*{last_name}*{first_name}*{middle_name}***MI*{policy_number}~".format(
463
+ entity_identifier_code=entity_identifier_code,
464
+ last_name=parsed_data['LAST'],
465
+ first_name=parsed_data['FIRST'],
466
+ middle_name=parsed_data['MIDDLE'],
467
+ policy_number=parsed_data['IPOLICY']
468
+ )
469
+
470
+ # Constructs the N3 and N4 segments for subscriber address based on parsed data.
471
+ def create_subscriber_address_segments(parsed_data):
472
+ return [
473
+ "N3*{}~".format(parsed_data['ISTREET']),
474
+ "N4*{}*{}*{}~".format(
475
+ parsed_data['ICITY'],
476
+ parsed_data['ISTATE'],
477
+ parsed_data['IZIP'][:5]
478
+ )
479
+ ]
480
+
481
+ # Constructs the DMG segment for subscriber based on parsed data.
482
+ def create_dmg_segment(parsed_data):
483
+ return "DMG*D8*{}*{}~".format(
484
+ convert_date_format(parsed_data['BDAY']),
485
+ parsed_data['SEX'] # ensure it returns a string instead of a list if it only returns one segment
486
+ )
487
+
488
+ def create_nm1_rendering_provider_segment(config, is_rendering_provider_different=False):
489
+ """
490
+ #BUG This is getting placed incorrectly.
491
+
492
+ Constructs the NM1 segment for the rendering provider in the 2310B loop using configuration data.
493
+
494
+ Parameters:
495
+ - config: Configuration dictionary including rendering provider details.
496
+ - is_rendering_provider_different: Boolean indicating if rendering provider differs from billing provider.
497
+
498
+ Returns:
499
+ - List containing the NM1 segment for the rendering provider if required, otherwise an empty list.
500
+ """
501
+ if is_rendering_provider_different:
502
+ segments = []
503
+ rp_npi = config.get('rendering_provider_npi', '')
504
+ rp_last_name = config.get('rendering_provider_last_name', '')
505
+ rp_first_name = config.get('rendering_provider_first_name', '')
506
+
507
+ # NM1 Segment for Rendering Provider
508
+ segments.append("NM1*82*1*{0}*{1}**XX*{2}~".format(
509
+ rp_last_name,
510
+ rp_first_name,
511
+ rp_npi
512
+ ))
513
+
514
+ # PRV Segment for Rendering Provider Taxonomy
515
+ if config.get('rendering_provider_taxonomy'):
516
+ segments.append("PRV*PE*PXC*{}~".format(config['billing_provider_taxonomy']))
517
+
518
+ return segments
519
+ else:
520
+ return []
521
+
522
+ def format_claim_number(chart_number, date_of_service):
523
+ # Remove any non-alphanumeric characters from chart number and date
524
+ chart_number_alphanumeric = ''.join(filter(str.isalnum, chart_number))
525
+ date_of_service_alphanumeric = ''.join(filter(str.isalnum, date_of_service))
526
+
527
+ # Combine the alphanumeric components without spaces
528
+ formatted_claim_number = chart_number_alphanumeric + date_of_service_alphanumeric
529
+
530
+ return formatted_claim_number
531
+
532
+ # Constructs the CLM and related segments based on parsed data and configuration.
533
+ def create_clm_and_related_segments(parsed_data, config):
534
+ """
535
+ Insert the claim information (2300 loop),
536
+ ensuring that details such as claim ID, total charge amount,and service date are included.
537
+
538
+ The HI segment for Health Care Diagnosis Codes should accurately reflect the diagnosis related to the service line.
539
+
540
+ Service Line Information (2400 Loop):
541
+ Verify that the service line number (LX), professional service (SV1), and service date (DTP) segments contain
542
+ accurate information and are formatted according to the claim's details.
543
+ """
544
+
545
+ segments = []
546
+
547
+ # Format the claim number
548
+ chart_number = parsed_data.get('CHART', '')
549
+ date_of_service = parsed_data.get('DATE', '')
550
+ formatted_claim_number = format_claim_number(chart_number, date_of_service)
551
+
552
+ # CLM - Claim Information
553
+ segments.append("CLM*{}*{}***{}:B:1*Y*A*Y*Y~".format(
554
+ formatted_claim_number,
555
+ parsed_data['AMOUNT'],
556
+ parsed_data['TOS']))
557
+
558
+ # HI - Health Care Diagnosis Code
559
+ # The code value should be preceded by the qualifier "ABK" or "BK" to specify the type of
560
+ # diagnosis code being used. "ABK" typically indicates ICD-10 codes, while "BK" indicates ICD-9 codes.
561
+ segments.append("HI*ABK:H{}~".format(''.join(char for char in parsed_data['DIAG'] if char.isalnum() or char.isdigit())))
562
+
563
+ # (2310C Loop) Service Facility Location NPI and Address Information
564
+ segments.extend(create_service_facility_location_npi_segment(config))
565
+
566
+ # For future reference, SBR - (Loop 2320: OI, NM1 (2330A), N3, N4, NM1 (2330B)) - Other Subscriber Information goes here.
567
+
568
+ # LX - Service Line Number
569
+ segments.append("LX*1~")
570
+
571
+ # SV1 - Professional Service
572
+ segments.append("SV1*HC:{}:{}*{}*MJ*{}***1~".format(
573
+ parsed_data['CODEA'],
574
+ parsed_data['POS'],
575
+ parsed_data['AMOUNT'],
576
+ parsed_data['MINTUES']))
577
+
578
+ # DTP - Date
579
+ segments.append("DTP*472*D8*{}~".format(convert_date_format(parsed_data['DATE'])))
580
+
581
+ # Is there REF - Line Item Control Number missing here?
582
+
583
+ return segments
584
+
585
+ # Generates the ISA and GS segments for the interchange header based on configuration and endpoint.
586
+ def create_interchange_header(config, endpoint):
587
+ """
588
+ Generate ISA and GS segments for the interchange header, ensuring endpoint-specific requirements are met.
589
+ Includes support for Availity, Optum, and PNT_DATA endpoints, with streamlined configuration and default handling.
590
+
591
+ Parameters:
592
+ - config: Configuration dictionary with settings and identifiers.
593
+ - endpoint: String indicating the target endpoint ('Availity', 'Optum', 'PNT_DATA').
594
+
595
+ Returns:
596
+ - Tuple containing the ISA and GS segment strings.
597
+ """
598
+ endpoint_config = config['endpoints'].get(endpoint.upper(), {})
599
+
600
+ # Set defaults for ISA segment values
601
+ isa02 = isa04 = " " # Default value for ISA02 and ISA04
602
+ isa05 = isa07 = 'ZZ' # Default qualifier
603
+ isa13 = '000000001' # Default Interchange Control Number. Not sure what to do with this. date? See also trailer
604
+ isa15 = 'P' # 'T' for Test, 'P' for Production
605
+
606
+ # Conditional values from config
607
+ isa_sender_id = endpoint_config.get('isa_06_value', config.get('submitterId', '')).rstrip()
608
+ isa07_value = endpoint_config.get('isa_07_value', isa07)
609
+ isa_receiver_id = endpoint_config.get('isa_08_value', config.get('receiverId', '')).rstrip()
610
+ isa13_value = endpoint_config.get('isa_13_value', isa13) # Needs to match IEA02
611
+ gs_sender_code = endpoint_config.get('gs_02_value', config.get('submitterEdi', ''))
612
+ gs_receiver_code = endpoint_config.get('gs_03_value', config.get('receiverEdi', ''))
613
+ isa15_value = endpoint_config.get('isa_15_value', isa15)
614
+
615
+ # ISA Segment
616
+ isa_segment = "ISA*00*{}*00*{}*{}*{}*{}*{}*{}*{}*^*00501*{}*0*{}*:~".format(
617
+ isa02, isa04, isa05, isa_sender_id.ljust(15), isa07_value, isa_receiver_id.ljust(15),
618
+ format_datetime(format_type='isa'), format_datetime(format_type='time'), isa13_value, isa15_value
619
+ )
620
+
621
+ # GS Segment
622
+ # GS04 YYYYMMDD
623
+ # GS06 Group Control Number, Field Length 1/9, must match GE02
624
+ gs06 = '1' # Placeholder for now?
625
+
626
+ gs_segment = "GS*HC*{}*{}*{}*{}*{}*X*005010X222A1~".format(
627
+ gs_sender_code, gs_receiver_code, format_datetime(), format_datetime(format_type='time'), gs06
628
+ )
629
+
630
+ log("Created interchange header for endpoint: {}".format(endpoint), config, level="INFO")
631
+
632
+ return isa_segment, gs_segment
633
+
634
+ # Generates the GE and IEA segments for the interchange trailer based on the number of transactions and functional groups.
635
+ def create_interchange_trailer(config, endpoint, num_transactions, num_functional_groups=1):
636
+ """
637
+ Generate GE and IEA segments for the interchange trailer.
638
+
639
+ Parameters:
640
+ - num_transactions: The number of transactions within the functional group.
641
+ - num_functional_groups: The number of functional groups within the interchange. Default is 1.
642
+
643
+ Returns:
644
+ - Tuple containing the GE and IEA segment strings.
645
+ """
646
+ endpoint_config = config['endpoints'].get(endpoint.upper(), {})
647
+ # GE Segment: Functional Group Trailer
648
+ # Indicates the end of a functional group and provides the count of the number of transactions within it.
649
+ # GE02 Group Control Number, Field Length 1/9, must match GS06 (Header)
650
+ ge02 = '1' #Placeholder for now?
651
+ ge_segment = "GE*{}*{}~".format(num_transactions, ge02)
652
+
653
+ # IEA Segment: Interchange Control Trailer
654
+ isa13 = '000000001' # Default Interchange Control Number. Not sure what to do with this. date? See also trailer
655
+ isa13_value = endpoint_config.get('isa_13_value', isa13) # Needs to match IEA02, can this be a date/time?
656
+ # Indicates the end of an interchange and provides the count of the number of functional groups within it.
657
+ # The Interchange Control Number needs to match isa13 and iea02
658
+ iea_segment = "IEA*{}*{}~".format(num_functional_groups, isa13_value)
659
+
660
+ log("Created interchange trailer", config, level="INFO")
661
+
662
+ return ge_segment, iea_segment
663
+
664
+ # Generates segment counts for the formatted 837P transaction and updates SE segment.
665
+ def generate_segment_counts(compiled_segments, transaction_set_control_number):
666
+ # Count the number of segments, not including the placeholder SE segment
667
+ segment_count = compiled_segments.count('~') # + 1 Including SE segment itself, but seems to be giving errors.
668
+
669
+ # Ensure transaction set control number is correctly formatted as a string
670
+ formatted_control_number = str(transaction_set_control_number).zfill(4) # Pad to ensure minimum 4 characters
671
+
672
+ # Construct the SE segment with the actual segment count and the formatted transaction set control_number
673
+ se_segment = "SE*{0}*{1}~".format(segment_count, formatted_control_number)
674
+
675
+ # Assuming the placeholder SE segment was the last segment added before compiling
676
+ # This time, we directly replace the placeholder with the correct SE segment
677
+ formatted_837p = compiled_segments.rsplit('SE**', 1)[0] + se_segment
678
+
679
+ return formatted_837p