sonuprasad23 commited on
Commit
6538ff6
·
1 Parent(s): 9b6af7f

Fixing this

Browse files
Files changed (1) hide show
  1. server.py +340 -46
server.py CHANGED
@@ -1,10 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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,7 +25,6 @@ from datetime import datetime
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,6 +33,269 @@ 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')
@@ -84,6 +359,7 @@ class GmailApiService:
84
  print(f"[Server Log] Failed to send email: {e}")
85
  return False
86
 
 
87
  class GoogleDriveService:
88
  def __init__(self):
89
  self.creds = None
@@ -119,9 +395,11 @@ class GoogleDriveService:
119
  print(f"[Server Log] G-Drive ERROR: File upload failed: {e}")
120
  return False
121
 
 
122
  email_service = GmailApiService()
123
  drive_service = GoogleDriveService()
124
 
 
125
  def get_email_list():
126
  try:
127
  with open('config/emails.conf', 'r') as f:
@@ -129,6 +407,14 @@ def get_email_list():
129
  except FileNotFoundError:
130
  return []
131
 
 
 
 
 
 
 
 
 
132
  def run_automation_process(session_id):
133
  global bot_instance
134
  results = []
@@ -158,6 +444,7 @@ def run_automation_process(session_id):
158
  if session_id in session_data:
159
  del session_data[session_id]
160
 
 
161
  def generate_and_send_reports(session_id, results, is_crash_report=False, is_terminated=False):
162
  data = session_data.get(session_id, {})
163
  if not data:
@@ -182,7 +469,6 @@ def generate_and_send_reports(session_id, results, is_crash_report=False, is_ter
182
  full_report_name = f"{custom_name}_Full.csv"
183
  bad_report_name = f"{custom_name}_Bad.csv"
184
  skipped_report_name = f"{custom_name}_Skipped.csv"
185
-
186
  full_report_content = final_report_df.to_csv(index=False)
187
  drive_service.upload_file(full_report_name, full_report_content)
188
 
@@ -191,7 +477,6 @@ def generate_and_send_reports(session_id, results, is_crash_report=False, is_ter
191
  bad_report_name: bad_df.to_csv(index=False),
192
  skipped_report_name: skipped_df.to_csv(index=False)
193
  }
194
-
195
  status_text = "Terminated by User" if is_terminated else "Crashed" if is_crash_report else "Completed Successfully"
196
 
197
  stats = {
@@ -204,25 +489,19 @@ def generate_and_send_reports(session_id, results, is_crash_report=False, is_ter
204
  process_type_str = data.get('mode', 'Unknown').title()
205
  subject = f"{process_type_str} Automation Report [{status_text.upper()}]: {custom_name}"
206
 
207
- professional_body = email_service.create_professional_email_template(
208
- subject, status_text, stats, custom_name, process_type_str
209
- )
210
 
211
  email_service.send_report(data.get('emails'), subject, professional_body, attachments)
212
  socketio.emit('process_complete', {'message': f'Process {status_text}. Report sent.'})
213
 
 
214
  @app.route('/')
215
  def status_page():
216
  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>"""
217
  return Response(APP_STATUS_HTML)
218
 
219
- def extract_patient_name(raw_name):
220
- if not isinstance(raw_name, str):
221
- return ""
222
- name_only = raw_name.split('DOB').strip()
223
- return re.sub(r'[:\d\-\s]+$', '', name_only).strip()
224
 
225
- # Existing HTTP route (kept as-is)
226
  @app.route('/process_and_initialize', methods=['POST'])
227
  def handle_file_processing_and_init():
228
  session_id = 'user_session'
@@ -265,7 +544,19 @@ def handle_file_processing_and_init():
265
  print(f"[Server Log] ERROR during file processing: {e}")
266
  return jsonify({"error": str(e)}), 500
267
 
268
- # NEW: Socket.IO handler to mirror the HTTP route for the base64 payload from the frontend
 
 
 
 
 
 
 
 
 
 
 
 
269
  @socketio.on('initialize_and_process_files')
270
  def initialize_and_process_files(payload):
271
  session_id = 'user_session'
@@ -277,57 +568,57 @@ def initialize_and_process_files(payload):
277
  'start_date': payload.get('start_date'),
278
  'end_date': payload.get('end_date')
279
  }
 
280
 
281
- # Decode base64 content sent by the frontend
282
- app_bytes = base64.b64decode(payload['app_data_content'])
283
- quantum_bytes = base64.b64decode(payload['quantum_data_content'])
 
 
 
 
 
284
 
285
- # Read into DataFrames based on file extensions
286
- if payload['app_data_filename'].lower().endswith('.xlsx'):
287
- df_app = pd.read_excel(io.BytesIO(app_bytes))
288
- else:
289
- df_app = pd.read_csv(io.BytesIO(app_bytes))
290
 
291
- if payload['quantum_data_filename'].lower().endswith('.xlsx'):
292
- df_quantum = pd.read_excel(io.BytesIO(quantum_bytes))
293
- else:
294
- df_quantum = pd.read_csv(io.BytesIO(quantum_bytes))
295
 
296
- # Validations (same as HTTP route)
297
  if 'Patient Name' not in df_app.columns or 'PRN' not in df_app.columns:
298
- raise ValueError("App Data must contain 'Patient Name' and 'PRN' columns.")
 
299
  if 'Name' not in df_quantum.columns:
300
- raise ValueError("Quantum Data must contain a 'Name' column.")
 
301
 
 
302
  df_app_filtered = df_app.dropna(subset=['PRN'])
303
  df_app_filtered = df_app_filtered[df_app_filtered['PRN'].astype(str).str.strip() != '']
304
- prn_lookup_dict = {extract_patient_name(row['Patient Name']): row['PRN'] for _, row in df_app_filtered.iterrows()}
 
 
 
305
 
 
306
  df_quantum['PRN'] = df_quantum['Name'].apply(lambda name: prn_lookup_dict.get(name, ""))
307
 
 
308
  master_df = df_quantum.copy()
309
  master_df['Status'] = ''
310
 
311
  data['patient_data_for_report'] = master_df
312
  data['patient_data'] = master_df.to_dict('records')
313
 
314
- session_data[session_id] = data
315
-
316
- # Trigger the frontend to open the login modal
317
  socketio.emit('data_processed')
318
  except Exception as e:
319
- print(f"[Server Log] ERROR during file processing (socket): {e}")
320
- socketio.emit('error', {'message': str(e)})
321
-
322
- @socketio.on('connect')
323
- def handle_connect():
324
- print('Frontend connected.')
325
- emit('email_list', {'emails': get_email_list()})
326
 
327
- # Optional: respond to explicit client ask
328
- @socketio.on('get_email_list')
329
- def handle_get_email_list():
330
- emit('email_list', {'emails': get_email_list()})
331
 
332
  @socketio.on('start_login')
333
  def handle_login(credentials):
@@ -345,6 +636,7 @@ def handle_login(credentials):
345
  else:
346
  emit('error', {'message': f'Failed to initialize bot: {error_message}'})
347
 
 
348
  @socketio.on('submit_otp')
349
  def handle_otp(data):
350
  if not bot_instance:
@@ -357,12 +649,14 @@ def handle_otp(data):
357
  else:
358
  emit('error', {'message': f'OTP failed: {error_message}'})
359
 
 
360
  @socketio.on('terminate_process')
361
  def handle_terminate():
362
  if bot_instance:
363
  print("Termination signal received.")
364
  bot_instance.stop()
365
 
 
366
  if __name__ == '__main__':
367
  print("====================================================================")
368
  print(" 🤗 Hillside Automation - Definitive Multi-Workflow Platform")
 
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
  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
 
34
  load_dotenv()
35
 
36
+
37
+ class QuantumBot:
38
+ def __init__(self, socketio, app):
39
+ self.socketio = socketio
40
+ self.app = app
41
+ self.driver = None
42
+ self.DEFAULT_TIMEOUT = 30
43
+ self.termination_event = threading.Event()
44
+
45
+ def initialize_driver(self):
46
+ try:
47
+ self.micro_status("Initializing headless browser with stable configuration...")
48
+ options = ChromeOptions()
49
+ options.binary_location = "/usr/bin/chromium"
50
+
51
+ # Minimal, stable args
52
+ options.add_argument("--headless=new")
53
+ options.add_argument("--no-sandbox")
54
+ options.add_argument("--disable-dev-shm-usage")
55
+ options.add_argument("--window-size=1920,1080")
56
+
57
+ service = ChromeService(executable_path="/usr/bin/chromedriver")
58
+ self.driver = webdriver.Chrome(service=service, options=options)
59
+ self.micro_status("Browser initialized successfully.")
60
+ return True, None
61
+ except Exception as e:
62
+ error_message = f"Message: {str(e)}"
63
+ print(f"CRITICAL ERROR in WebDriver Initialization: {error_message}")
64
+ return False, error_message
65
+
66
+ def micro_status(self, message):
67
+ print(f"[Bot Log] {message}")
68
+ with self.app.app_context():
69
+ self.socketio.emit('micro_status_update', {'message': message})
70
+
71
+ def stop(self):
72
+ self.micro_status("Termination signal received...")
73
+ self.termination_event.set()
74
+
75
+ def login(self, username, password):
76
+ try:
77
+ self.micro_status("Navigating to login page...")
78
+ self.driver.get("https://gateway.quantumepay.com/account/login")
79
+ time.sleep(2)
80
+ self.micro_status("Entering credentials...")
81
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "Username"))).send_keys(username)
82
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "Password"))).send_keys(password)
83
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "login"))).click()
84
+ self.micro_status("Waiting for OTP screen...")
85
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "code1")))
86
+ return True, None
87
+ except Exception as e:
88
+ error_message = f"Error during login: {str(e)}"
89
+ print(f"[Bot Log] ERROR during login: {error_message}")
90
+ return False, error_message
91
+
92
+ def submit_otp(self, otp):
93
+ try:
94
+ self.micro_status(f"Submitting OTP...")
95
+ otp_digits = list(otp)
96
+ for i in range(6):
97
+ self.driver.find_element(By.ID, f"code{i+1}").send_keys(otp_digits[i])
98
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "login"))).click()
99
+ self.micro_status("Verifying login success...")
100
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//span[text()='Payments']")))
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(patient_data, kwargs.get('start_date'), kwargs.get('end_date'))
112
+ else:
113
+ return []
114
+
115
+ def process_void_list(self, patient_data):
116
+ results = []
117
+ for index, record in enumerate(patient_data):
118
+ if self.termination_event.is_set():
119
+ print("[Bot Log] Termination detected.")
120
+ break
121
+ patient_name = record['Name']
122
+ patient_prn = record.get('PRN', '')
123
+ status = 'Skipped - No PRN'
124
+ if patient_prn and str(patient_prn).strip():
125
+ self.micro_status(f"Processing VOID for '{patient_name}' ({index + 1}/{len(patient_data)})...")
126
+ status = self._process_single_void(patient_name, patient_prn)
127
+ else:
128
+ self.micro_status(f"Skipping '{patient_name}' (No PRN).")
129
+ time.sleep(0.5)
130
+
131
+ results.append({'Name': patient_name, 'PRN': patient_prn, 'Status': status})
132
+ with self.app.app_context():
133
+ self.socketio.emit('log_update', {'name': patient_name, 'prn': patient_prn, 'status': status})
134
+ self.socketio.emit('stats_update', {'processed': index + 1, 'remaining': len(patient_data) - (index + 1), 'status': status})
135
+ return results
136
+
137
+ def process_refund_list(self, patient_data, start_date_str, end_date_str):
138
+ results = []
139
+ try:
140
+ self.micro_status("Navigating to Refund page to set date range...")
141
+ self.driver.get("https://gateway.quantumepay.com/credit-card/refund")
142
+ if not self._set_date_range_on_page(start_date_str, end_date_str):
143
+ raise Exception("Failed to set date range.")
144
+ except Exception as e:
145
+ self.micro_status(f"Critical error setting date range: {e}. Aborting refund process.")
146
+ for record in patient_data:
147
+ results.append({'Name': record['Name'], 'PRN': record.get('PRN', ''), 'Status': 'Error - Date Setup Failed'})
148
+ return results
149
+
150
+ for index, record in enumerate(patient_data):
151
+ if self.termination_event.is_set():
152
+ print("[Bot Log] Termination detected.")
153
+ break
154
+ patient_name = record['Name']
155
+ patient_prn = record.get('PRN', '')
156
+ self.micro_status(f"Processing REFUND for '{patient_name}' ({index + 1}/{len(patient_data)})...")
157
+ status = self._process_single_refund(patient_name, patient_prn)
158
+ results.append({'Name': patient_name, 'PRN': patient_prn, 'Status': status})
159
+ with self.app.app_context():
160
+ self.socketio.emit('log_update', {'name': patient_name, 'prn': patient_prn, 'status': status})
161
+ self.socketio.emit('stats_update', {'processed': index + 1, 'remaining': len(patient_data) - (index + 1), 'status': status})
162
+ return results
163
+
164
+ def _process_single_void(self, patient_name, patient_prn):
165
+ try:
166
+ self.micro_status(f"Navigating to Void page for '{patient_name}'")
167
+ self.driver.get("https://gateway.quantumepay.com/credit-card/void")
168
+ search_successful = False
169
+ for attempt in range(15):
170
+ try:
171
+ self.micro_status(f"Searching... (Attempt {attempt + 1})")
172
+ WebDriverWait(self.driver, 2).until(EC.presence_of_element_located((By.XPATH, "//div[contains(@class, 'table-wrapper')]")))
173
+ search_box = WebDriverWait(self.driver, 2).until(EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']")))
174
+ search_box.click()
175
+ time.sleep(0.5)
176
+ search_box.clear()
177
+ time.sleep(0.5)
178
+ search_box.send_keys(patient_name)
179
+ search_successful = True
180
+ break
181
+ except Exception:
182
+ time.sleep(1)
183
+ if not search_successful:
184
+ raise Exception("Failed to search for patient.")
185
+ time.sleep(3)
186
+ self.micro_status("Opening transaction details...")
187
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
188
+ EC.element_to_be_clickable((By.XPATH, f"//tr[contains(., \"{patient_name}\")]//button[@data-v-b6b33fa0]"))
189
+ ).click()
190
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.LINK_TEXT, "Transaction Detail"))).click()
191
+ self.micro_status("Adding to Vault...")
192
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Add to Vault']"))).click()
193
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//div[@class='modal-footer']//button/span[normalize-space()='Confirm']"))).click()
194
+ try:
195
+ self.micro_status("Verifying success and saving...")
196
+ company_input = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.NAME, "company_name")))
197
+ company_input.clear()
198
+ company_input.send_keys(patient_name)
199
+ contact_input = WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable((By.NAME, "company_contact")))
200
+ self.micro_status("Clearing Contact Name field...")
201
+ contact_input.click()
202
+ contact_input.send_keys(Keys.CONTROL + "a")
203
+ contact_input.send_keys(Keys.BACK_SPACE)
204
+ self.micro_status(f"Entering PRN: {patient_prn}...")
205
+ contact_input.send_keys(str(patient_prn))
206
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Save Changes']"))).click()
207
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Confirm']]"))).click()
208
+ time.sleep(5)
209
+ return 'Done'
210
+ except TimeoutException:
211
+ self.micro_status(f"'{patient_name}' is in a bad state, cancelling.")
212
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Cancel']]"))).click()
213
+ return 'Bad'
214
+ except Exception as e:
215
+ print(f"An error occurred during VOID for {patient_name}: {e}")
216
+ return 'Error'
217
+
218
+ def _get_calendar_months(self):
219
+ try:
220
+ titles = self.driver.find_elements(By.XPATH, "//div[contains(@class, 'vc-title')]")
221
+ return [datetime.strptime(title.text.strip(), "%B %Y") for title in titles]
222
+ except Exception:
223
+ return []
224
+
225
+ def _select_date_in_calendar(self, target_date):
226
+ target_month_str = target_date.strftime("%B %Y")
227
+ self.micro_status(f"Navigating calendar to {target_month_str}...")
228
+ for _ in range(24):
229
+ visible_months = self._get_calendar_months()
230
+ if any(d.strftime("%B %Y") == target_month_str for d in visible_months):
231
+ self.micro_status(f"Found month. Selecting day {target_date.day}.")
232
+ day_format = "%#d" if os.name == 'nt' else "%-d"
233
+ aria_label = target_date.strftime(f"%A, %B {day_format}, %Y")
234
+ day_xpath = f"//span[@aria-label='{aria_label}']"
235
+ day_element = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.XPATH, day_xpath)))
236
+ self.driver.execute_script("arguments.click();", day_element)
237
+ return
238
+ arrow_button_xpath = (
239
+ "//div[contains(@class, 'vc-arrow') and contains(@class, 'is-left')]"
240
+ if target_date < visible_months
241
+ else "//div[contains(@class, 'vc-arrow') and contains(@class, 'is-right')]"
242
+ )
243
+ self.driver.find_element(By.XPATH, arrow_button_xpath).click()
244
+ time.sleep(0.5)
245
+ raise Exception(f"Could not navigate to date {target_date.strftime('%Y-%m-%d')}")
246
+
247
+ def _set_date_range_on_page(self, start_date_str, end_date_str):
248
+ try:
249
+ self.micro_status("Opening calendar to set date range...")
250
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[contains(text(), '-')]]"))).click()
251
+ time.sleep(1)
252
+ start_date = datetime.strptime(start_date_str, "%Y-%m-%d")
253
+ end_date = datetime.strptime(end_date_str, "%Y-%m-%d")
254
+ self._select_date_in_calendar(start_date)
255
+ time.sleep(1)
256
+ self._select_date_in_calendar(end_date)
257
+ self.micro_status("Date range set. Waiting for data to filter...")
258
+ time.sleep(5)
259
+ return True
260
+ except Exception as e:
261
+ self.micro_status(f"Error setting date range: {e}")
262
+ return False
263
+
264
+ def _process_single_refund(self, patient_name, patient_prn):
265
+ try:
266
+ search_successful = False
267
+ for attempt in range(15):
268
+ try:
269
+ self.micro_status(f"Searching for '{patient_name}'... (Attempt {attempt+1})")
270
+ search_box = WebDriverWait(self.driver, 2).until(EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']")))
271
+ search_box.click()
272
+ time.sleep(0.5)
273
+ search_box.clear()
274
+ time.sleep(0.5)
275
+ search_box.send_keys(patient_name)
276
+ search_successful = True
277
+ break
278
+ except Exception:
279
+ time.sleep(1)
280
+ if not search_successful:
281
+ raise Exception("Failed to search for patient in Refund.")
282
+ time.sleep(3)
283
+ self.micro_status(f"Performing refund action for '{patient_name}'...")
284
+ time.sleep(2)
285
+ return "Done"
286
+ except Exception as e:
287
+ print(f"An error occurred during REFUND for {patient_name}: {e}")
288
+ return 'Error'
289
+
290
+ def shutdown(self):
291
+ try:
292
+ if self.driver:
293
+ self.driver.quit()
294
+ print("[Bot Log] Chrome session closed.")
295
+ except Exception as e:
296
+ print(f"[Bot Log] Error during shutdown: {e}")
297
+
298
+
299
  app = Flask(__name__)
300
  app.config['SECRET_KEY'] = 'secret-key-for-hillside-automation'
301
  FRONTEND_ORIGIN = os.getenv('FRONTEND_URL', 'https://quantbot.netlify.app')
 
359
  print(f"[Server Log] Failed to send email: {e}")
360
  return False
361
 
362
+
363
  class GoogleDriveService:
364
  def __init__(self):
365
  self.creds = None
 
395
  print(f"[Server Log] G-Drive ERROR: File upload failed: {e}")
396
  return False
397
 
398
+
399
  email_service = GmailApiService()
400
  drive_service = GoogleDriveService()
401
 
402
+
403
  def get_email_list():
404
  try:
405
  with open('config/emails.conf', 'r') as f:
 
407
  except FileNotFoundError:
408
  return []
409
 
410
+
411
+ def extract_patient_name(raw_name):
412
+ if not isinstance(raw_name, str):
413
+ return ""
414
+ name_only = raw_name.split('DOB').strip()
415
+ return re.sub(r'[:\d\-\s]+$', '', name_only).strip()
416
+
417
+
418
  def run_automation_process(session_id):
419
  global bot_instance
420
  results = []
 
444
  if session_id in session_data:
445
  del session_data[session_id]
446
 
447
+
448
  def generate_and_send_reports(session_id, results, is_crash_report=False, is_terminated=False):
449
  data = session_data.get(session_id, {})
450
  if not data:
 
469
  full_report_name = f"{custom_name}_Full.csv"
470
  bad_report_name = f"{custom_name}_Bad.csv"
471
  skipped_report_name = f"{custom_name}_Skipped.csv"
 
472
  full_report_content = final_report_df.to_csv(index=False)
473
  drive_service.upload_file(full_report_name, full_report_content)
474
 
 
477
  bad_report_name: bad_df.to_csv(index=False),
478
  skipped_report_name: skipped_df.to_csv(index=False)
479
  }
 
480
  status_text = "Terminated by User" if is_terminated else "Crashed" if is_crash_report else "Completed Successfully"
481
 
482
  stats = {
 
489
  process_type_str = data.get('mode', 'Unknown').title()
490
  subject = f"{process_type_str} Automation Report [{status_text.upper()}]: {custom_name}"
491
 
492
+ professional_body = email_service.create_professional_email_template(subject, status_text, stats, custom_name, process_type_str)
 
 
493
 
494
  email_service.send_report(data.get('emails'), subject, professional_body, attachments)
495
  socketio.emit('process_complete', {'message': f'Process {status_text}. Report sent.'})
496
 
497
+
498
  @app.route('/')
499
  def status_page():
500
  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>"""
501
  return Response(APP_STATUS_HTML)
502
 
 
 
 
 
 
503
 
504
+ # ------------ Existing HTTP route (kept unchanged) ------------
505
  @app.route('/process_and_initialize', methods=['POST'])
506
  def handle_file_processing_and_init():
507
  session_id = 'user_session'
 
544
  print(f"[Server Log] ERROR during file processing: {e}")
545
  return jsonify({"error": str(e)}), 500
546
 
547
+
548
+ # ------------ Socket.IO Handlers ------------
549
+ @socketio.on('connect')
550
+ def handle_connect():
551
+ print('Frontend connected.')
552
+ emit('email_list', {'emails': get_email_list()})
553
+
554
+
555
+ @socketio.on('get_email_list')
556
+ def handle_get_email_list():
557
+ emit('email_list', {'emails': get_email_list()})
558
+
559
+
560
  @socketio.on('initialize_and_process_files')
561
  def initialize_and_process_files(payload):
562
  session_id = 'user_session'
 
568
  'start_date': payload.get('start_date'),
569
  'end_date': payload.get('end_date')
570
  }
571
+ session_data[session_id] = data
572
 
573
+ # Validate presence of base64 files
574
+ app_b64 = payload.get('app_data_content')
575
+ app_name = payload.get('app_data_filename', '')
576
+ quantum_b64 = payload.get('quantum_data_content')
577
+ quantum_name = payload.get('quantum_data_filename', '')
578
+ if not app_b64 or not quantum_b64:
579
+ emit('error', {'message': 'Both files are required.'})
580
+ return
581
 
582
+ # Decode base64 -> BytesIO
583
+ app_buf = io.BytesIO(base64.b64decode(app_b64))
584
+ quantum_buf = io.BytesIO(base64.b64decode(quantum_b64))
 
 
585
 
586
+ # Load frames as per file extension
587
+ df_app = pd.read_excel(app_buf) if app_name.endswith('.xlsx') else pd.read_csv(app_buf)
588
+ df_quantum = pd.read_excel(quantum_buf) if quantum_name.endswith('.xlsx') else pd.read_csv(quantum_buf)
 
589
 
590
+ # Validations
591
  if 'Patient Name' not in df_app.columns or 'PRN' not in df_app.columns:
592
+ emit('error', {'message': "App Data must contain 'Patient Name' and 'PRN' columns."})
593
+ return
594
  if 'Name' not in df_quantum.columns:
595
+ emit('error', {'message': "Quantum Data must contain a 'Name' column."})
596
+ return
597
 
598
+ # Filter and build lookup
599
  df_app_filtered = df_app.dropna(subset=['PRN'])
600
  df_app_filtered = df_app_filtered[df_app_filtered['PRN'].astype(str).str.strip() != '']
601
+ prn_lookup_dict = {
602
+ extract_patient_name(row['Patient Name']): row['PRN']
603
+ for _, row in df_app_filtered.iterrows()
604
+ }
605
 
606
+ # Attach PRN to Quantum data
607
  df_quantum['PRN'] = df_quantum['Name'].apply(lambda name: prn_lookup_dict.get(name, ""))
608
 
609
+ # Build master
610
  master_df = df_quantum.copy()
611
  master_df['Status'] = ''
612
 
613
  data['patient_data_for_report'] = master_df
614
  data['patient_data'] = master_df.to_dict('records')
615
 
616
+ # Notify frontend to open login modal
 
 
617
  socketio.emit('data_processed')
618
  except Exception as e:
619
+ print(f"[Server Log] ERROR during file processing: {e}")
620
+ emit('error', {'message': str(e)})
 
 
 
 
 
621
 
 
 
 
 
622
 
623
  @socketio.on('start_login')
624
  def handle_login(credentials):
 
636
  else:
637
  emit('error', {'message': f'Failed to initialize bot: {error_message}'})
638
 
639
+
640
  @socketio.on('submit_otp')
641
  def handle_otp(data):
642
  if not bot_instance:
 
649
  else:
650
  emit('error', {'message': f'OTP failed: {error_message}'})
651
 
652
+
653
  @socketio.on('terminate_process')
654
  def handle_terminate():
655
  if bot_instance:
656
  print("Termination signal received.")
657
  bot_instance.stop()
658
 
659
+
660
  if __name__ == '__main__':
661
  print("====================================================================")
662
  print(" 🤗 Hillside Automation - Definitive Multi-Workflow Platform")