sonuprasad23 commited on
Commit
ee55d84
·
1 Parent(s): 7f56c93

Fixing this

Browse files
Files changed (2) hide show
  1. server.py +111 -346
  2. worker.py +72 -288
server.py CHANGED
@@ -1,23 +1,10 @@
1
- import time
2
- import threading
3
- import subprocess
4
- import pandas as pd
5
- from selenium import webdriver
6
- from selenium.webdriver.chrome.service import Service as ChromeService
7
- from selenium.webdriver.chrome.options import Options as ChromeOptions
8
- from selenium.webdriver.common.by import By
9
- from selenium.webdriver.common.keys import Keys
10
- from selenium.webdriver.support.ui import WebDriverWait
11
- from selenium.webdriver.support import expected_conditions as EC
12
- from selenium.common.exceptions import TimeoutException
13
- from datetime import datetime
14
- import os
15
-
16
- # Socket / Flask stack
17
  import eventlet
18
  eventlet.monkey_patch()
19
 
 
20
  import io
 
 
21
  import base64
22
  import json
23
  import re
@@ -25,6 +12,7 @@ from datetime import datetime
25
  from flask import Flask, Response, request, jsonify
26
  from flask_socketio import SocketIO, emit
27
  from flask_cors import CORS
 
28
  from email.mime.multipart import MIMEMultipart
29
  from email.mime.text import MIMEText
30
  from email.mime.base import MIMEBase
@@ -33,216 +21,6 @@ from dotenv import load_dotenv
33
 
34
  load_dotenv()
35
 
36
-
37
- class QuantumBot:
38
- def __init__(self, socketio, app):
39
- self.socketio = socketio; self.app = app; self.driver = None
40
- self.DEFAULT_TIMEOUT = 30; self.termination_event = threading.Event()
41
-
42
- def initialize_driver(self):
43
- try:
44
- self.micro_status("Initializing headless browser with stable configuration...")
45
- options = ChromeOptions()
46
- options.binary_location = "/usr/bin/chromium"
47
- options.add_argument("--headless=new")
48
- options.add_argument("--no-sandbox")
49
- options.add_argument("--disable-dev-shm-usage")
50
- options.add_argument("--window-size=1920,1080")
51
- service = ChromeService(executable_path="/usr/bin/chromedriver")
52
- self.driver = webdriver.Chrome(service=service, options=options)
53
- self.micro_status("Browser initialized successfully.")
54
- return True, None
55
- except Exception as e:
56
- error_message = f"Message: {str(e)}"; print(f"CRITICAL ERROR in WebDriver Initialization: {error_message}")
57
- return False, error_message
58
-
59
- def micro_status(self, message):
60
- print(f"[Bot Log] {message}")
61
- with self.app.app_context():
62
- self.socketio.emit('micro_status_update', {'message': message})
63
-
64
- def stop(self):
65
- self.micro_status("Termination signal received..."); self.termination_event.set()
66
-
67
- def login(self, username, password):
68
- try:
69
- self.micro_status("Navigating to login page..."); self.driver.get("https://gateway.quantumepay.com/account/login")
70
- time.sleep(2); self.micro_status("Entering credentials...")
71
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "Username"))).send_keys(username)
72
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "Password"))).send_keys(password)
73
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "login"))).click()
74
- self.micro_status("Waiting for OTP screen...")
75
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "code1")))
76
- return True, None
77
- except Exception as e:
78
- error_message = f"Error during login: {str(e)}"; print(f"[Bot Log] ERROR during login: {error_message}")
79
- return False, error_message
80
-
81
- def submit_otp(self, otp):
82
- try:
83
- self.micro_status(f"Submitting OTP..."); otp_digits = list(otp)
84
- for i in range(6): self.driver.find_element(By.ID, f"code{i+1}").send_keys(otp_digits[i])
85
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "login"))).click()
86
- self.micro_status("Verifying login success...")
87
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//span[text()='Payments']")))
88
- return True, None
89
- except Exception as e:
90
- error_message = f"Error during OTP submission: {str(e)}"; print(f"[Bot Log] ERROR during OTP submission: {error_message}")
91
- return False, error_message
92
-
93
- def start_processing(self, process_type, patient_data, **kwargs):
94
- if process_type == 'void':
95
- return self.process_void_list(patient_data)
96
- elif process_type == 'refund':
97
- return self.process_refund_list(patient_data, kwargs.get('start_date'), kwargs.get('end_date'))
98
- else: return []
99
-
100
- def process_void_list(self, patient_data):
101
- results = []
102
- for index, record in enumerate(patient_data):
103
- if self.termination_event.is_set(): print("[Bot Log] Termination detected."); break
104
- patient_name = record['Name']; patient_prn = record.get('PRN', '')
105
- status = 'Skipped - No PRN'
106
- if patient_prn and str(patient_prn).strip():
107
- self.micro_status(f"Processing VOID for '{patient_name}' ({index + 1}/{len(patient_data)})...")
108
- status = self._process_single_void(patient_name, patient_prn)
109
- else:
110
- self.micro_status(f"Skipping '{patient_name}' (No PRN)."); time.sleep(0.5)
111
- results.append({'Name': patient_name, 'PRN': patient_prn, 'Status': status})
112
- with self.app.app_context():
113
- self.socketio.emit('log_update', {'name': patient_name, 'prn': patient_prn, 'status': status})
114
- self.socketio.emit('stats_update', {'processed': index + 1, 'remaining': len(patient_data) - (index + 1), 'status': status})
115
- return results
116
-
117
- def process_refund_list(self, patient_data, start_date_str, end_date_str):
118
- results = []
119
- try:
120
- self.micro_status("Navigating to Refund page to set date range...")
121
- self.driver.get("https://gateway.quantumepay.com/credit-card/refund")
122
- if not self._set_date_range_on_page(start_date_str, end_date_str):
123
- raise Exception("Failed to set date range.")
124
- except Exception as e:
125
- self.micro_status(f"Critical error setting date range: {e}. Aborting refund process.")
126
- for record in patient_data:
127
- results.append({'Name': record['Name'], 'PRN': record.get('PRN', ''), 'Status': 'Error - Date Setup Failed'})
128
- return results
129
- for index, record in enumerate(patient_data):
130
- if self.termination_event.is_set(): print("[Bot Log] Termination detected."); break
131
- patient_name = record['Name']; patient_prn = record.get('PRN', '')
132
- self.micro_status(f"Processing REFUND for '{patient_name}' ({index + 1}/{len(patient_data)})...")
133
- status = self._process_single_refund(patient_name, patient_prn)
134
- results.append({'Name': patient_name, 'PRN': patient_prn, 'Status': status})
135
- with self.app.app_context():
136
- self.socketio.emit('log_update', {'name': patient_name, 'prn': patient_prn, 'status': status})
137
- self.socketio.emit('stats_update', {'processed': index + 1, 'remaining': len(patient_data) - (index + 1), 'status': status})
138
- return results
139
-
140
- def _process_single_void(self, patient_name, patient_prn):
141
- try:
142
- self.micro_status(f"Navigating to Void page for '{patient_name}'")
143
- self.driver.get("https://gateway.quantumepay.com/credit-card/void")
144
- search_successful = False
145
- for attempt in range(15):
146
- try:
147
- self.micro_status(f"Searching... (Attempt {attempt + 1})")
148
- WebDriverWait(self.driver, 2).until(EC.presence_of_element_located((By.XPATH, "//div[contains(@class, 'table-wrapper')]")))
149
- search_box = WebDriverWait(self.driver, 2).until(EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']")))
150
- search_box.click(); time.sleep(0.5); search_box.clear(); time.sleep(0.5)
151
- search_box.send_keys(patient_name); search_successful = True; break
152
- except Exception: time.sleep(1)
153
- if not search_successful: raise Exception("Failed to search for patient.")
154
- time.sleep(3)
155
- self.micro_status("Opening transaction details...")
156
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, f"//tr[contains(., \"{patient_name}\")]//button[@data-v-b6b33fa0]"))).click()
157
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.LINK_TEXT, "Transaction Detail"))).click()
158
- self.micro_status("Adding to Vault...")
159
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Add to Vault']"))).click()
160
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//div[@class='modal-footer']//button/span[normalize-space()='Confirm']"))).click()
161
- try:
162
- self.micro_status("Verifying success and saving...")
163
- company_input = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.NAME, "company_name")))
164
- company_input.clear(); company_input.send_keys(patient_name)
165
- contact_input = WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable((By.NAME, "company_contact")))
166
- self.micro_status("Clearing Contact Name field...")
167
- contact_input.click(); contact_input.send_keys(Keys.CONTROL + "a"); contact_input.send_keys(Keys.BACK_SPACE)
168
- self.micro_status(f"Entering PRN: {patient_prn}...")
169
- contact_input.send_keys(str(patient_prn))
170
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Save Changes']"))).click()
171
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Confirm']]"))).click()
172
- time.sleep(5); return 'Done'
173
- except TimeoutException:
174
- self.micro_status(f"'{patient_name}' is in a bad state, cancelling.")
175
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Cancel']]"))).click()
176
- return 'Bad'
177
- except Exception as e:
178
- print(f"An error occurred during VOID for {patient_name}: {e}"); return 'Error'
179
-
180
- def _get_calendar_months(self):
181
- try:
182
- titles = self.driver.find_elements(By.XPATH, "//div[contains(@class, 'vc-title')]")
183
- return [datetime.strptime(title.text.strip(), "%B %Y") for title in titles]
184
- except Exception: return []
185
-
186
- def _select_date_in_calendar(self, target_date):
187
- target_month_str = target_date.strftime("%B %Y")
188
- self.micro_status(f"Navigating calendar to {target_month_str}...")
189
- for _ in range(24):
190
- visible_months = self._get_calendar_months()
191
- if any(d.strftime("%B %Y") == target_month_str for d in visible_months):
192
- self.micro_status(f"Found month. Selecting day {target_date.day}.")
193
- day_format = "%#d" if os.name == 'nt' else "%-d"
194
- aria_label = target_date.strftime(f"%A, %B {day_format}, %Y")
195
- day_xpath = f"//span[@aria-label='{aria_label}']"
196
- day_element = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.XPATH, day_xpath)))
197
- self.driver.execute_script("arguments.click();", day_element)
198
- return
199
- arrow_button_xpath = "//div[contains(@class, 'vc-arrow') and contains(@class, 'is-left')]" if target_date < visible_months else "//div[contains(@class, 'vc-arrow') and contains(@class, 'is-right')]"
200
- self.driver.find_element(By.XPATH, arrow_button_xpath).click()
201
- time.sleep(0.5)
202
- raise Exception(f"Could not navigate to date {target_date.strftime('%Y-%m-%d')}")
203
-
204
- def _set_date_range_on_page(self, start_date_str, end_date_str):
205
- try:
206
- self.micro_status("Opening calendar to set date range...")
207
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[contains(text(), '-')]]"))).click()
208
- time.sleep(1)
209
- start_date = datetime.strptime(start_date_str, "%Y-%m-%d")
210
- end_date = datetime.strptime(end_date_str, "%Y-%m-%d")
211
- self._select_date_in_calendar(start_date)
212
- time.sleep(1)
213
- self._select_date_in_calendar(end_date)
214
- self.micro_status("Date range set. Waiting for data to filter...")
215
- time.sleep(5)
216
- return True
217
- except Exception as e:
218
- self.micro_status(f"Error setting date range: {e}")
219
- return False
220
-
221
- def _process_single_refund(self, patient_name, patient_prn):
222
- try:
223
- search_successful = False
224
- for attempt in range(15):
225
- try:
226
- self.micro_status(f"Searching for '{patient_name}'... (Attempt {attempt+1})")
227
- search_box = WebDriverWait(self.driver, 2).until(EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']")))
228
- search_box.click(); time.sleep(0.5); search_box.clear(); time.sleep(0.5)
229
- search_box.send_keys(patient_name); search_successful = True; break
230
- except Exception: time.sleep(1)
231
- if not search_successful: raise Exception("Failed to search for patient in Refund.")
232
- time.sleep(3)
233
- self.micro_status(f"Performing refund action for '{patient_name}'...")
234
- time.sleep(2)
235
- return "Done"
236
- except Exception as e:
237
- print(f"An error occurred during REFUND for {patient_name}: {e}"); return 'Error'
238
-
239
- def shutdown(self):
240
- try:
241
- if self.driver: self.driver.quit()
242
- print("[Bot Log] Chrome session closed.")
243
- except Exception as e: print(f"[Bot Log] Error during shutdown: {e}")
244
-
245
-
246
  app = Flask(__name__)
247
  app.config['SECRET_KEY'] = 'secret-key-for-hillside-automation'
248
  FRONTEND_ORIGIN = os.getenv('FRONTEND_URL', 'https://quantbot.netlify.app')
@@ -252,7 +30,6 @@ socketio = SocketIO(app, cors_allowed_origins=[FRONTEND_ORIGIN, "http://localhos
252
  bot_instance = None
253
  session_data = {}
254
 
255
-
256
  class GmailApiService:
257
  def __init__(self):
258
  self.sender_email = os.getenv('EMAIL_SENDER'); self.service = None
@@ -266,11 +43,70 @@ class GmailApiService:
266
  self.service = build('gmail', 'v1', credentials=credentials)
267
  print("[Server Log] Gmail API Service initialized successfully")
268
  except Exception as e: print(f"[Server Log] Gmail API Error: {e}")
269
-
270
- def create_professional_email_template(self, subject, status_text, stats, custom_name, process_type):
271
  status_color = "#28a745" if "completed" in status_text else "#ffc107" if "terminated" in status_text else "#dc3545"
272
  current_date = datetime.now().strftime("%B %d, %Y at %I:%M %p")
273
- html_template = f"""<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>{subject}</title><style> * {{ margin: 0; padding: 0; box-sizing: border-box; }} body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #2c2c2c; background-color: #f8f8f8; }} .email-container {{ max-width: 700px; margin: 20px auto; background-color: #ffffff; box-shadow: 0 8px 32px rgba(139, 0, 0, 0.15); border-radius: 12px; overflow: hidden; }} .header {{ background: linear-gradient(135deg, #8A0303 0%, #4c00ff 100%); color: white; padding: 40px 30px; text-align: center; }} .header h1 {{ font-size: 32px; font-weight: 700; margin-bottom: 8px; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); }} .header p {{ font-size: 18px; opacity: 0.95; font-weight: 300; letter-spacing: 1px; }} .status-banner {{ background: {status_color}; color: white; text-align: center; padding: 18px; font-size: 16px; font-weight: 600; text-transform: uppercase; letter-spacing: 1.5px; }} .content {{ padding: 40px 30px; }} .report-info {{ background: #fdfdff; border-left: 6px solid #4c00ff; padding: 25px; margin-bottom: 35px; border-radius: 8px; box-shadow: 0 4px 12px rgba(76, 0, 255, 0.1); }} .info-grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 15px 25px; }} .info-item {{ display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #f0f0f0; }} .stats-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 20px; margin: 35px 0; }} .stat-card {{ background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 12px; padding: 25px 15px; text-align: center; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); }} .stat-number {{ font-size: 36px; font-weight: 700; color: #4c00ff; margin-bottom: 10px; }} .attachments-section {{ background: #f8f5ff; border: 1px solid #e0e0e0; border-radius: 12px; padding: 25px; margin: 35px 0; }} .footer {{ background: #2c2c2c; color: white; padding: 35px 30px; text-align: center; }} h3 {{ color: #8A0303; margin-bottom: 20px; font-size: 20px; font-weight: 600; }} </style></head><body> <div class="email-container"> <div class="header"><h1>Hillside's Quantum Automation</h1><p>Patient Processing Report</p></div> <div class="status-banner">Process {status_text}</div> <div class="content"> <div class="report-info"> <h3>Report Summary</h3> <div class="info-grid"> <div class="info-item"><span><b>Report Name</b></span><span>{custom_name}</span></div> <div class="info-item"><span><b>Process Type</b></span><span>{process_type}</span></div> <div class="info-item"><span><b>Generated</b></span><span>{current_date}</span></div> <div class="info-item"><span><b>Status</b></span><span style="font-weight: bold; color: {status_color};">{status_text}</span></div> </div> </div> <h3>Processing Results</h3> <div class="stats-grid"> <div class="stat-card"><div class="stat-number">{stats['total']}</div><div>Total in File</div></div> <div class="stat-card"><div class="stat-number">{stats['processed']}</div><div>Attempted</div></div> <div class="stat-card"><div class="stat-number">{stats['successful']}</div><div>Done</div></div> <div class="stat-card"><div class="stat-number">{stats['bad']}</div><div>Bad State</div></div> <div class="stat-card"><div class="stat-number">{stats['skipped']}</div><div>Skipped</div></div> </div> <div class="attachments-section"> <h3>Attached Reports</h3> <p style=\"margin-bottom: 1rem;\">The following files are attached and have been uploaded to Google Drive:</p> <ul style=\"list-style-position: inside; padding-left: 0;\"> <li><b>{custom_name}_Full.csv:</b> The complete report with the final status for every patient.</li> <li><b>{custom_name}_Bad.csv:</b> A filtered list of patients that resulted in a \"Bad\" state.</li> <li><b>{custom_name}_Skipped.csv:</b> A filtered list of patients that were skipped.</li> </ul> </div> </div> <div class=\"footer\"><h4>Hillside Automation</h4><p>This is an automated report. Please do not reply.</p></div> </div> </body></html>"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  return html_template
275
 
276
  def send_report(self, recipients, subject, body, attachments=None):
@@ -289,7 +125,6 @@ class GmailApiService:
289
  print(f"[Server Log] Email sent successfully! Message ID: {sent_message['id']}"); return True
290
  except Exception as e: print(f"[Server Log] Failed to send email: {e}"); return False
291
 
292
-
293
  class GoogleDriveService:
294
  def __init__(self):
295
  self.creds = None; self.service = None; self.folder_id = os.getenv('GOOGLE_DRIVE_FOLDER_ID')
@@ -302,7 +137,7 @@ class GoogleDriveService:
302
  self.service = build('drive', 'v3', credentials=self.creds)
303
  print("[Server Log] Google Drive Service initialized successfully.")
304
  except Exception as e: print(f"[Server Log] G-Drive ERROR: Could not initialize service: {e}")
305
-
306
  def upload_file(self, filename, file_content):
307
  if not self.service: return False
308
  try:
@@ -313,26 +148,22 @@ class GoogleDriveService:
313
  print(f"[Server Log] File '{filename}' uploaded to Google Drive."); return True
314
  except Exception as e: print(f"[Server Log] G-Drive ERROR: File upload failed: {e}"); return False
315
 
316
-
317
  email_service = GmailApiService()
318
  drive_service = GoogleDriveService()
319
 
320
-
321
  def get_email_list():
322
  try:
323
  with open('config/emails.conf', 'r') as f: return [line.strip() for line in f if line.strip()]
324
  except FileNotFoundError: return []
325
 
326
-
327
  def run_automation_process(session_id):
328
  global bot_instance
329
  results = []; is_terminated = False; is_crash = False
330
  try:
331
- data = session_data.get(session_id, {})
332
- patient_data = data.get('patient_data'); mode = data.get('mode')
333
  if not patient_data: raise ValueError("No patient data prepared for automation.")
334
  socketio.emit('initial_stats', {'total': len(patient_data)})
335
- results = bot_instance.start_processing(mode, patient_data, start_date=data.get('start_date'), end_date=data.get('end_date'))
336
  is_terminated = bot_instance.termination_event.is_set()
337
  except Exception as e:
338
  print(f"[Server Log] Fatal error in automation thread: {e}"); is_crash = True
@@ -343,169 +174,106 @@ def run_automation_process(session_id):
343
  if bot_instance: bot_instance.shutdown(); bot_instance = None
344
  if session_id in session_data: del session_data[session_id]
345
 
346
-
347
  def generate_and_send_reports(session_id, results, is_crash_report=False, is_terminated=False):
 
348
  data = session_data.get(session_id, {})
349
- if not data: print("[Server Log] Session data not found for reporting."); return
350
 
351
  full_df = pd.DataFrame(data.get('patient_data_for_report'))
 
352
  if results:
353
- result_df = pd.DataFrame(results).set_index('Name')
354
- full_df.set_index('Name', inplace=True)
355
- full_df['Status'] = full_df['Status'].astype('object')
356
- full_df.update(result_df); full_df.reset_index(inplace=True)
 
 
 
357
  full_df['Status'].fillna('Not Processed', inplace=True)
358
 
 
359
  final_report_df = full_df[['Name', 'PRN', 'Status']]
360
  bad_df = final_report_df[final_report_df['Status'] == 'Bad']
361
  skipped_df = final_report_df[final_report_df['Status'] == 'Skipped - No PRN']
362
 
363
  timestamp = datetime.now().strftime("%d_%b_%Y"); custom_name = data.get('filename') or timestamp
364
  full_report_name = f"{custom_name}_Full.csv"; bad_report_name = f"{custom_name}_Bad.csv"; skipped_report_name = f"{custom_name}_Skipped.csv"
 
365
  full_report_content = final_report_df.to_csv(index=False)
366
  drive_service.upload_file(full_report_name, full_report_content)
367
-
368
  attachments = {
369
  full_report_name: full_report_content,
370
  bad_report_name: bad_df.to_csv(index=False),
371
  skipped_report_name: skipped_df.to_csv(index=False)
372
  }
373
- status_text = "Terminated by User" if is_terminated else "Crashed" if is_crash_report else "Completed Successfully"
374
-
375
  stats = {
376
- 'total': len(full_df), 'processed': len(results),
 
377
  'successful': len(full_df[full_df['Status'] == 'Done']),
378
- 'bad': len(bad_df), 'skipped': len(skipped_df)
 
379
  }
380
- process_type_str = data.get('mode', 'Unknown').title()
381
- subject = f"{process_type_str} Automation Report [{status_text.upper()}]: {custom_name}"
382
-
383
- professional_body = email_service.create_professional_email_template(subject, status_text, stats, custom_name, process_type_str)
384
-
385
  email_service.send_report(data.get('emails'), subject, professional_body, attachments)
386
  socketio.emit('process_complete', {'message': f'Process {status_text}. Report sent.'})
387
 
388
-
389
  @app.route('/')
390
  def status_page():
391
  APP_STATUS_HTML = """<!DOCTYPE html><html lang="en"><head><title>API Status</title><style>body{font-family:sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#f0f2f5;}.status-box{text-align:center;padding:40px 60px;background:white;border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,0.1);}h1{font-size:24px;color:#333;margin-bottom:10px;} .indicator{font-size:18px;font-weight:600;padding:8px 16px;border-radius:20px;}.active{color:#28a745;background-color:#e9f7ea;}</style></head><body><div class="status-box"><h1>Hillside Automation API</h1><div class="indicator active">● Active</div></div></body></html>"""
392
  return Response(APP_STATUS_HTML)
393
 
394
-
395
  def extract_patient_name(raw_name):
396
  if not isinstance(raw_name, str): return ""
397
- name_only = raw_name.split('DOB').strip()
398
  return re.sub(r'[:\d\-\s]+$', '', name_only).strip()
399
 
400
-
401
- # Existing HTTP route (kept for multipart workflows)
402
- @app.route('/process_and_initialize', methods=['POST'])
403
- def handle_file_processing_and_init():
404
  session_id = 'user_session'
405
  try:
406
- data = {
407
- 'emails': request.form.getlist('emails[]'),
408
- 'filename': request.form.get('filename'),
409
- 'mode': request.form.get('mode'),
410
- 'start_date': request.form.get('start_date'),
411
- 'end_date': request.form.get('end_date')
412
- }
413
- session_data[session_id] = data
414
-
415
  if 'app_data' not in request.files or 'quantum_data' not in request.files:
416
  return jsonify({"error": "Both files are required."}), 400
417
-
418
  df_app = pd.read_excel(request.files['app_data']) if request.files['app_data'].filename.endswith('.xlsx') else pd.read_csv(request.files['app_data'])
419
  df_quantum = pd.read_excel(request.files['quantum_data']) if request.files['quantum_data'].filename.endswith('.xlsx') else pd.read_csv(request.files['quantum_data'])
420
-
421
  if 'Patient Name' not in df_app.columns or 'PRN' not in df_app.columns: raise ValueError("App Data must contain 'Patient Name' and 'PRN' columns.")
422
  if 'Name' not in df_quantum.columns: raise ValueError("Quantum Data must contain a 'Name' column.")
423
-
424
  df_app_filtered = df_app.dropna(subset=['PRN']); df_app_filtered = df_app_filtered[df_app_filtered['PRN'].astype(str).str.strip() != '']
425
  prn_lookup_dict = {extract_patient_name(row['Patient Name']): row['PRN'] for _, row in df_app_filtered.iterrows()}
426
-
427
  df_quantum['PRN'] = df_quantum['Name'].apply(lambda name: prn_lookup_dict.get(name, ""))
428
-
429
  master_df = df_quantum.copy()
430
  master_df['Status'] = ''
431
-
432
- data['patient_data_for_report'] = master_df
433
- data['patient_data'] = master_df.to_dict('records')
434
-
435
  socketio.emit('data_processed')
 
436
  return jsonify({"message": "Data processed successfully."})
437
  except Exception as e:
438
  print(f"[Server Log] ERROR during file processing: {e}")
439
  return jsonify({"error": str(e)}), 500
440
 
441
-
442
- # Socket.IO Handlers
443
  @socketio.on('connect')
444
  def handle_connect():
445
  print(f'Frontend connected.')
446
  emit('email_list', {'emails': get_email_list()})
447
 
448
-
449
- @socketio.on('get_email_list')
450
- def handle_get_email_list():
451
- emit('email_list', {'emails': get_email_list()})
452
-
453
-
454
- @socketio.on('initialize_and_process_files')
455
- def initialize_and_process_files(payload):
456
  session_id = 'user_session'
457
- try:
458
- data = {
459
- 'emails': payload.get('emails', []),
460
- 'filename': payload.get('filename'),
461
- 'mode': payload.get('mode'),
462
- 'start_date': payload.get('start_date'),
463
- 'end_date': payload.get('end_date')
464
- }
465
- session_data[session_id] = data
466
-
467
- app_b64 = payload.get('app_data_content')
468
- app_name = payload.get('app_data_filename', '')
469
- quantum_b64 = payload.get('quantum_data_content')
470
- quantum_name = payload.get('quantum_data_filename', '')
471
- if not app_b64 or not quantum_b64:
472
- emit('error', {'message': 'Both files are required.'})
473
- return
474
-
475
- app_buf = io.BytesIO(base64.b64decode(app_b64))
476
- quantum_buf = io.BytesIO(base64.b64decode(quantum_b64))
477
-
478
- df_app = pd.read_excel(app_buf) if app_name.endswith('.xlsx') else pd.read_csv(app_buf)
479
- df_quantum = pd.read_excel(quantum_buf) if quantum_name.endswith('.xlsx') else pd.read_csv(quantum_buf)
480
-
481
- if 'Patient Name' not in df_app.columns or 'PRN' not in df_app.columns:
482
- emit('error', {'message': "App Data must contain 'Patient Name' and 'PRN' columns."})
483
- return
484
- if 'Name' not in df_quantum.columns:
485
- emit('error', {'message': "Quantum Data must contain a 'Name' column."})
486
- return
487
-
488
- df_app_filtered = df_app.dropna(subset=['PRN'])
489
- df_app_filtered = df_app_filtered[df_app_filtered['PRN'].astype(str).str.strip() != '']
490
-
491
- prn_lookup_dict = {
492
- extract_patient_name(row['Patient Name']): row['PRN']
493
- for _, row in df_app_filtered.iterrows()
494
- }
495
-
496
- df_quantum['PRN'] = df_quantum['Name'].apply(lambda name: prn_lookup_dict.get(name, ""))
497
-
498
- master_df = df_quantum.copy()
499
- master_df['Status'] = ''
500
-
501
- data['patient_data_for_report'] = master_df
502
- data['patient_data'] = master_df.to_dict('records')
503
-
504
- socketio.emit('data_processed')
505
- except Exception as e:
506
- print(f"[Server Log] ERROR during file processing: {e}")
507
- emit('error', {'message': str(e)})
508
-
509
 
510
  @socketio.on('start_login')
511
  def handle_login(credentials):
@@ -520,7 +288,6 @@ def handle_login(credentials):
520
  else:
521
  emit('error', {'message': f'Failed to initialize bot: {error_message}'})
522
 
523
-
524
  @socketio.on('submit_otp')
525
  def handle_otp(data):
526
  if not bot_instance: return emit('error', {'message': 'Bot not initialized.'})
@@ -531,16 +298,14 @@ def handle_otp(data):
531
  socketio.start_background_task(run_automation_process, session_id)
532
  else: emit('error', {'message': f'OTP failed: {error_message}'})
533
 
534
-
535
  @socketio.on('terminate_process')
536
  def handle_terminate():
537
  if bot_instance: print("Termination signal received."); bot_instance.stop()
538
 
539
-
540
  if __name__ == '__main__':
541
  print("====================================================================")
542
- print(" 🤗 Hillside Automation - Definitive Multi-Workflow Platform")
543
  print(f" Frontend URL: {FRONTEND_ORIGIN}")
544
  print(f" Port: {os.getenv('PORT', 7860)}")
545
  print("====================================================================")
546
- socketio.run(app, host='0.0.0.0', port=int(os.getenv('PORT', 7860)))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import eventlet
2
  eventlet.monkey_patch()
3
 
4
+ import pandas as pd
5
  import io
6
+ import threading
7
+ import os
8
  import base64
9
  import json
10
  import re
 
12
  from flask import Flask, Response, request, jsonify
13
  from flask_socketio import SocketIO, emit
14
  from flask_cors import CORS
15
+ from worker import QuantumBot
16
  from email.mime.multipart import MIMEMultipart
17
  from email.mime.text import MIMEText
18
  from email.mime.base import MIMEBase
 
21
 
22
  load_dotenv()
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  app = Flask(__name__)
25
  app.config['SECRET_KEY'] = 'secret-key-for-hillside-automation'
26
  FRONTEND_ORIGIN = os.getenv('FRONTEND_URL', 'https://quantbot.netlify.app')
 
30
  bot_instance = None
31
  session_data = {}
32
 
 
33
  class GmailApiService:
34
  def __init__(self):
35
  self.sender_email = os.getenv('EMAIL_SENDER'); self.service = None
 
43
  self.service = build('gmail', 'v1', credentials=credentials)
44
  print("[Server Log] Gmail API Service initialized successfully")
45
  except Exception as e: print(f"[Server Log] Gmail API Error: {e}")
46
+
47
+ def create_professional_email_template(self, subject, status_text, stats, custom_name):
48
  status_color = "#28a745" if "completed" in status_text else "#ffc107" if "terminated" in status_text else "#dc3545"
49
  current_date = datetime.now().strftime("%B %d, %Y at %I:%M %p")
50
+
51
+ html_template = f"""
52
+ <!DOCTYPE html>
53
+ <html lang="en">
54
+ <head>
55
+ <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>{subject}</title>
56
+ <style>
57
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
58
+ body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #2c2c2c; background-color: #f8f8f8; }}
59
+ .email-container {{ max-width: 700px; margin: 20px auto; background-color: #ffffff; box-shadow: 0 8px 32px rgba(139, 0, 0, 0.15); border-radius: 12px; overflow: hidden; }}
60
+ .header {{ background: linear-gradient(135deg, #8A0303 0%, #4c00ff 100%); color: white; padding: 40px 30px; text-align: center; }}
61
+ .header h1 {{ font-size: 32px; font-weight: 700; margin-bottom: 8px; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); }}
62
+ .header p {{ font-size: 18px; opacity: 0.95; font-weight: 300; letter-spacing: 1px; }}
63
+ .status-banner {{ background: {status_color}; color: white; text-align: center; padding: 18px; font-size: 16px; font-weight: 600; text-transform: uppercase; letter-spacing: 1.5px; }}
64
+ .content {{ padding: 40px 30px; }}
65
+ .report-info {{ background: #fdfdff; border-left: 6px solid var(--violet); padding: 25px; margin-bottom: 35px; border-radius: 8px; box-shadow: 0 4px 12px rgba(76, 0, 255, 0.1); }}
66
+ .info-grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 15px 25px; }}
67
+ .info-item {{ display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #f0f0f0; }}
68
+ .stats-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 20px; margin: 35px 0; }}
69
+ .stat-card {{ background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 12px; padding: 25px 15px; text-align: center; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); }}
70
+ .stat-number {{ font-size: 36px; font-weight: 700; color: var(--violet); margin-bottom: 10px; }}
71
+ .attachments-section {{ background: #f8f5ff; border: 1px solid #e0e0e0; border-radius: 12px; padding: 25px; margin: 35px 0; }}
72
+ .footer {{ background: #2c2c2c; color: white; padding: 35px 30px; text-align: center; }}
73
+ h3 {{ color: var(--blood-red); margin-bottom: 20px; font-size: 20px; font-weight: 600; }}
74
+ </style>
75
+ </head>
76
+ <body>
77
+ <div class="email-container">
78
+ <div class="header"><h1>Hillside's Quantum Automation</h1><p>Patient Processing Report</p></div>
79
+ <div class="status-banner">Process {status_text}</div>
80
+ <div class="content">
81
+ <div class="report-info">
82
+ <h3>Report Summary</h3>
83
+ <div class="info-grid">
84
+ <div class="info-item"><span><b>Report Name</b></span><span>{custom_name}</span></div>
85
+ <div class="info-item"><span><b>Generated</b></span><span>{current_date}</span></div>
86
+ </div>
87
+ </div>
88
+ <h3>Processing Results</h3>
89
+ <div class="stats-grid">
90
+ <div class="stat-card"> <div class="stat-number">{stats['total']}</div> <div>Total in File</div> </div>
91
+ <div class="stat-card"> <div class="stat-number">{stats['processed']}</div> <div>Processed</div> </div>
92
+ <div class="stat-card"> <div class="stat-number">{stats['successful']}</div> <div>Done</div> </div>
93
+ <div class="stat-card"> <div class="stat-number">{stats['bad']}</div> <div>Bad State</div> </div>
94
+ <div class="stat-card"> <div class="stat-number">{stats['skipped']}</div> <div>Skipped (No PRN)</div> </div>
95
+ </div>
96
+ <div class="attachments-section">
97
+ <h3>Attached Reports</h3>
98
+ <p>The following files are attached to this email and have been uploaded to Google Drive:</p>
99
+ <ul>
100
+ <li><b>{custom_name}_Full.csv:</b> The complete report with the final status for every patient.</li>
101
+ <li><b>{custom_name}_Bad.csv:</b> A filtered list of patients that resulted in a "Bad" state.</li>
102
+ <li><b>{custom_name}_Skipped.csv:</b> A filtered list of patients that were skipped due to having no PRN.</li>
103
+ </ul>
104
+ </div>
105
+ </div>
106
+ <div class="footer"><h4>Hillside Automation</h4><p>This is an automated report. Please do not reply.</p></div>
107
+ </div>
108
+ </body></html>
109
+ """
110
  return html_template
111
 
112
  def send_report(self, recipients, subject, body, attachments=None):
 
125
  print(f"[Server Log] Email sent successfully! Message ID: {sent_message['id']}"); return True
126
  except Exception as e: print(f"[Server Log] Failed to send email: {e}"); return False
127
 
 
128
  class GoogleDriveService:
129
  def __init__(self):
130
  self.creds = None; self.service = None; self.folder_id = os.getenv('GOOGLE_DRIVE_FOLDER_ID')
 
137
  self.service = build('drive', 'v3', credentials=self.creds)
138
  print("[Server Log] Google Drive Service initialized successfully.")
139
  except Exception as e: print(f"[Server Log] G-Drive ERROR: Could not initialize service: {e}")
140
+
141
  def upload_file(self, filename, file_content):
142
  if not self.service: return False
143
  try:
 
148
  print(f"[Server Log] File '{filename}' uploaded to Google Drive."); return True
149
  except Exception as e: print(f"[Server Log] G-Drive ERROR: File upload failed: {e}"); return False
150
 
 
151
  email_service = GmailApiService()
152
  drive_service = GoogleDriveService()
153
 
 
154
  def get_email_list():
155
  try:
156
  with open('config/emails.conf', 'r') as f: return [line.strip() for line in f if line.strip()]
157
  except FileNotFoundError: return []
158
 
 
159
  def run_automation_process(session_id):
160
  global bot_instance
161
  results = []; is_terminated = False; is_crash = False
162
  try:
163
+ data = session_data.get(session_id, {}); patient_data = data.get('patient_data')
 
164
  if not patient_data: raise ValueError("No patient data prepared for automation.")
165
  socketio.emit('initial_stats', {'total': len(patient_data)})
166
+ results = bot_instance.process_patient_list(patient_data)
167
  is_terminated = bot_instance.termination_event.is_set()
168
  except Exception as e:
169
  print(f"[Server Log] Fatal error in automation thread: {e}"); is_crash = True
 
174
  if bot_instance: bot_instance.shutdown(); bot_instance = None
175
  if session_id in session_data: del session_data[session_id]
176
 
 
177
  def generate_and_send_reports(session_id, results, is_crash_report=False, is_terminated=False):
178
+ print("[Server Log] Preparing final reports...")
179
  data = session_data.get(session_id, {})
180
+ if not data: print("[Server Log] Session data not found, cannot generate report."); return
181
 
182
  full_df = pd.DataFrame(data.get('patient_data_for_report'))
183
+
184
  if results:
185
+ result_df = pd.DataFrame(results)
186
+ if not result_df.empty:
187
+ result_df.set_index('Name', inplace=True)
188
+ full_df.set_index('Name', inplace=True)
189
+ full_df.update(result_df)
190
+ full_df.reset_index(inplace=True)
191
+
192
  full_df['Status'].fillna('Not Processed', inplace=True)
193
 
194
+ # --- Definitive Reporting Fix: Select ONLY the required columns ---
195
  final_report_df = full_df[['Name', 'PRN', 'Status']]
196
  bad_df = final_report_df[final_report_df['Status'] == 'Bad']
197
  skipped_df = final_report_df[final_report_df['Status'] == 'Skipped - No PRN']
198
 
199
  timestamp = datetime.now().strftime("%d_%b_%Y"); custom_name = data.get('filename') or timestamp
200
  full_report_name = f"{custom_name}_Full.csv"; bad_report_name = f"{custom_name}_Bad.csv"; skipped_report_name = f"{custom_name}_Skipped.csv"
201
+
202
  full_report_content = final_report_df.to_csv(index=False)
203
  drive_service.upload_file(full_report_name, full_report_content)
204
+
205
  attachments = {
206
  full_report_name: full_report_content,
207
  bad_report_name: bad_df.to_csv(index=False),
208
  skipped_report_name: skipped_df.to_csv(index=False)
209
  }
210
+ status_text = "terminated by user" if is_terminated else "crashed" if is_crash_report else "completed successfully"
211
+
212
  stats = {
213
+ 'total': len(full_df),
214
+ 'processed': len(results),
215
  'successful': len(full_df[full_df['Status'] == 'Done']),
216
+ 'bad': len(bad_df),
217
+ 'skipped': len(skipped_df)
218
  }
219
+
220
+ subject = f"Automation Report [{status_text.upper()}]: {custom_name}"
221
+
222
+ professional_body = email_service.create_professional_email_template(subject, status_text, stats, custom_name)
223
+
224
  email_service.send_report(data.get('emails'), subject, professional_body, attachments)
225
  socketio.emit('process_complete', {'message': f'Process {status_text}. Report sent.'})
226
 
 
227
  @app.route('/')
228
  def status_page():
229
  APP_STATUS_HTML = """<!DOCTYPE html><html lang="en"><head><title>API Status</title><style>body{font-family:sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#f0f2f5;}.status-box{text-align:center;padding:40px 60px;background:white;border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,0.1);}h1{font-size:24px;color:#333;margin-bottom:10px;} .indicator{font-size:18px;font-weight:600;padding:8px 16px;border-radius:20px;}.active{color:#28a745;background-color:#e9f7ea;}</style></head><body><div class="status-box"><h1>Hillside Automation API</h1><div class="indicator active">● Active</div></div></body></html>"""
230
  return Response(APP_STATUS_HTML)
231
 
 
232
  def extract_patient_name(raw_name):
233
  if not isinstance(raw_name, str): return ""
234
+ name_only = raw_name.split('DOB')[0].strip()
235
  return re.sub(r'[:\d\-\s]+$', '', name_only).strip()
236
 
237
+ @app.route('/process_files_for_automation', methods=['POST'])
238
+ def handle_file_processing():
 
 
239
  session_id = 'user_session'
240
  try:
 
 
 
 
 
 
 
 
 
241
  if 'app_data' not in request.files or 'quantum_data' not in request.files:
242
  return jsonify({"error": "Both files are required."}), 400
243
+
244
  df_app = pd.read_excel(request.files['app_data']) if request.files['app_data'].filename.endswith('.xlsx') else pd.read_csv(request.files['app_data'])
245
  df_quantum = pd.read_excel(request.files['quantum_data']) if request.files['quantum_data'].filename.endswith('.xlsx') else pd.read_csv(request.files['quantum_data'])
246
+
247
  if 'Patient Name' not in df_app.columns or 'PRN' not in df_app.columns: raise ValueError("App Data must contain 'Patient Name' and 'PRN' columns.")
248
  if 'Name' not in df_quantum.columns: raise ValueError("Quantum Data must contain a 'Name' column.")
249
+
250
  df_app_filtered = df_app.dropna(subset=['PRN']); df_app_filtered = df_app_filtered[df_app_filtered['PRN'].astype(str).str.strip() != '']
251
  prn_lookup_dict = {extract_patient_name(row['Patient Name']): row['PRN'] for _, row in df_app_filtered.iterrows()}
252
+
253
  df_quantum['PRN'] = df_quantum['Name'].apply(lambda name: prn_lookup_dict.get(name, ""))
254
+
255
  master_df = df_quantum.copy()
256
  master_df['Status'] = ''
257
+
258
+ session_data[session_id]['patient_data_for_report'] = master_df
259
+ session_data[session_id]['patient_data'] = master_df.to_dict('records')
260
+
261
  socketio.emit('data_processed')
262
+ print(f"[Server Log] Data prepared. Total records: {len(master_df)}")
263
  return jsonify({"message": "Data processed successfully."})
264
  except Exception as e:
265
  print(f"[Server Log] ERROR during file processing: {e}")
266
  return jsonify({"error": str(e)}), 500
267
 
 
 
268
  @socketio.on('connect')
269
  def handle_connect():
270
  print(f'Frontend connected.')
271
  emit('email_list', {'emails': get_email_list()})
272
 
273
+ @socketio.on('initialize_session')
274
+ def handle_init(data):
 
 
 
 
 
 
275
  session_id = 'user_session'
276
+ session_data[session_id] = {'emails': data['emails'], 'filename': data['filename']}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
 
278
  @socketio.on('start_login')
279
  def handle_login(credentials):
 
288
  else:
289
  emit('error', {'message': f'Failed to initialize bot: {error_message}'})
290
 
 
291
  @socketio.on('submit_otp')
292
  def handle_otp(data):
293
  if not bot_instance: return emit('error', {'message': 'Bot not initialized.'})
 
298
  socketio.start_background_task(run_automation_process, session_id)
299
  else: emit('error', {'message': f'OTP failed: {error_message}'})
300
 
 
301
  @socketio.on('terminate_process')
302
  def handle_terminate():
303
  if bot_instance: print("Termination signal received."); bot_instance.stop()
304
 
 
305
  if __name__ == '__main__':
306
  print("====================================================================")
307
+ print(" 🤗 Hillside Automation - Definitive Version")
308
  print(f" Frontend URL: {FRONTEND_ORIGIN}")
309
  print(f" Port: {os.getenv('PORT', 7860)}")
310
  print("====================================================================")
311
+ socketio.run(app, host='0.0.0.0', port=int(os.getenv('PORT', 7860)))
worker.py CHANGED
@@ -1,5 +1,7 @@
1
  import time
2
  import threading
 
 
3
  from selenium import webdriver
4
  from selenium.webdriver.chrome.service import Service as ChromeService
5
  from selenium.webdriver.chrome.options import Options as ChromeOptions
@@ -8,47 +10,36 @@ from selenium.webdriver.common.keys import Keys
8
  from selenium.webdriver.support.ui import WebDriverWait
9
  from selenium.webdriver.support import expected_conditions as EC
10
  from selenium.common.exceptions import TimeoutException
11
- from datetime import datetime
12
- import os
13
 
14
  class QuantumBot:
15
  def __init__(self, socketio, app):
16
- self.socketio = socketio
17
- self.app = app
18
- self.driver = None
19
- self.DEFAULT_TIMEOUT = 30
20
- self.termination_event = threading.Event()
21
 
22
- def initialize_driver(self):
23
  try:
24
- self.micro_status("Initializing headless browser with stable configuration...")
25
- options = ChromeOptions()
 
26
 
27
- # Minimal, stable args (unchanged)
28
- options.add_argument("--headless=new")
29
- options.add_argument("--no-sandbox")
30
- options.add_argument("--disable-dev-shm-usage")
 
 
 
31
  options.add_argument("--window-size=1920,1080")
32
-
33
- # Pick first available Chrome/Chromium binary (robust fallback)
34
- candidates = [
35
- "/usr/bin/chromium",
36
- "/usr/bin/chromium-browser",
37
- "/usr/bin/google-chrome",
38
- "/usr/bin/google-chrome-stable"
39
- ]
40
- for p in candidates:
41
- if os.path.exists(p):
42
- options.binary_location = p
43
- break
44
-
45
  service = ChromeService(executable_path="/usr/bin/chromedriver")
46
  self.driver = webdriver.Chrome(service=service, options=options)
47
- self.micro_status("Browser initialized successfully.")
 
 
48
  return True, None
49
  except Exception as e:
50
- error_message = f"Message: {str(e)}"
51
- print(f"CRITICAL ERROR in WebDriver Initialization: {error_message}")
52
  return False, error_message
53
 
54
  def micro_status(self, message):
@@ -57,133 +48,56 @@ class QuantumBot:
57
  self.socketio.emit('micro_status_update', {'message': message})
58
 
59
  def stop(self):
60
- self.micro_status("Termination signal received...")
61
- self.termination_event.set()
62
 
63
  def login(self, username, password):
64
  try:
65
- self.micro_status("Navigating to login page...")
66
- self.driver.get("https://gateway.quantumepay.com/account/login")
67
- time.sleep(2)
68
- self.micro_status("Entering credentials...")
69
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
70
- EC.element_to_be_clickable((By.ID, "Username"))
71
- ).send_keys(username)
72
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
73
- EC.element_to_be_clickable((By.ID, "Password"))
74
- ).send_keys(password)
75
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
76
- EC.element_to_be_clickable((By.ID, "login"))
77
- ).click()
78
  self.micro_status("Waiting for OTP screen...")
79
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
80
- EC.presence_of_element_located((By.ID, "code1"))
81
- )
82
  return True, None
83
  except Exception as e:
84
- error_message = f"Error during login: {str(e)}"
85
- print(f"[Bot Log] ERROR during login: {error_message}")
86
  return False, error_message
87
 
88
  def submit_otp(self, otp):
89
  try:
90
- self.micro_status(f"Submitting OTP...")
91
- otp_digits = list(otp)
92
- for i in range(6):
93
- self.driver.find_element(By.ID, f"code{i+1}").send_keys(otp_digits[i])
94
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
95
- EC.element_to_be_clickable((By.ID, "login"))
96
- ).click()
97
  self.micro_status("Verifying login success...")
98
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
99
- EC.element_to_be_clickable((By.XPATH, "//span[text()='Payments']"))
100
- )
101
  return True, None
102
  except Exception as e:
103
- error_message = f"Error during OTP submission: {str(e)}"
104
- print(f"[Bot Log] ERROR during OTP submission: {error_message}")
105
  return False, error_message
106
 
107
- def start_processing(self, process_type, patient_data, **kwargs):
108
- if process_type == 'void':
109
- return self.process_void_list(patient_data)
110
- elif process_type == 'refund':
111
- return self.process_refund_list(
112
- patient_data, kwargs.get('start_date'), kwargs.get('end_date')
113
- )
114
- else:
115
- return []
116
-
117
- def process_void_list(self, patient_data):
118
  results = []
119
  for index, record in enumerate(patient_data):
120
- if self.termination_event.is_set():
121
- print("[Bot Log] Termination detected.")
122
- break
123
- patient_name = record['Name']
124
- patient_prn = record.get('PRN', '')
125
- status = 'Skipped - No PRN'
126
- if patient_prn and str(patient_prn).strip():
127
- self.micro_status(
128
- f"Processing VOID for '{patient_name}' ({index + 1}/{len(patient_data)})..."
129
- )
130
- status = self._process_single_void(patient_name, patient_prn)
131
- else:
132
  self.micro_status(f"Skipping '{patient_name}' (No PRN).")
133
- time.sleep(0.5)
134
-
135
- results.append({'Name': patient_name, 'PRN': patient_prn, 'Status': status})
136
- with self.app.app_context():
137
- self.socketio.emit('log_update', {
138
- 'name': patient_name, 'prn': patient_prn, 'status': status
139
- })
140
- self.socketio.emit('stats_update', {
141
- 'processed': index + 1,
142
- 'remaining': len(patient_data) - (index + 1),
143
- 'status': status
144
- })
145
- return results
146
-
147
- def process_refund_list(self, patient_data, start_date_str, end_date_str):
148
- results = []
149
- try:
150
- self.micro_status("Navigating to Refund page to set date range...")
151
- self.driver.get("https://gateway.quantumepay.com/credit-card/refund")
152
- if not self._set_date_range_on_page(start_date_str, end_date_str):
153
- raise Exception("Failed to set date range.")
154
- except Exception as e:
155
- self.micro_status(f"Critical error setting date range: {e}. Aborting refund process.")
156
- for record in patient_data:
157
- results.append({
158
- 'Name': record['Name'],
159
- 'PRN': record.get('PRN', ''),
160
- 'Status': 'Error - Date Setup Failed'
161
- })
162
- return results
163
 
164
- for index, record in enumerate(patient_data):
165
- if self.termination_event.is_set():
166
- print("[Bot Log] Termination detected.")
167
- break
168
- patient_name = record['Name']
169
- patient_prn = record.get('PRN', '')
170
- self.micro_status(
171
- f"Processing REFUND for '{patient_name}' ({index + 1}/{len(patient_data)})..."
172
- )
173
- status = self._process_single_refund(patient_name, patient_prn)
174
  results.append({'Name': patient_name, 'PRN': patient_prn, 'Status': status})
175
  with self.app.app_context():
176
- self.socketio.emit('log_update', {
177
- 'name': patient_name, 'prn': patient_prn, 'status': status
178
- })
179
- self.socketio.emit('stats_update', {
180
- 'processed': index + 1,
181
- 'remaining': len(patient_data) - (index + 1),
182
- 'status': status
183
- })
184
  return results
185
 
186
- def _process_single_void(self, patient_name, patient_prn):
187
  try:
188
  self.micro_status(f"Navigating to Void page for '{patient_name}'")
189
  self.driver.get("https://gateway.quantumepay.com/credit-card/void")
@@ -191,173 +105,43 @@ class QuantumBot:
191
  for attempt in range(15):
192
  try:
193
  self.micro_status(f"Searching... (Attempt {attempt + 1})")
194
- WebDriverWait(self.driver, 2).until(
195
- EC.presence_of_element_located((By.XPATH, "//div[contains(@class, 'table-wrapper')]"))
196
- )
197
- search_box = WebDriverWait(self.driver, 2).until(
198
- EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']"))
199
- )
200
- search_box.click()
201
- time.sleep(0.5)
202
- search_box.clear()
203
- time.sleep(0.5)
204
- search_box.send_keys(patient_name)
205
- search_successful = True
206
- break
207
- except Exception:
208
- time.sleep(1)
209
- if not search_successful:
210
- raise Exception("Failed to search for patient.")
211
-
212
  time.sleep(3)
213
  self.micro_status("Opening transaction details...")
214
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
215
- EC.element_to_be_clickable((
216
- By.XPATH,
217
- f"//tr[contains(., \"{patient_name}\")]//button[@data-v-b6b33fa0]"
218
- ))
219
- ).click()
220
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
221
- EC.element_to_be_clickable((By.LINK_TEXT, "Transaction Detail"))
222
- ).click()
223
-
224
  self.micro_status("Adding to Vault...")
225
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
226
- EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Add to Vault']"))
227
- ).click()
228
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
229
- EC.element_to_be_clickable((
230
- By.XPATH, "//div[@class='modal-footer']//button/span[normalize-space()='Confirm']"
231
- ))
232
- ).click()
233
-
234
  try:
235
  self.micro_status("Verifying success and saving...")
236
- company_input = WebDriverWait(self.driver, 10).until(
237
- EC.element_to_be_clickable((By.NAME, "company_name"))
238
- )
239
- company_input.clear()
240
- company_input.send_keys(patient_name)
241
-
242
- contact_input = WebDriverWait(self.driver, 5).until(
243
- EC.element_to_be_clickable((By.NAME, "company_contact"))
244
- )
245
  self.micro_status("Clearing Contact Name field...")
246
- contact_input.click()
247
- contact_input.send_keys(Keys.CONTROL + "a")
248
- contact_input.send_keys(Keys.BACK_SPACE)
249
-
250
  self.micro_status(f"Entering PRN: {patient_prn}...")
251
  contact_input.send_keys(str(patient_prn))
252
 
253
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
254
- EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Save Changes']"))
255
- ).click()
256
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
257
- EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Confirm']]"))
258
- ).click()
259
- time.sleep(5)
260
- return 'Done'
261
  except TimeoutException:
262
  self.micro_status(f"'{patient_name}' is in a bad state, cancelling.")
263
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
264
- EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Cancel']]"))
265
- ).click()
266
  return 'Bad'
267
-
268
  except Exception as e:
269
- print(f"An error occurred during VOID for {patient_name}: {e}")
270
- return 'Error'
271
-
272
- def _get_calendar_months(self):
273
- try:
274
- titles = self.driver.find_elements(By.XPATH, "//div[contains(@class, 'vc-title')]")
275
- return [datetime.strptime(title.text.strip(), "%B %Y") for title in titles]
276
- except Exception:
277
- return []
278
-
279
- def _select_date_in_calendar(self, target_date):
280
- target_month_str = target_date.strftime("%B %Y")
281
- self.micro_status(f"Navigating calendar to {target_month_str}...")
282
- for _ in range(24):
283
- visible_months = self._get_calendar_months()
284
- if any(d.strftime("%B %Y") == target_month_str for d in visible_months):
285
- self.micro_status(f"Found month. Selecting day {target_date.day}.")
286
- day_format = "%#d" if os.name == 'nt' else "%-d"
287
- aria_label = target_date.strftime(f"%A, %B {day_format}, %Y")
288
- day_xpath = f"//span[@aria-label='{aria_label}']"
289
- day_element = WebDriverWait(self.driver, 10).until(
290
- EC.element_to_be_clickable((By.XPATH, day_xpath))
291
- )
292
- self.driver.execute_script("arguments.click();", day_element)
293
- return
294
-
295
- arrow_button_xpath = (
296
- "//div[contains(@class, 'vc-arrow') and contains(@class, 'is-left')]"
297
- if visible_months and target_date < visible_months
298
- else "//div[contains(@class, 'vc-arrow') and contains(@class, 'is-right')]"
299
- )
300
- self.driver.find_element(By.XPATH, arrow_button_xpath).click()
301
- time.sleep(0.5)
302
-
303
- raise Exception(f"Could not navigate to date {target_date.strftime('%Y-%m-%d')}")
304
-
305
- def _set_date_range_on_page(self, start_date_str, end_date_str):
306
- try:
307
- self.micro_status("Opening calendar to set date range...")
308
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
309
- EC.element_to_be_clickable((By.XPATH, "//button[.//span[contains(text(), '-')]]"))
310
- ).click()
311
- time.sleep(1)
312
-
313
- start_date = datetime.strptime(start_date_str, "%Y-%m-%d")
314
- end_date = datetime.strptime(end_date_str, "%Y-%m-%d")
315
-
316
- self._select_date_in_calendar(start_date)
317
- time.sleep(1)
318
- self._select_date_in_calendar(end_date)
319
-
320
- self.micro_status("Date range set. Waiting for data to filter...")
321
- time.sleep(5)
322
- return True
323
- except Exception as e:
324
- self.micro_status(f"Error setting date range: {e}")
325
- return False
326
-
327
- def _process_single_refund(self, patient_name, patient_prn):
328
- try:
329
- search_successful = False
330
- for attempt in range(15):
331
- try:
332
- self.micro_status(f"Searching for '{patient_name}'... (Attempt {attempt+1})")
333
- search_box = WebDriverWait(self.driver, 2).until(
334
- EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']"))
335
- )
336
- search_box.click()
337
- time.sleep(0.5)
338
- search_box.clear()
339
- time.sleep(0.5)
340
- search_box.send_keys(patient_name)
341
- search_successful = True
342
- break
343
- except Exception:
344
- time.sleep(1)
345
-
346
- if not search_successful:
347
- raise Exception("Failed to search for patient in Refund.")
348
-
349
- time.sleep(3)
350
- self.micro_status(f"Performing refund action for '{patient_name}'...")
351
- time.sleep(2)
352
- return "Done"
353
- except Exception as e:
354
- print(f"An error occurred during REFUND for {patient_name}: {e}")
355
- return 'Error'
356
 
357
  def shutdown(self):
358
  try:
359
- if self.driver:
360
- self.driver.quit()
361
- print("[Bot Log] Chrome session closed.")
362
- except Exception as e:
363
- print(f"[Bot Log] Error during shutdown: {e}")
 
1
  import time
2
  import threading
3
+ import subprocess
4
+ import pandas as pd
5
  from selenium import webdriver
6
  from selenium.webdriver.chrome.service import Service as ChromeService
7
  from selenium.webdriver.chrome.options import Options as ChromeOptions
 
10
  from selenium.webdriver.support.ui import WebDriverWait
11
  from selenium.webdriver.support import expected_conditions as EC
12
  from selenium.common.exceptions import TimeoutException
 
 
13
 
14
  class QuantumBot:
15
  def __init__(self, socketio, app):
16
+ self.socketio = socketio; self.app = app; self.driver = None
17
+ self.DEFAULT_TIMEOUT = 30; self.termination_event = threading.Event()
 
 
 
18
 
19
+ def _kill_chrome_processes(self):
20
  try:
21
+ subprocess.run(['pkill', '-f', 'chromium'], check=True, timeout=5)
22
+ time.sleep(1)
23
+ except Exception: pass
24
 
25
+ def initialize_driver(self):
26
+ try:
27
+ self.micro_status("Initializing headless browser...")
28
+ self._kill_chrome_processes()
29
+ options = ChromeOptions(); options.binary_location = "/usr/bin/chromium"
30
+ options.add_argument("--headless=new"); options.add_argument("--no-sandbox")
31
+ options.add_argument("--disable-dev-shm-usage"); options.add_argument("--disable-gpu")
32
  options.add_argument("--window-size=1920,1080")
33
+ user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
34
+ options.add_argument(f"--user-agent={user_agent}")
 
 
 
 
 
 
 
 
 
 
 
35
  service = ChromeService(executable_path="/usr/bin/chromedriver")
36
  self.driver = webdriver.Chrome(service=service, options=options)
37
+ self.driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
38
+ 'source': "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
39
+ })
40
  return True, None
41
  except Exception as e:
42
+ error_message = f"Message: {str(e)}"; print(f"CRITICAL ERROR in WebDriver Initialization: {error_message}")
 
43
  return False, error_message
44
 
45
  def micro_status(self, message):
 
48
  self.socketio.emit('micro_status_update', {'message': message})
49
 
50
  def stop(self):
51
+ self.micro_status("Termination signal received..."); self.termination_event.set()
 
52
 
53
  def login(self, username, password):
54
  try:
55
+ self.micro_status("Navigating to login page..."); self.driver.get("https://gateway.quantumepay.com/")
56
+ time.sleep(2); self.micro_status("Entering credentials...")
57
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "Username"))).send_keys(username)
58
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "Password"))).send_keys(password)
59
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "login"))).click()
 
 
 
 
 
 
 
 
60
  self.micro_status("Waiting for OTP screen...")
61
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "code1")))
 
 
62
  return True, None
63
  except Exception as e:
64
+ error_message = f"Error during login: {str(e)}"; print(f"[Bot Log] ERROR during login: {error_message}")
 
65
  return False, error_message
66
 
67
  def submit_otp(self, otp):
68
  try:
69
+ self.micro_status(f"Submitting OTP..."); otp_digits = list(otp)
70
+ for i in range(6): self.driver.find_element(By.ID, f"code{i+1}").send_keys(otp_digits[i])
71
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "login"))).click()
 
 
 
 
72
  self.micro_status("Verifying login success...")
73
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//span[text()='Payments']")))
 
 
74
  return True, None
75
  except Exception as e:
76
+ error_message = f"Error during OTP submission: {str(e)}"; print(f"[Bot Log] ERROR during OTP submission: {error_message}")
 
77
  return False, error_message
78
 
79
+ def process_patient_list(self, patient_data):
 
 
 
 
 
 
 
 
 
 
80
  results = []
81
  for index, record in enumerate(patient_data):
82
+ if self.termination_event.is_set(): print("[Bot Log] Termination detected."); break
83
+ patient_name = record['Name']; patient_prn = record.get('PRN', '')
84
+
85
+ # --- Definitive Conditional Logic ---
86
+ if not patient_prn or not str(patient_prn).strip():
 
 
 
 
 
 
 
87
  self.micro_status(f"Skipping '{patient_name}' (No PRN).")
88
+ status = 'Skipped - No PRN'
89
+ time.sleep(0.5) # A brief pause to make the skip visible on the UI
90
+ else:
91
+ self.micro_status(f"Processing '{patient_name}' ({index + 1}/{len(patient_data)})...")
92
+ status = self._process_single_patient(patient_name, patient_prn)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
 
 
 
 
 
 
 
 
 
 
94
  results.append({'Name': patient_name, 'PRN': patient_prn, 'Status': status})
95
  with self.app.app_context():
96
+ self.socketio.emit('log_update', {'name': patient_name, 'prn': patient_prn, 'status': status})
97
+ self.socketio.emit('stats_update', {'processed': len(results), 'remaining': len(patient_data) - len(results), 'status': status})
 
 
 
 
 
 
98
  return results
99
 
100
+ def _process_single_patient(self, patient_name, patient_prn):
101
  try:
102
  self.micro_status(f"Navigating to Void page for '{patient_name}'")
103
  self.driver.get("https://gateway.quantumepay.com/credit-card/void")
 
105
  for attempt in range(15):
106
  try:
107
  self.micro_status(f"Searching... (Attempt {attempt + 1})")
108
+ WebDriverWait(self.driver, 2).until(EC.presence_of_element_located((By.XPATH, "//div[contains(@class, 'table-wrapper')]")))
109
+ search_box = WebDriverWait(self.driver, 2).until(EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']")))
110
+ search_box.click(); time.sleep(0.5); search_box.clear(); time.sleep(0.5)
111
+ search_box.send_keys(patient_name); search_successful = True; break
112
+ except Exception: time.sleep(1)
113
+ if not search_successful: raise Exception("Failed to search for patient.")
 
 
 
 
 
 
 
 
 
 
 
 
114
  time.sleep(3)
115
  self.micro_status("Opening transaction details...")
116
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, f"//tr[contains(., \"{patient_name}\")]//button[@data-v-b6b33fa0]"))).click()
117
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.LINK_TEXT, "Transaction Detail"))).click()
 
 
 
 
 
 
 
 
118
  self.micro_status("Adding to Vault...")
119
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Add to Vault']"))).click()
120
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//div[@class='modal-footer']//button/span[normalize-space()='Confirm']"))).click()
 
 
 
 
 
 
 
121
  try:
122
  self.micro_status("Verifying success and saving...")
123
+ company_input = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.NAME, "company_name")))
124
+ company_input.clear(); company_input.send_keys(patient_name)
125
+
126
+ contact_input = WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable((By.NAME, "company_contact")))
 
 
 
 
 
127
  self.micro_status("Clearing Contact Name field...")
128
+ contact_input.click(); contact_input.send_keys(Keys.CONTROL + "a"); contact_input.send_keys(Keys.BACK_SPACE)
 
 
 
129
  self.micro_status(f"Entering PRN: {patient_prn}...")
130
  contact_input.send_keys(str(patient_prn))
131
 
132
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Save Changes']"))).click()
133
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Confirm']]"))).click()
134
+ time.sleep(5); return 'Done'
 
 
 
 
 
135
  except TimeoutException:
136
  self.micro_status(f"'{patient_name}' is in a bad state, cancelling.")
137
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Cancel']]"))).click()
 
 
138
  return 'Bad'
 
139
  except Exception as e:
140
+ print(f"An error occurred while processing {patient_name}: {e}"); return 'Error'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
142
  def shutdown(self):
143
  try:
144
+ if self.driver: self.driver.quit()
145
+ self._kill_chrome_processes()
146
+ print("[Bot Log] Chrome session closed and cleaned up.")
147
+ except Exception as e: print(f"[Bot Log] Error during shutdown: {e}")