sonuprasad23 commited on
Commit
5515ec2
·
1 Parent(s): 5078a4a

Almost Done

Browse files
Files changed (2) hide show
  1. server.py +70 -35
  2. worker.py +61 -68
server.py CHANGED
@@ -8,6 +8,7 @@ import os
8
  import base64
9
  import json
10
  import re
 
11
  from datetime import datetime
12
  from flask import Flask, Response, request, jsonify
13
  from flask_socketio import SocketIO, emit
@@ -21,6 +22,10 @@ from dotenv import load_dotenv
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,27 +35,24 @@ socketio = SocketIO(app, cors_allowed_origins=[FRONTEND_ORIGIN, "http://localhos
30
  bot_instance = None
31
  session_data = {}
32
 
33
- def log(message):
34
- print(f"[Server Log] {datetime.now().strftime('%H:%M:%S')} - {message}")
35
-
36
  class GmailApiService:
37
  def __init__(self):
38
  self.sender_email = os.getenv('EMAIL_SENDER'); self.service = None
39
  try:
40
  from google.oauth2 import service_account; from googleapiclient.discovery import build
41
  base64_creds = os.getenv('GDRIVE_SA_KEY_BASE64')
42
- if not base64_creds: log("WARNING: GDRIVE_SA_KEY_BASE64 not found."); return
43
  creds_json = base64.b64decode(base64_creds).decode('utf-8'); creds_dict = json.loads(creds_json)
44
  credentials = service_account.Credentials.from_service_account_info(creds_dict, scopes=['https://www.googleapis.com/auth/gmail.send'])
45
  if self.sender_email: credentials = credentials.with_subject(self.sender_email)
46
  self.service = build('gmail', 'v1', credentials=credentials)
47
- log("Gmail API Service initialized successfully")
48
- except Exception as e: log(f"Gmail API Error: {e}")
49
 
50
  def create_professional_email_template(self, subject, status_text, stats, custom_name):
51
  status_color = "#28a745" if "completed" in status_text else "#ffc107" if "terminated" in status_text else "#dc3545"
52
  current_date = datetime.now().strftime("%B %d, %Y at %I:%M %p")
53
- return f"""
54
  <!DOCTYPE html>
55
  <html><head><title>{subject}</title><style>
56
  body{{font-family: 'Segoe UI', sans-serif; background-color: #f8f8f8; color: #2c2c2c;}}
@@ -81,6 +83,7 @@ class GmailApiService:
81
  <li><b>{custom_name}_Skipped.csv:</b> A filtered list of patients that were skipped due to having no PRN.</li></ul></div></div>
82
  <div class="footer"><p>&copy; {datetime.now().year} Hillside Automation. This is an automated report.</p></div></div></body></html>
83
  """
 
84
 
85
  def send_report(self, recipients, subject, body, attachments=None):
86
  if not self.service or not recipients: return False
@@ -95,8 +98,9 @@ class GmailApiService:
95
  message.attach(part)
96
  raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode('utf-8')
97
  sent_message = self.service.users().messages().send(userId='me', body={'raw': raw_message}).execute()
98
- log(f"Email sent successfully! Message ID: {sent_message['id']}"); return True
99
- except Exception as e: log(f"Failed to send email: {e}"); return False
 
100
 
101
  class GoogleDriveService:
102
  def __init__(self):
@@ -108,8 +112,8 @@ class GoogleDriveService:
108
  creds_json = base64.b64decode(base64_creds).decode('utf-8'); creds_dict = json.loads(creds_json)
109
  self.creds = service_account.Credentials.from_service_account_info(creds_dict, scopes=['https://www.googleapis.com/auth/drive'])
110
  self.service = build('drive', 'v3', credentials=self.creds)
111
- log("Google Drive Service initialized.")
112
- except Exception as e: log(f"G-Drive ERROR: {e}")
113
 
114
  def upload_file(self, filename, file_content):
115
  if not self.service: return False
@@ -118,8 +122,8 @@ class GoogleDriveService:
118
  file_metadata = {'name': filename, 'parents': [self.folder_id]}
119
  media = MediaIoBaseUpload(io.BytesIO(file_content.encode('utf-8')), mimetype='text/csv', resumable=True)
120
  self.service.files().create(body=file_metadata, media_body=media, fields='id').execute()
121
- log(f"File '{filename}' uploaded to G-Drive."); return True
122
- except Exception as e: log(f"G-Drive ERROR: {e}"); return False
123
 
124
  email_service = GmailApiService()
125
  drive_service = GoogleDriveService()
@@ -133,26 +137,30 @@ def run_automation_process(session_id):
133
  global bot_instance
134
  results = []; is_terminated = False; is_crash = False
135
  try:
 
136
  data = session_data.get(session_id, {}); patient_data = data.get('patient_data'); workflow = data.get('workflow')
137
  if not patient_data: raise ValueError("No patient data prepared.")
138
  socketio.emit('initial_stats', {'total': len(patient_data)})
139
-
140
- results = bot_instance.process_patient_list(patient_data, workflow, data)
141
-
 
 
142
  is_terminated = bot_instance.termination_event.is_set()
143
  except Exception as e:
144
- log(f"Fatal error in automation thread: {e}"); is_crash = True
 
145
  socketio.emit('error', {'message': f'A fatal error occurred: {e}'})
146
  finally:
147
- socketio.emit('micro_status_update', {'message': 'Generating final reports...'})
148
  generate_and_send_reports(session_id, results, is_crash_report=is_crash, is_terminated=is_terminated)
149
  if bot_instance: bot_instance.shutdown(); bot_instance = None
150
  if session_id in session_data: del session_data[session_id]
151
 
152
  def generate_and_send_reports(session_id, results, is_crash_report=False, is_terminated=False):
153
- log("Preparing final reports...")
154
  data = session_data.get(session_id, {})
155
- if not data: log("Session data expired, cannot generate report."); return
156
 
157
  full_df = pd.DataFrame(data.get('patient_data_for_report'))
158
  if results:
@@ -179,14 +187,14 @@ def generate_and_send_reports(session_id, results, is_crash_report=False, is_ter
179
  bad_report_name: bad_df.to_csv(index=False),
180
  skipped_report_name: skipped_df.to_csv(index=False)
181
  }
182
- status_text = "terminated by user" if is_terminated else "crashed" if is_crash_report else "completed successfully"
183
  stats = {
184
  'total': len(full_df), 'processed': len(results),
185
  'successful': len(full_df[full_df['Status'] == 'Done']),
186
  'bad': len(bad_df), 'skipped': len(skipped_df)
187
  }
188
  subject = f"Automation Report [{status_text.upper()}]: {custom_name}"
189
- professional_body = email_service.create_professional_email_template(subject, status_text.capitalize(), stats, custom_name)
190
  email_service.send_report(data.get('emails'), subject, professional_body, attachments)
191
  socketio.emit('process_complete', {'message': f'Process {status_text}. Report sent.'})
192
 
@@ -200,9 +208,39 @@ def extract_patient_name(raw_name):
200
  name_only = raw_name.split('DOB')[0].strip()
201
  return re.sub(r'[:\d\-\s]+$', '', name_only).strip()
202
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  @socketio.on('connect')
204
  def handle_connect():
205
- log('Frontend connected.')
206
  emit('email_list', {'emails': get_email_list()})
207
 
208
  @socketio.on('get_email_list')
@@ -212,7 +250,6 @@ def handle_get_email_list():
212
  @socketio.on('initialize_session')
213
  def handle_init(data):
214
  session_id = 'user_session'
215
- log("Received initialize_session event.")
216
  session_data.setdefault(session_id, {})
217
  session_data[session_id].update({
218
  'emails': data.get('emails', []), 'filename': data.get('filename'),
@@ -251,17 +288,16 @@ def handle_init(data):
251
  session_data[session_id]['patient_data'] = master_df.to_dict('records')
252
 
253
  socketio.emit('data_processed')
254
- log(f"Data prepared. Total records: {len(master_df)}")
255
  except Exception as e:
256
- log(f"ERROR during file processing: {e}")
257
  emit('error', {'message': f'Error preparing data: {e}'})
258
 
259
  @socketio.on('start_login')
260
  def handle_login(credentials):
261
- log("Login process started.")
262
  global bot_instance
263
  if bot_instance: bot_instance.shutdown()
264
- bot_instance = QuantumBot(socketio, app)
265
  is_success, error_message = bot_instance.initialize_driver()
266
  if is_success:
267
  is_login_success, login_error = bot_instance.login(credentials['username'], credentials['password'])
@@ -272,7 +308,6 @@ def handle_login(credentials):
272
 
273
  @socketio.on('submit_otp')
274
  def handle_otp(data):
275
- log("OTP submission received.")
276
  if not bot_instance: return emit('error', {'message': 'Bot not initialized.'})
277
  is_success, error_message = bot_instance.submit_otp(data['otp'])
278
  if is_success:
@@ -283,12 +318,12 @@ def handle_otp(data):
283
 
284
  @socketio.on('terminate_process')
285
  def handle_terminate():
286
- if bot_instance: log("Termination signal received."); bot_instance.stop()
287
 
288
  if __name__ == '__main__':
289
- print("====================================================================")
290
- print(" 🤗 Hillside Automation - Definitive Dual-Workflow Version")
291
- print(f" Frontend URL: {FRONTEND_ORIGIN}")
292
- print(f" Port: {os.getenv('PORT', 7860)}")
293
- print("====================================================================")
294
  socketio.run(app, host='0.0.0.0', port=int(os.getenv('PORT', 7860)))
 
8
  import base64
9
  import json
10
  import re
11
+ import logging
12
  from datetime import datetime
13
  from flask import Flask, Response, request, jsonify
14
  from flask_socketio import SocketIO, emit
 
22
 
23
  load_dotenv()
24
 
25
+ # --- Definitive Logging Setup ---
26
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
27
+ logger = logging.getLogger(__name__)
28
+
29
  app = Flask(__name__)
30
  app.config['SECRET_KEY'] = 'secret-key-for-hillside-automation'
31
  FRONTEND_ORIGIN = os.getenv('FRONTEND_URL', 'https://quantbot.netlify.app')
 
35
  bot_instance = None
36
  session_data = {}
37
 
 
 
 
38
  class GmailApiService:
39
  def __init__(self):
40
  self.sender_email = os.getenv('EMAIL_SENDER'); self.service = None
41
  try:
42
  from google.oauth2 import service_account; from googleapiclient.discovery import build
43
  base64_creds = os.getenv('GDRIVE_SA_KEY_BASE64')
44
+ if not base64_creds: logger.warning("GDRIVE_SA_KEY_BASE64 not found in secrets."); return
45
  creds_json = base64.b64decode(base64_creds).decode('utf-8'); creds_dict = json.loads(creds_json)
46
  credentials = service_account.Credentials.from_service_account_info(creds_dict, scopes=['https://www.googleapis.com/auth/gmail.send'])
47
  if self.sender_email: credentials = credentials.with_subject(self.sender_email)
48
  self.service = build('gmail', 'v1', credentials=credentials)
49
+ logger.info("Gmail API Service initialized successfully")
50
+ except Exception: logger.exception("Gmail API Service initialization failed")
51
 
52
  def create_professional_email_template(self, subject, status_text, stats, custom_name):
53
  status_color = "#28a745" if "completed" in status_text else "#ffc107" if "terminated" in status_text else "#dc3545"
54
  current_date = datetime.now().strftime("%B %d, %Y at %I:%M %p")
55
+ html_template = f"""
56
  <!DOCTYPE html>
57
  <html><head><title>{subject}</title><style>
58
  body{{font-family: 'Segoe UI', sans-serif; background-color: #f8f8f8; color: #2c2c2c;}}
 
83
  <li><b>{custom_name}_Skipped.csv:</b> A filtered list of patients that were skipped due to having no PRN.</li></ul></div></div>
84
  <div class="footer"><p>&copy; {datetime.now().year} Hillside Automation. This is an automated report.</p></div></div></body></html>
85
  """
86
+ return html_template
87
 
88
  def send_report(self, recipients, subject, body, attachments=None):
89
  if not self.service or not recipients: return False
 
98
  message.attach(part)
99
  raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode('utf-8')
100
  sent_message = self.service.users().messages().send(userId='me', body={'raw': raw_message}).execute()
101
+ logger.info(f"Email sent successfully! Message ID: {sent_message['id']}")
102
+ return True
103
+ except Exception: logger.exception("Failed to send email"); return False
104
 
105
  class GoogleDriveService:
106
  def __init__(self):
 
112
  creds_json = base64.b64decode(base64_creds).decode('utf-8'); creds_dict = json.loads(creds_json)
113
  self.creds = service_account.Credentials.from_service_account_info(creds_dict, scopes=['https://www.googleapis.com/auth/drive'])
114
  self.service = build('drive', 'v3', credentials=self.creds)
115
+ logger.info("Google Drive Service initialized successfully.")
116
+ except Exception: logger.exception("G-Drive ERROR: Could not initialize service")
117
 
118
  def upload_file(self, filename, file_content):
119
  if not self.service: return False
 
122
  file_metadata = {'name': filename, 'parents': [self.folder_id]}
123
  media = MediaIoBaseUpload(io.BytesIO(file_content.encode('utf-8')), mimetype='text/csv', resumable=True)
124
  self.service.files().create(body=file_metadata, media_body=media, fields='id').execute()
125
+ logger.info(f"File '{filename}' uploaded to Google Drive."); return True
126
+ except Exception: logger.exception("G-Drive ERROR: File upload failed"); return False
127
 
128
  email_service = GmailApiService()
129
  drive_service = GoogleDriveService()
 
137
  global bot_instance
138
  results = []; is_terminated = False; is_crash = False
139
  try:
140
+ logger.info(f"Starting automation thread for session: {session_id}")
141
  data = session_data.get(session_id, {}); patient_data = data.get('patient_data'); workflow = data.get('workflow')
142
  if not patient_data: raise ValueError("No patient data prepared.")
143
  socketio.emit('initial_stats', {'total': len(patient_data)})
144
+ if workflow == 'void':
145
+ results = bot_instance.process_patient_list(patient_data, 'void')
146
+ elif workflow == 'refund':
147
+ date_range = {'start_date': data.get('start_date'), 'end_date': data.get('end_date')}
148
+ results = bot_instance.process_patient_list(patient_data, 'refund', date_range)
149
  is_terminated = bot_instance.termination_event.is_set()
150
  except Exception as e:
151
+ logger.exception("Fatal error in automation thread")
152
+ is_crash = True
153
  socketio.emit('error', {'message': f'A fatal error occurred: {e}'})
154
  finally:
155
+ logger.info("Automation thread finished. Generating final reports...")
156
  generate_and_send_reports(session_id, results, is_crash_report=is_crash, is_terminated=is_terminated)
157
  if bot_instance: bot_instance.shutdown(); bot_instance = None
158
  if session_id in session_data: del session_data[session_id]
159
 
160
  def generate_and_send_reports(session_id, results, is_crash_report=False, is_terminated=False):
161
+ logger.info(f"Preparing final reports for session {session_id}. Terminated: {is_terminated}, Crashed: {is_crash}")
162
  data = session_data.get(session_id, {})
163
+ if not data: logger.error("Session data not found, cannot generate report."); return
164
 
165
  full_df = pd.DataFrame(data.get('patient_data_for_report'))
166
  if results:
 
187
  bad_report_name: bad_df.to_csv(index=False),
188
  skipped_report_name: skipped_df.to_csv(index=False)
189
  }
190
+ status_text = "terminated by user" if is_terminated else "crashed due to an error" if is_crash_report else "completed successfully"
191
  stats = {
192
  'total': len(full_df), 'processed': len(results),
193
  'successful': len(full_df[full_df['Status'] == 'Done']),
194
  'bad': len(bad_df), 'skipped': len(skipped_df)
195
  }
196
  subject = f"Automation Report [{status_text.upper()}]: {custom_name}"
197
+ professional_body = email_service.create_professional_email_template(subject, status_text, stats, custom_name)
198
  email_service.send_report(data.get('emails'), subject, professional_body, attachments)
199
  socketio.emit('process_complete', {'message': f'Process {status_text}. Report sent.'})
200
 
 
208
  name_only = raw_name.split('DOB')[0].strip()
209
  return re.sub(r'[:\d\-\s]+$', '', name_only).strip()
210
 
211
+ @app.route('/process_files_for_automation', methods=['POST'])
212
+ def handle_file_processing():
213
+ session_id = 'user_session'
214
+ try:
215
+ if 'app_data' not in request.files or 'quantum_data' not in request.files:
216
+ return jsonify({"error": "Both files are required."}), 400
217
+
218
+ 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'])
219
+ 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'])
220
+
221
+ 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.")
222
+ if 'Name' not in df_quantum.columns: raise ValueError("Quantum Data must contain a 'Name' column.")
223
+
224
+ df_app_filtered = df_app.dropna(subset=['PRN']); df_app_filtered = df_app_filtered[df_app_filtered['PRN'].astype(str).str.strip() != '']
225
+ prn_lookup_dict = {extract_patient_name(row['Patient Name']): row['PRN'] for _, row in df_app_filtered.iterrows()}
226
+
227
+ master_df = df_quantum.copy()
228
+ master_df['PRN'] = master_df['Name'].apply(lambda name: prn_lookup_dict.get(name, ""))
229
+ master_df['Status'] = ''
230
+
231
+ session_data[session_id]['patient_data_for_report'] = master_df
232
+ session_data[session_id]['patient_data'] = master_df.to_dict('records')
233
+
234
+ socketio.emit('data_processed')
235
+ logger.info(f"Data prepared for session {session_id}. Total records: {len(master_df)}")
236
+ return jsonify({"message": "Data processed successfully."})
237
+ except Exception as e:
238
+ logger.exception("Error during file processing")
239
+ return jsonify({"error": str(e)}), 500
240
+
241
  @socketio.on('connect')
242
  def handle_connect():
243
+ logger.info('Frontend connected.')
244
  emit('email_list', {'emails': get_email_list()})
245
 
246
  @socketio.on('get_email_list')
 
250
  @socketio.on('initialize_session')
251
  def handle_init(data):
252
  session_id = 'user_session'
 
253
  session_data.setdefault(session_id, {})
254
  session_data[session_id].update({
255
  'emails': data.get('emails', []), 'filename': data.get('filename'),
 
288
  session_data[session_id]['patient_data'] = master_df.to_dict('records')
289
 
290
  socketio.emit('data_processed')
291
+ logger.info(f"Data prepared for session {session_id}. Total records: {len(master_df)}")
292
  except Exception as e:
293
+ logger.exception("Error preparing data")
294
  emit('error', {'message': f'Error preparing data: {e}'})
295
 
296
  @socketio.on('start_login')
297
  def handle_login(credentials):
 
298
  global bot_instance
299
  if bot_instance: bot_instance.shutdown()
300
+ bot_instance = QuantumBot(socketio, app, logger)
301
  is_success, error_message = bot_instance.initialize_driver()
302
  if is_success:
303
  is_login_success, login_error = bot_instance.login(credentials['username'], credentials['password'])
 
308
 
309
  @socketio.on('submit_otp')
310
  def handle_otp(data):
 
311
  if not bot_instance: return emit('error', {'message': 'Bot not initialized.'})
312
  is_success, error_message = bot_instance.submit_otp(data['otp'])
313
  if is_success:
 
318
 
319
  @socketio.on('terminate_process')
320
  def handle_terminate():
321
+ if bot_instance: logger.info("Termination signal received from frontend."); bot_instance.stop()
322
 
323
  if __name__ == '__main__':
324
+ logger.info("====================================================================")
325
+ logger.info(" 🤗 Hillside Automation - Definitive Dual-Workflow Version")
326
+ logger.info(f" Frontend URL: {FRONTEND_ORIGIN}")
327
+ logger.info(f" Port: {os.getenv('PORT', 7860)}")
328
+ logger.info("====================================================================")
329
  socketio.run(app, host='0.0.0.0', port=int(os.getenv('PORT', 7860)))
worker.py CHANGED
@@ -14,26 +14,19 @@ from selenium.webdriver.support import expected_conditions as EC
14
  from selenium.common.exceptions import TimeoutException
15
 
16
  class QuantumBot:
17
- def __init__(self, socketio, app):
18
  self.socketio = socketio; self.app = app; self.driver = None
19
  self.DEFAULT_TIMEOUT = 30; self.termination_event = threading.Event()
20
-
21
- def _log(self, message):
22
- print(f"[Bot Log] {datetime.now().strftime('%H:%M:%S')} - {message}")
23
-
24
- def micro_status(self, message):
25
- self._log(message)
26
- with self.app.app_context():
27
- self.socketio.emit('micro_status_update', {'message': message})
28
 
29
  def _kill_chrome_processes(self):
30
  try:
31
- self._log("Attempting to kill stale chromium processes...")
32
  subprocess.run(['pkill', '-f', 'chromium'], check=True, timeout=5)
33
  time.sleep(1)
34
- self._log("...Stale processes cleared.")
35
  except Exception as e:
36
- self._log(f"Info: Could not kill chrome processes (this is normal if none were running): {e}")
37
 
38
  def initialize_driver(self):
39
  try:
@@ -47,11 +40,19 @@ class QuantumBot:
47
  options.add_argument(f"--user-agent={user_agent}")
48
  service = ChromeService(executable_path="/usr/bin/chromedriver")
49
  self.driver = webdriver.Chrome(service=service, options=options)
50
- self.driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {'source': "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"})
 
 
 
51
  return True, None
52
- except Exception as e:
53
- error_message = f"Message: {str(e)}"; self._log(f"CRITICAL ERROR in WebDriver Initialization: {error_message}")
54
- return False, error_message
 
 
 
 
 
55
 
56
  def stop(self):
57
  self.micro_status("Termination signal received..."); self.termination_event.set()
@@ -65,11 +66,11 @@ class QuantumBot:
65
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "login"))).click()
66
  self.micro_status("Waiting for OTP screen...")
67
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "code1")))
68
- self._log("...Login step successful, OTP screen detected.")
69
  return True, None
70
- except Exception as e:
71
- error_message = f"Error during login: {str(e)}"; self._log(f"ERROR during login: {error_message}")
72
- return False, error_message
73
 
74
  def submit_otp(self, otp):
75
  try:
@@ -78,18 +79,19 @@ class QuantumBot:
78
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "login"))).click()
79
  self.micro_status("Verifying login success...")
80
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//span[text()='Payments']")))
81
- self._log("...OTP submission and login verification successful.")
82
  return True, None
83
- except Exception as e:
84
- error_message = f"Error during OTP submission: {str(e)}"; self._log(f"ERROR during OTP submission: {error_message}")
85
- return False, error_message
86
-
87
  def _get_calendar_months(self):
88
  try:
89
  titles = self.driver.find_elements(By.XPATH, "//div[contains(@class, 'vc-title')]")
90
  if not titles: return []
91
  return [datetime.strptime(title.text, "%B %Y") for title in titles]
92
- except Exception: return []
 
93
 
94
  def _select_date_in_calendar(self, target_date):
95
  target_month_str = target_date.strftime("%B %Y")
@@ -98,8 +100,7 @@ class QuantumBot:
98
  visible_months = self._get_calendar_months()
99
  if not visible_months: raise Exception("Calendar months not visible.")
100
  if any(d.strftime("%B %Y") == target_month_str for d in visible_months):
101
- self._log(f"...Found month. Selecting day {target_date.day}.")
102
- day_format = "%-d"
103
  day_xpath = f"//span[@aria-label='{target_date.strftime(f'%A, %B {day_format}, %Y')}']"
104
  day_element = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.XPATH, day_xpath)))
105
  self.driver.execute_script("arguments[0].click();", day_element); return
@@ -114,28 +115,21 @@ class QuantumBot:
114
  self._select_date_in_calendar(start_date); time.sleep(1)
115
  self._select_date_in_calendar(end_date); time.sleep(1)
116
  self.driver.find_element(By.TAG_NAME, "body").click(); time.sleep(3)
117
- self._log("...Date range set successfully.")
118
 
119
  def process_patient_list(self, patient_data, workflow, date_range=None):
 
120
  results = []
121
- records_to_process = list(patient_data)
122
  if workflow == 'refund':
123
- try:
124
- self._log(f"Setting up Refund workflow for date range {date_range['start_date']} to {date_range['end_date']}")
125
- self.driver.get("https://gateway.quantumepay.com/credit-card/refund")
126
- self._set_date_range(date_range['start_date'], date_range['end_date'])
127
- except Exception as e:
128
- self._log(f"FATAL ERROR during Refund setup: {e}. Aborting process.")
129
- return results # Return empty results if setup fails
130
-
131
- for index, record in enumerate(records_to_process):
132
- if self.termination_event.is_set(): self._log("Termination detected."); break
133
  patient_name = record['Name']; patient_prn = record.get('PRN', '')
134
  if not patient_prn or not str(patient_prn).strip():
135
- status = 'Skipped - No PRN'
136
- self.micro_status(f"Skipping '{patient_name}' (No PRN)."); time.sleep(0.1)
137
  else:
138
- self.micro_status(f"Processing '{patient_name}' ({index + 1}/{len(records_to_process)})...")
139
  if workflow == 'void':
140
  status = self._process_single_void(patient_name, patient_prn)
141
  elif workflow == 'refund':
@@ -143,59 +137,58 @@ class QuantumBot:
143
  results.append({'Name': patient_name, 'PRN': patient_prn, 'Status': status})
144
  with self.app.app_context():
145
  self.socketio.emit('log_update', {'name': patient_name, 'prn': patient_prn, 'status': status})
146
- self.socketio.emit('stats_update', {'processed': len(results), 'remaining': len(records_to_process) - len(results), 'status': status})
 
147
  return results
148
 
149
  def _perform_core_patient_processing(self, patient_name, patient_prn):
150
  try:
151
- self.micro_status(f"Searching for '{patient_name}'..."); search_box = WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']")))
152
- search_box.click(); time.sleep(0.5); search_box.clear(); time.sleep(0.5); search_box.send_keys(patient_name); time.sleep(3)
153
- self._log("...Search complete.")
154
-
155
- self.micro_status("Opening transaction details..."); WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, f"//tr[contains(., \"{patient_name}\")]//button[@data-v-b6b33fa0]"))).click()
156
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.LINK_TEXT, "Transaction Detail"))).click(); self._log("...Details opened.")
157
-
158
- self.micro_status("Adding to Vault..."); WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Add to Vault']"))).click()
159
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//div[@class='modal-footer']//button/span[normalize-space()='Confirm']"))).click(); self._log("...Vault confirmed.")
160
-
161
  try:
162
- self.micro_status("Verifying success and entering data...")
163
  company_input = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.NAME, "company_name")))
164
- company_input.click(); company_input.send_keys(Keys.CONTROL + "a"); company_input.send_keys(Keys.BACK_SPACE); company_input.send_keys(patient_name)
165
- self._log("...Company Name entered.")
166
-
167
  contact_input = WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable((By.NAME, "company_contact")))
168
  contact_input.click(); contact_input.send_keys(Keys.CONTROL + "a"); contact_input.send_keys(Keys.BACK_SPACE)
169
  contact_input.send_keys(str(patient_prn))
170
- self._log("...Contact Name (PRN) entered.")
171
-
172
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Save Changes']"))).click()
173
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Confirm']]"))).click()
174
- time.sleep(5); self._log("...Save complete."); return 'Done'
175
  except TimeoutException:
176
  self.micro_status(f"'{patient_name}' is in a bad state, cancelling.")
177
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Cancel']]"))).click()
178
  return 'Bad'
179
  except Exception as e:
180
- self._log(f"ERROR: A step failed for patient '{patient_name}'. Reason: {e}"); return 'Error'
 
181
 
182
  def _process_single_void(self, patient_name, patient_prn):
183
  try:
184
- self.micro_status(f"Navigating to Void page for '{patient_name}'")
185
- self.driver.get("https://gateway.quantumepay.com/credit-card/void")
186
  return self._perform_core_patient_processing(patient_name, patient_prn)
187
  except Exception as e:
188
- self._log(f"ERROR in Void workflow for {patient_name}: {e}"); return 'Error'
 
189
 
190
  def _process_single_refund(self, patient_name, patient_prn):
191
  try:
192
  return self._perform_core_patient_processing(patient_name, patient_prn)
193
  except Exception as e:
194
- self._log(f"ERROR in Refund workflow for {patient_name}: {e}"); return 'Error'
 
195
 
196
  def shutdown(self):
197
  try:
198
  if self.driver: self.driver.quit()
199
  self._kill_chrome_processes()
200
- self._log("Chrome session closed and cleaned up.")
201
- except Exception as e: self._log(f"Error during shutdown: {e}")
 
14
  from selenium.common.exceptions import TimeoutException
15
 
16
  class QuantumBot:
17
+ def __init__(self, socketio, app, logger):
18
  self.socketio = socketio; self.app = app; self.driver = None
19
  self.DEFAULT_TIMEOUT = 30; self.termination_event = threading.Event()
20
+ self.logger = logger
 
 
 
 
 
 
 
21
 
22
  def _kill_chrome_processes(self):
23
  try:
24
+ self.logger.info("Attempting to kill any lingering chromium processes...")
25
  subprocess.run(['pkill', '-f', 'chromium'], check=True, timeout=5)
26
  time.sleep(1)
27
+ self.logger.info("Lingering processes terminated.")
28
  except Exception as e:
29
+ self.logger.warning(f"Could not kill chrome processes (this is often normal): {e}")
30
 
31
  def initialize_driver(self):
32
  try:
 
40
  options.add_argument(f"--user-agent={user_agent}")
41
  service = ChromeService(executable_path="/usr/bin/chromedriver")
42
  self.driver = webdriver.Chrome(service=service, options=options)
43
+ self.driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
44
+ 'source': "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
45
+ })
46
+ self.logger.info("WebDriver initialized successfully.")
47
  return True, None
48
+ except Exception:
49
+ self.logger.exception("CRITICAL ERROR in WebDriver Initialization")
50
+ return False, "WebDriver initialization failed. See backend logs for details."
51
+
52
+ def micro_status(self, message):
53
+ self.logger.info(message)
54
+ with self.app.app_context():
55
+ self.socketio.emit('micro_status_update', {'message': message})
56
 
57
  def stop(self):
58
  self.micro_status("Termination signal received..."); self.termination_event.set()
 
66
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "login"))).click()
67
  self.micro_status("Waiting for OTP screen...")
68
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "code1")))
69
+ self.logger.info("Login successful, OTP screen is visible.")
70
  return True, None
71
+ except Exception:
72
+ self.logger.exception("ERROR during login process")
73
+ return False, "An error occurred during login. See backend logs."
74
 
75
  def submit_otp(self, otp):
76
  try:
 
79
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "login"))).click()
80
  self.micro_status("Verifying login success...")
81
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//span[text()='Payments']")))
82
+ self.logger.info("OTP submission successful, dashboard is visible.")
83
  return True, None
84
+ except Exception:
85
+ self.logger.exception("ERROR during OTP submission")
86
+ return False, "OTP submission failed. See backend logs."
87
+
88
  def _get_calendar_months(self):
89
  try:
90
  titles = self.driver.find_elements(By.XPATH, "//div[contains(@class, 'vc-title')]")
91
  if not titles: return []
92
  return [datetime.strptime(title.text, "%B %Y") for title in titles]
93
+ except Exception as e:
94
+ self.logger.warning(f"Could not get calendar months: {e}"); return []
95
 
96
  def _select_date_in_calendar(self, target_date):
97
  target_month_str = target_date.strftime("%B %Y")
 
100
  visible_months = self._get_calendar_months()
101
  if not visible_months: raise Exception("Calendar months not visible.")
102
  if any(d.strftime("%B %Y") == target_month_str for d in visible_months):
103
+ day_format = "%#d" if os.name == 'nt' else "%-d"
 
104
  day_xpath = f"//span[@aria-label='{target_date.strftime(f'%A, %B {day_format}, %Y')}']"
105
  day_element = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.XPATH, day_xpath)))
106
  self.driver.execute_script("arguments[0].click();", day_element); return
 
115
  self._select_date_in_calendar(start_date); time.sleep(1)
116
  self._select_date_in_calendar(end_date); time.sleep(1)
117
  self.driver.find_element(By.TAG_NAME, "body").click(); time.sleep(3)
 
118
 
119
  def process_patient_list(self, patient_data, workflow, date_range=None):
120
+ self.logger.info(f"Starting patient processing for '{workflow}' workflow. Total patients: {len(patient_data)}")
121
  results = []
 
122
  if workflow == 'refund':
123
+ self._navigate_and_verify("https://gateway.quantumepay.com/credit-card/refund", "//button[.//span[contains(text(), '-')]]")
124
+ self._set_date_range(date_range['start_date'], date_range['end_date'])
125
+
126
+ for index, record in enumerate(patient_data):
127
+ if self.termination_event.is_set(): self.logger.info("Termination detected. Ending patient processing loop."); break
 
 
 
 
 
128
  patient_name = record['Name']; patient_prn = record.get('PRN', '')
129
  if not patient_prn or not str(patient_prn).strip():
130
+ status = 'Skipped - No PRN'; self.micro_status(f"Skipping '{patient_name}' (No PRN)."); time.sleep(0.1)
 
131
  else:
132
+ self.micro_status(f"Processing '{patient_name}' ({index + 1}/{len(patient_data)})...")
133
  if workflow == 'void':
134
  status = self._process_single_void(patient_name, patient_prn)
135
  elif workflow == 'refund':
 
137
  results.append({'Name': patient_name, 'PRN': patient_prn, 'Status': status})
138
  with self.app.app_context():
139
  self.socketio.emit('log_update', {'name': patient_name, 'prn': patient_prn, 'status': status})
140
+ self.socketio.emit('stats_update', {'processed': len(results), 'remaining': len(patient_data) - len(results), 'status': status})
141
+ self.logger.info("Finished processing patient list.")
142
  return results
143
 
144
  def _perform_core_patient_processing(self, patient_name, patient_prn):
145
  try:
146
+ self.micro_status(f"Searching for '{patient_name}'...")
147
+ search_box = WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']")))
148
+ search_box.click(); time.sleep(0.5); search_box.clear(); time.sleep(0.5)
149
+ search_box.send_keys(patient_name); time.sleep(3)
150
+ self.micro_status("Opening transaction details...")
151
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, f"//tr[contains(., \"{patient_name}\")]//button[@data-v-b6b33fa0]"))).click()
152
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.LINK_TEXT, "Transaction Detail"))).click()
153
+ self.micro_status("Adding to Vault...")
154
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Add to Vault']"))).click()
155
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//div[@class='modal-footer']//button/span[normalize-space()='Confirm']"))).click()
156
  try:
157
+ self.micro_status("Verifying success and saving data...")
158
  company_input = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.NAME, "company_name")))
159
+ company_input.clear(); company_input.send_keys(patient_name)
 
 
160
  contact_input = WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable((By.NAME, "company_contact")))
161
  contact_input.click(); contact_input.send_keys(Keys.CONTROL + "a"); contact_input.send_keys(Keys.BACK_SPACE)
162
  contact_input.send_keys(str(patient_prn))
 
 
163
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Save Changes']"))).click()
164
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Confirm']]"))).click()
165
+ time.sleep(5); return 'Done'
166
  except TimeoutException:
167
  self.micro_status(f"'{patient_name}' is in a bad state, cancelling.")
168
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Cancel']]"))).click()
169
  return 'Bad'
170
  except Exception as e:
171
+ self.logger.exception(f"An error occurred while processing {patient_name} (core)")
172
+ return 'Error'
173
 
174
  def _process_single_void(self, patient_name, patient_prn):
175
  try:
176
+ self._navigate_and_verify("https://gateway.quantumepay.com/credit-card/void", "//input[@placeholder='Search']")
 
177
  return self._perform_core_patient_processing(patient_name, patient_prn)
178
  except Exception as e:
179
+ self.logger.exception(f"Error in Void workflow for {patient_name}")
180
+ return 'Error'
181
 
182
  def _process_single_refund(self, patient_name, patient_prn):
183
  try:
184
  return self._perform_core_patient_processing(patient_name, patient_prn)
185
  except Exception as e:
186
+ self.logger.exception(f"Error in Refund workflow for {patient_name}")
187
+ return 'Error'
188
 
189
  def shutdown(self):
190
  try:
191
  if self.driver: self.driver.quit()
192
  self._kill_chrome_processes()
193
+ self.logger.info("Chrome session closed and cleaned up successfully.")
194
+ except Exception as e: self.logger.error(f"Error during shutdown: {e}")