medicafe 0.250725.18__tar.gz → 0.250728.0__tar.gz
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.
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediBot/MediBot.bat +47 -1
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediBot/MediBot.py +7 -2
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediBot/MediBot_Crosswalk_Library.py +30 -10
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediBot/MediBot_Preprocessor_lib.py +94 -6
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediBot/MediBot_UI.py +348 -310
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediBot/update_medicafe.py +29 -1
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/MediLink.py +161 -22
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/MediLink_837p_encoder.py +31 -11
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/MediLink_837p_encoder_library.py +5 -1
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/MediLink_API_v3.py +76 -18
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/MediLink_DataMgmt.py +4 -2
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/MediLink_UI.py +7 -2
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/MediLink_Up.py +32 -17
- {medicafe-0.250725.18 → medicafe-0.250728.0}/PKG-INFO +1 -1
- {medicafe-0.250725.18 → medicafe-0.250728.0}/medicafe.egg-info/PKG-INFO +1 -1
- {medicafe-0.250725.18 → medicafe-0.250728.0}/medicafe.egg-info/SOURCES.txt +0 -1
- {medicafe-0.250725.18 → medicafe-0.250728.0}/setup.py +1 -1
- medicafe-0.250725.18/MediLink/MediLink_batch.bat +0 -7
- {medicafe-0.250725.18 → medicafe-0.250728.0}/LICENSE +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MANIFEST.in +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediBot/MediBot_Charges.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediBot/MediBot_Post.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediBot/MediBot_Preprocessor.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediBot/MediBot_dataformat_library.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediBot/MediBot_docx_decoder.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediBot/PDF_to_CSV_Cleaner.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediBot/__init__.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediBot/update_json.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/MediLink_837p_cob_library.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/MediLink_837p_utilities.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/MediLink_API_Generator.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/MediLink_API_v2.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/MediLink_APIs.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/MediLink_Azure.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/MediLink_ClaimStatus.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/MediLink_ConfigLoader.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/MediLink_Decoder.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/MediLink_Deductible.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/MediLink_Deductible_Validator.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/MediLink_Down.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/MediLink_Gmail.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/MediLink_GraphQL.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/MediLink_Mailer.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/MediLink_Parser.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/MediLink_Scan.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/MediLink_Scheduler.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/Soumit_api.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/__init__.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/openssl.cnf +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/test.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/test_cob_library.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/test_validation.py +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/MediLink/webapp.html +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/README.md +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/medicafe.egg-info/dependency_links.txt +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/medicafe.egg-info/not-zip-safe +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/medicafe.egg-info/requires.txt +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/medicafe.egg-info/top_level.txt +0 -0
- {medicafe-0.250725.18 → medicafe-0.250728.0}/setup.cfg +0 -0
|
@@ -106,6 +106,42 @@ if exist "C:\Python34\Lib\site-packages\MediBot\update_medicafe.py" (
|
|
|
106
106
|
)
|
|
107
107
|
)
|
|
108
108
|
|
|
109
|
+
:: Check for updates function
|
|
110
|
+
:check_for_updates
|
|
111
|
+
:: Run the check using the existing update script
|
|
112
|
+
for /f "delims=" %%a in ('python "%upgrade_medicafe%" --check-only 2^>nul') do (
|
|
113
|
+
set "update_check_result=%%a"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
:: Check if update is available (with better error handling)
|
|
117
|
+
if "!update_check_result!"=="UP_TO_DATE" (
|
|
118
|
+
echo .
|
|
119
|
+
ping -n 2 127.0.0.1 >nul
|
|
120
|
+
) else if "!update_check_result!"=="ERROR" (
|
|
121
|
+
echo Auto-Update Check Error.
|
|
122
|
+
ping -n 2 127.0.0.1 >nul
|
|
123
|
+
) else if "!update_check_result:~0,16!"=="UPDATE_AVAILABLE:" (
|
|
124
|
+
set "new_version=!update_check_result:~16!"
|
|
125
|
+
echo.
|
|
126
|
+
echo =============================================================
|
|
127
|
+
echo UPDATE AVAILABLE
|
|
128
|
+
echo =============================================================
|
|
129
|
+
echo.
|
|
130
|
+
echo A new version of MediCafe is available!
|
|
131
|
+
echo Current version: %medicafe_version%
|
|
132
|
+
echo New version: !new_version!
|
|
133
|
+
echo.
|
|
134
|
+
echo To update, select option 1 from the menu below.
|
|
135
|
+
echo.
|
|
136
|
+
echo =============================================================
|
|
137
|
+
echo.
|
|
138
|
+
ping -n 3 127.0.0.1 >nul
|
|
139
|
+
) else (
|
|
140
|
+
:: Handle any other unexpected results (empty, malformed, etc.)
|
|
141
|
+
echo Checking for updates... Unable to check for updates.
|
|
142
|
+
ping -n 2 127.0.0.1 >nul
|
|
143
|
+
)
|
|
144
|
+
|
|
109
145
|
:: Main menu
|
|
110
146
|
:main_menu
|
|
111
147
|
cls
|
|
@@ -114,6 +150,16 @@ echo --------------------------------------------------------------
|
|
|
114
150
|
echo .//* Welcome to MediCafe *\\.
|
|
115
151
|
echo --------------------------------------------------------------
|
|
116
152
|
echo.
|
|
153
|
+
|
|
154
|
+
:: Check for updates if internet is available
|
|
155
|
+
if "!internet_available!"=="1" (
|
|
156
|
+
call :check_for_updates
|
|
157
|
+
if errorlevel 1 (
|
|
158
|
+
echo Checking for updates... Error occurred.
|
|
159
|
+
ping -n 2 127.0.0.1 >nul
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
|
|
117
163
|
echo Please select an option:
|
|
118
164
|
echo.
|
|
119
165
|
if "!internet_available!"=="1" (
|
|
@@ -184,7 +230,7 @@ goto main_menu
|
|
|
184
230
|
:medibot_flow
|
|
185
231
|
call :process_csvs
|
|
186
232
|
cls
|
|
187
|
-
echo
|
|
233
|
+
echo Starting MediBot...
|
|
188
234
|
py "%python_script2%" "%config_file%"
|
|
189
235
|
if errorlevel 1 echo Failed to run MediBot.
|
|
190
236
|
pause
|
|
@@ -318,7 +318,7 @@ class ExecutionState:
|
|
|
318
318
|
if __name__ == "__main__":
|
|
319
319
|
e_state = None
|
|
320
320
|
try:
|
|
321
|
-
print("
|
|
321
|
+
print("Initializing. Loading configuration and preparing environment...")
|
|
322
322
|
# Default paths for configuration and crosswalk files
|
|
323
323
|
default_config_path = os.path.join(os.path.dirname(__file__), '..', 'json', 'config.json')
|
|
324
324
|
default_crosswalk_path = os.path.join(os.path.dirname(__file__), '..', 'json', 'crosswalk.json')
|
|
@@ -352,7 +352,12 @@ if __name__ == "__main__":
|
|
|
352
352
|
|
|
353
353
|
print("Load Complete...")
|
|
354
354
|
MediLink_ConfigLoader.log("Load Complete event triggered. Clearing console. Displaying Menu...", level="INFO")
|
|
355
|
-
|
|
355
|
+
# Windows XP console buffer fix: Use cls with echo to reset buffer state
|
|
356
|
+
_ = os.system('cls && echo.')
|
|
357
|
+
|
|
358
|
+
# Additional buffer stabilization
|
|
359
|
+
time.sleep(0.1)
|
|
360
|
+
sys.stdout.flush()
|
|
356
361
|
|
|
357
362
|
proceed, selected_patient_ids, selected_indices, fixed_values = user_interaction(csv_data, interaction_mode, error_message, reverse_mapping)
|
|
358
363
|
|
|
@@ -716,13 +716,15 @@ def update_crosswalk_with_new_payer_id(client, insurance_name, payer_id, config,
|
|
|
716
716
|
print(message)
|
|
717
717
|
MediLink_ConfigLoader.log(message, config, level="ERROR")
|
|
718
718
|
|
|
719
|
-
def save_crosswalk(client, config, crosswalk):
|
|
719
|
+
def save_crosswalk(client, config, crosswalk, skip_api_operations=False):
|
|
720
720
|
"""
|
|
721
721
|
Saves the crosswalk to a JSON file. Ensures that all necessary keys are present and logs the outcome.
|
|
722
722
|
|
|
723
723
|
Args:
|
|
724
|
+
client (APIClient): API client for fetching payer names (ignored if skip_api_operations=True).
|
|
724
725
|
config (dict): Configuration settings for logging.
|
|
725
726
|
crosswalk (dict): The crosswalk dictionary to save.
|
|
727
|
+
skip_api_operations (bool): If True, skips API calls and user prompts for faster saves.
|
|
726
728
|
|
|
727
729
|
Returns:
|
|
728
730
|
bool: True if the crosswalk was saved successfully, False otherwise.
|
|
@@ -739,13 +741,21 @@ def save_crosswalk(client, config, crosswalk):
|
|
|
739
741
|
for payer_id, details in crosswalk.get('payer_id', {}).items():
|
|
740
742
|
current_endpoint = details.get('endpoint', None)
|
|
741
743
|
if current_endpoint and current_endpoint not in config['MediLink_Config']['endpoints']:
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
744
|
+
if skip_api_operations:
|
|
745
|
+
# Log warning but don't prompt user during API-bypass mode
|
|
746
|
+
MediLink_ConfigLoader.log("WARNING: Invalid endpoint '{}' for payer ID '{}' - skipping correction due to API bypass mode".format(current_endpoint, payer_id), config, level="WARNING")
|
|
747
|
+
else:
|
|
748
|
+
print("WARNING: The current endpoint '{}' for payer ID '{}' is not valid.".format(current_endpoint, payer_id))
|
|
749
|
+
MediLink_ConfigLoader.log("Current endpoint '{}' for payer ID '{}' is not valid. Prompting for selection.".format(current_endpoint, payer_id), config, level="WARNING")
|
|
750
|
+
selected_endpoint = select_endpoint(config, current_endpoint) # Prompt user to select a valid endpoint
|
|
751
|
+
crosswalk['payer_id'][payer_id]['endpoint'] = selected_endpoint # Update the endpoint in the crosswalk
|
|
752
|
+
MediLink_ConfigLoader.log("Updated payer ID {} with new endpoint '{}'.".format(payer_id, selected_endpoint), config, level="INFO")
|
|
747
753
|
|
|
748
754
|
try:
|
|
755
|
+
# Log API bypass mode if enabled
|
|
756
|
+
if skip_api_operations:
|
|
757
|
+
MediLink_ConfigLoader.log("save_crosswalk running in API bypass mode - skipping API calls and user prompts", config, level="INFO")
|
|
758
|
+
|
|
749
759
|
# Initialize the 'payer_id' key if it doesn't exist
|
|
750
760
|
if 'payer_id' not in crosswalk:
|
|
751
761
|
print("save_crosswalk is initializing 'payer_id' key...")
|
|
@@ -755,13 +765,23 @@ def save_crosswalk(client, config, crosswalk):
|
|
|
755
765
|
# Ensure all payer IDs have a name and initialize medisoft_id and medisoft_medicare_id as empty lists if they do not exist
|
|
756
766
|
for payer_id in crosswalk['payer_id']:
|
|
757
767
|
if 'name' not in crosswalk['payer_id'][payer_id]:
|
|
758
|
-
|
|
759
|
-
|
|
768
|
+
if skip_api_operations:
|
|
769
|
+
# Set placeholder name and log for MediBot to handle later
|
|
770
|
+
crosswalk['payer_id'][payer_id]['name'] = 'Unknown'
|
|
771
|
+
MediLink_ConfigLoader.log("Set placeholder name for payer ID {} - will be resolved by MediBot health check".format(payer_id), config, level="INFO")
|
|
772
|
+
else:
|
|
773
|
+
fetch_and_store_payer_name(client, payer_id, crosswalk, config)
|
|
774
|
+
MediLink_ConfigLoader.log("Fetched and stored payer name for payer ID: {}.".format(payer_id), config, level="DEBUG")
|
|
760
775
|
|
|
761
776
|
# Check for the endpoint key
|
|
762
777
|
if 'endpoint' not in crosswalk['payer_id'][payer_id]:
|
|
763
|
-
|
|
764
|
-
|
|
778
|
+
if skip_api_operations:
|
|
779
|
+
# Set default endpoint and log
|
|
780
|
+
crosswalk['payer_id'][payer_id]['endpoint'] = 'AVAILITY'
|
|
781
|
+
MediLink_ConfigLoader.log("Set default endpoint for payer ID {} - can be adjusted via MediBot if needed".format(payer_id), config, level="INFO")
|
|
782
|
+
else:
|
|
783
|
+
crosswalk['payer_id'][payer_id]['endpoint'] = select_endpoint(config) # Use the helper function to set the endpoint
|
|
784
|
+
MediLink_ConfigLoader.log("Initialized 'endpoint' for payer ID {}.".format(payer_id), config, level="DEBUG")
|
|
765
785
|
|
|
766
786
|
# Initialize medisoft_id and medisoft_medicare_id as empty lists if they do not exist
|
|
767
787
|
crosswalk['payer_id'][payer_id].setdefault('medisoft_id', [])
|
|
@@ -171,20 +171,108 @@ def filter_rows(csv_data):
|
|
|
171
171
|
excluded_insurance = {'AETNA', 'AETNA MEDICARE', 'HUMANA MED HMO'}
|
|
172
172
|
csv_data[:] = [row for row in csv_data if row.get('Patient ID') and row.get('Primary Insurance') not in excluded_insurance]
|
|
173
173
|
|
|
174
|
+
def clean_surgery_date_string(date_str):
|
|
175
|
+
"""
|
|
176
|
+
Cleans and normalizes surgery date strings to handle damaged data.
|
|
177
|
+
|
|
178
|
+
Parameters:
|
|
179
|
+
- date_str (str): The raw date string from the CSV
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
- str: Cleaned date string in MM/DD/YYYY format, or empty string if unparseable
|
|
183
|
+
"""
|
|
184
|
+
if not date_str:
|
|
185
|
+
return ''
|
|
186
|
+
|
|
187
|
+
# Convert to string and strip whitespace
|
|
188
|
+
date_str = str(date_str).strip()
|
|
189
|
+
if not date_str:
|
|
190
|
+
return ''
|
|
191
|
+
|
|
192
|
+
# Remove common problematic characters and normalize
|
|
193
|
+
date_str = date_str.replace('\n', ' ').replace('\r', ' ').replace('\t', ' ')
|
|
194
|
+
date_str = ' '.join(date_str.split()) # Normalize whitespace
|
|
195
|
+
|
|
196
|
+
# Handle common date format variations
|
|
197
|
+
date_formats = [
|
|
198
|
+
'%m/%d/%Y', # 12/25/2023
|
|
199
|
+
'%m-%d-%Y', # 12-25-2023
|
|
200
|
+
'%m/%d/%y', # 12/25/23
|
|
201
|
+
'%m-%d-%y', # 12-25-23
|
|
202
|
+
'%Y/%m/%d', # 2023/12/25
|
|
203
|
+
'%Y-%m-%d', # 2023-12-25
|
|
204
|
+
'%m/%d/%Y %H:%M:%S', # 12/25/2023 14:30:00
|
|
205
|
+
'%m-%d-%Y %H:%M:%S', # 12-25-2023 14:30:00
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
# Try to parse with different formats
|
|
209
|
+
for fmt in date_formats:
|
|
210
|
+
try:
|
|
211
|
+
parsed_date = datetime.strptime(date_str, fmt)
|
|
212
|
+
# Return in standard MM/DD/YYYY format
|
|
213
|
+
return parsed_date.strftime('%m/%d/%Y')
|
|
214
|
+
except ValueError:
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
# If no format matches, try to extract date components
|
|
218
|
+
try:
|
|
219
|
+
# Remove any time components and extra text
|
|
220
|
+
date_only = date_str.split()[0] # Take first part if there's extra text
|
|
221
|
+
|
|
222
|
+
# Try to extract numeric components
|
|
223
|
+
import re
|
|
224
|
+
numbers = re.findall(r'\d+', date_only)
|
|
225
|
+
|
|
226
|
+
if len(numbers) >= 3:
|
|
227
|
+
# Assume MM/DD/YYYY or MM-DD-YYYY format
|
|
228
|
+
month, day, year = int(numbers[0]), int(numbers[1]), int(numbers[2])
|
|
229
|
+
|
|
230
|
+
# Validate ranges
|
|
231
|
+
if 1 <= month <= 12 and 1 <= day <= 31 and 1900 <= year <= 2100:
|
|
232
|
+
# Handle 2-digit years
|
|
233
|
+
if year < 100:
|
|
234
|
+
year += 2000 if year < 50 else 1900
|
|
235
|
+
|
|
236
|
+
parsed_date = datetime(year, month, day)
|
|
237
|
+
return parsed_date.strftime('%m/%d/%Y')
|
|
238
|
+
except (ValueError, IndexError):
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
# If all parsing attempts fail, return empty string
|
|
242
|
+
return ''
|
|
243
|
+
|
|
174
244
|
def convert_surgery_date(csv_data):
|
|
245
|
+
"""
|
|
246
|
+
Converts surgery date strings to datetime objects with comprehensive data cleaning.
|
|
247
|
+
|
|
248
|
+
Parameters:
|
|
249
|
+
- csv_data (list): List of dictionaries containing CSV row data
|
|
250
|
+
"""
|
|
175
251
|
for row in csv_data:
|
|
176
252
|
surgery_date_str = row.get('Surgery Date', '')
|
|
253
|
+
|
|
177
254
|
if not surgery_date_str:
|
|
178
255
|
MediLink_ConfigLoader.log("Warning: Surgery Date not found for row: {}".format(row), level="WARNING")
|
|
179
|
-
# BUG This needs a cleaning step for the Surgery Date string in case we're receiving damaged data.
|
|
180
256
|
row['Surgery Date'] = datetime.min # Assign a minimum datetime value if empty
|
|
181
257
|
print("Surgery Date not found for row: {}".format(row))
|
|
182
258
|
else:
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
259
|
+
# Clean the date string first
|
|
260
|
+
cleaned_date_str = clean_surgery_date_string(surgery_date_str)
|
|
261
|
+
|
|
262
|
+
if not cleaned_date_str:
|
|
263
|
+
MediLink_ConfigLoader.log("Error: Could not clean Surgery Date '{}' for row: {}".format(surgery_date_str, row), level="ERROR")
|
|
264
|
+
row['Surgery Date'] = datetime.min # Assign a minimum datetime value if cleaning fails
|
|
265
|
+
print("Could not clean Surgery Date '{}' for row: {}".format(surgery_date_str, row))
|
|
266
|
+
else:
|
|
267
|
+
try:
|
|
268
|
+
# Parse the cleaned date string
|
|
269
|
+
row['Surgery Date'] = datetime.strptime(cleaned_date_str, '%m/%d/%Y')
|
|
270
|
+
MediLink_ConfigLoader.log("Successfully cleaned and parsed Surgery Date '{}' -> '{}' for row: {}".format(
|
|
271
|
+
surgery_date_str, cleaned_date_str, row), level="DEBUG")
|
|
272
|
+
except ValueError as e:
|
|
273
|
+
MediLink_ConfigLoader.log("Error parsing cleaned Surgery Date '{}': {} for row: {}".format(
|
|
274
|
+
cleaned_date_str, e, row), level="ERROR")
|
|
275
|
+
row['Surgery Date'] = datetime.min # Assign a minimum datetime value if parsing fails
|
|
188
276
|
|
|
189
277
|
def sort_and_deduplicate(csv_data):
|
|
190
278
|
# Create a dictionary to hold unique patients based on Patient ID
|