sonuprasad23 commited on
Commit
30ba375
·
1 Parent(s): c1e4b14

Fixing this

Browse files
Files changed (2) hide show
  1. server.py +37 -48
  2. worker.py +113 -125
server.py CHANGED
@@ -124,9 +124,7 @@ def generate_and_send_reports(session_id, results, is_crash_report=False, is_ter
124
  if results:
125
  result_df = pd.DataFrame(results).set_index('Name')
126
  full_df.set_index('Name', inplace=True)
127
- full_df['Status'] = full_df['Status'].astype('object')
128
  full_df.update(result_df); full_df.reset_index(inplace=True)
129
-
130
  full_df['Status'].fillna('Not Processed', inplace=True)
131
 
132
  final_report_df = full_df[['Name', 'PRN', 'Status']]
@@ -163,43 +161,22 @@ def status_page():
163
  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>"""
164
  return Response(APP_STATUS_HTML)
165
 
166
- def read_file_content(b64_content, filename):
167
- raw_bytes = base64.b64decode(b64_content)
168
- try:
169
- return raw_bytes.decode('utf-8')
170
- except UnicodeDecodeError:
171
- print(f"[Server Log] UTF-8 decoding failed for {filename}, falling back to Latin-1.")
172
- return raw_bytes.decode('latin-1')
173
-
174
  def extract_patient_name(raw_name):
175
  if not isinstance(raw_name, str): return ""
176
  name_only = raw_name.split('DOB')[0].strip()
177
  return re.sub(r'[:\d\-\s]+$', '', name_only).strip()
178
 
179
- @socketio.on('connect')
180
- def handle_connect():
181
- print(f'Frontend connected.')
182
- emit('email_list', {'emails': get_email_list()})
183
-
184
- @socketio.on('process_and_initialize')
185
- def handle_init_and_process(data):
186
  session_id = 'user_session'
187
  try:
188
- print("[Server Log] Received initialization and file processing request.")
189
- session_data[session_id] = {
190
- 'emails': data['emails'], 'filename': data['filename'], 'mode': data.get('mode'),
191
- 'start_date': data.get('start_date'), 'end_date': data.get('end_date')
192
- }
193
-
194
- app_data_file = data.get('app_data_file')
195
- quantum_data_file = data.get('quantum_data_file')
196
 
197
- app_data_content = read_file_content(app_data_file['content'], app_data_file['name'])
198
- quantum_data_content = read_file_content(quantum_data_file['content'], quantum_data_file['name'])
199
 
200
- df_app = pd.read_csv(io.StringIO(app_data_content))
201
- df_quantum = pd.read_csv(io.StringIO(quantum_data_content))
202
-
203
  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.")
204
  if 'Name' not in df_quantum.columns: raise ValueError("Quantum Data must contain a 'Name' column.")
205
 
@@ -211,28 +188,40 @@ def handle_init_and_process(data):
211
  master_df = df_quantum.copy()
212
  master_df['Status'] = ''
213
 
214
- session_data[session_id]['patient_data_for_report'] = master_df
215
- session_data[session_id]['patient_data'] = master_df.to_dict('records')
 
 
 
 
 
 
216
 
217
- global bot_instance
218
- if bot_instance: bot_instance.shutdown()
219
- bot_instance = QuantumBot(socketio, app)
220
- is_success, error_message = bot_instance.initialize_driver()
221
- if is_success:
222
- emit('data_processed')
223
- else:
224
- emit('error', {'message': f'Failed to initialize bot: {error_message}'})
225
 
226
- except Exception as e:
227
- print(f"[Server Log] ERROR during initialization: {e}")
228
- emit('error', {'message': str(e)})
 
 
 
 
229
 
230
  @socketio.on('start_login')
231
  def handle_login(credentials):
232
- if not bot_instance: return emit('error', {'message': 'Bot not initialized.'})
233
- is_success, error_message = bot_instance.login(credentials['username'], credentials['password'])
234
- if is_success: emit('otp_required')
235
- else: emit('error', {'message': f'Login failed: {error_message}'})
 
 
 
 
 
 
236
 
237
  @socketio.on('submit_otp')
238
  def handle_otp(data):
@@ -250,7 +239,7 @@ def handle_terminate():
250
 
251
  if __name__ == '__main__':
252
  print("====================================================================")
253
- print(" 🤗 Hillside Automation - Definitive Final Version")
254
  print(f" Frontend URL: {FRONTEND_ORIGIN}")
255
  print(f" Port: {os.getenv('PORT', 7860)}")
256
  print("====================================================================")
 
124
  if results:
125
  result_df = pd.DataFrame(results).set_index('Name')
126
  full_df.set_index('Name', inplace=True)
 
127
  full_df.update(result_df); full_df.reset_index(inplace=True)
 
128
  full_df['Status'].fillna('Not Processed', inplace=True)
129
 
130
  final_report_df = full_df[['Name', 'PRN', 'Status']]
 
161
  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>"""
162
  return Response(APP_STATUS_HTML)
163
 
 
 
 
 
 
 
 
 
164
  def extract_patient_name(raw_name):
165
  if not isinstance(raw_name, str): return ""
166
  name_only = raw_name.split('DOB')[0].strip()
167
  return re.sub(r'[:\d\-\s]+$', '', name_only).strip()
168
 
169
+ @app.route('/process_and_initialize', methods=['POST'])
170
+ def handle_file_processing_and_init():
 
 
 
 
 
171
  session_id = 'user_session'
172
  try:
173
+ data = session_data.get(session_id, {})
174
+ if 'app_data' not in request.files or 'quantum_data' not in request.files:
175
+ return jsonify({"error": "Both files are required."}), 400
 
 
 
 
 
176
 
177
+ 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'])
178
+ 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'])
179
 
 
 
 
180
  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.")
181
  if 'Name' not in df_quantum.columns: raise ValueError("Quantum Data must contain a 'Name' column.")
182
 
 
188
  master_df = df_quantum.copy()
189
  master_df['Status'] = ''
190
 
191
+ data['patient_data_for_report'] = master_df
192
+ data['patient_data'] = master_df.to_dict('records')
193
+
194
+ socketio.emit('data_processed')
195
+ return jsonify({"message": "Data processed successfully."})
196
+ except Exception as e:
197
+ print(f"[Server Log] ERROR during file processing: {e}")
198
+ return jsonify({"error": str(e)}), 500
199
 
200
+ @socketio.on('connect')
201
+ def handle_connect():
202
+ print(f'Frontend connected.')
203
+ emit('email_list', {'emails': get_email_list()})
 
 
 
 
204
 
205
+ @socketio.on('initialize_session')
206
+ def handle_init(data):
207
+ session_id = 'user_session'
208
+ session_data[session_id] = {
209
+ 'emails': data['emails'], 'filename': data['filename'], 'mode': data.get('mode'),
210
+ 'start_date': data.get('start_date'), 'end_date': data.get('end_date')
211
+ }
212
 
213
  @socketio.on('start_login')
214
  def handle_login(credentials):
215
+ global bot_instance
216
+ if bot_instance: bot_instance.shutdown()
217
+ bot_instance = QuantumBot(socketio, app)
218
+ is_success, error_message = bot_instance.initialize_driver()
219
+ if is_success:
220
+ is_login_success, login_error = bot_instance.login(credentials['username'], credentials['password'])
221
+ if is_login_success: emit('otp_required')
222
+ else: emit('error', {'message': f'Login failed: {login_error}'})
223
+ else:
224
+ emit('error', {'message': f'Failed to initialize bot: {error_message}'})
225
 
226
  @socketio.on('submit_otp')
227
  def handle_otp(data):
 
239
 
240
  if __name__ == '__main__':
241
  print("====================================================================")
242
+ print(" 🤗 Hillside Automation - Definitive Multi-Workflow Platform")
243
  print(f" Frontend URL: {FRONTEND_ORIGIN}")
244
  print(f" Port: {os.getenv('PORT', 7860)}")
245
  print("====================================================================")
worker.py CHANGED
@@ -2,6 +2,8 @@ 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,8 +12,6 @@ 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
  class QuantumBot:
17
  def __init__(self, socketio, app):
@@ -28,13 +28,21 @@ class QuantumBot:
28
  try:
29
  self.micro_status("Initializing headless browser...")
30
  self._kill_chrome_processes()
31
- options = ChromeOptions(); options.binary_location = "/usr/bin/chromium"
32
  options.add_argument("--headless=new"); options.add_argument("--no-sandbox")
33
  options.add_argument("--disable-dev-shm-usage"); options.add_argument("--disable-gpu")
34
  options.add_argument("--window-size=1920,1080")
35
  user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
36
  options.add_argument(f"--user-agent={user_agent}")
37
- service = ChromeService(executable_path="/usr/bin/chromedriver")
 
 
 
 
 
 
 
 
38
  self.driver = webdriver.Chrome(service=service, options=options)
39
  self.driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
40
  'source': "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
@@ -55,8 +63,7 @@ class QuantumBot:
55
  def login(self, username, password):
56
  try:
57
  self.micro_status("Navigating to login page..."); self.driver.get("https://gateway.quantumepay.com/")
58
- self._wait_for_page_load()
59
- self.micro_status("Entering credentials...")
60
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "Username"))).send_keys(username)
61
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "Password"))).send_keys(password)
62
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "login"))).click()
@@ -72,47 +79,12 @@ class QuantumBot:
72
  self.micro_status(f"Submitting OTP..."); otp_digits = list(otp)
73
  for i in range(6): self.driver.find_element(By.ID, f"code{i+1}").send_keys(otp_digits[i])
74
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "login"))).click()
75
- self._wait_for_page_load()
76
  self.micro_status("Verifying login success...")
77
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//span[text()='Payments']")))
78
  return True, None
79
  except Exception as e:
80
  error_message = f"Error during OTP submission: {str(e)}"; print(f"[Bot Log] ERROR during OTP submission: {error_message}")
81
  return False, error_message
82
-
83
- def _wait_for_page_load(self, timeout=None):
84
- if timeout is None: timeout = self.DEFAULT_TIMEOUT
85
- self.micro_status("Waiting for page activity to cease...")
86
- loading_overlay_xpath = "//div[contains(@class, 'vld-background')]"
87
- try:
88
- WebDriverWait(self.driver, timeout).until(EC.invisibility_of_element_located((By.XPATH, loading_overlay_xpath)))
89
- self.micro_status("...Page is ready.")
90
- except TimeoutException:
91
- self.micro_status("Warning: Loading overlay did not disappear in time.")
92
-
93
- def _get_calendar_months(self):
94
- try:
95
- titles = self.driver.find_elements(By.XPATH, "//div[contains(@class, 'vc-title')]")
96
- return [datetime.strptime(title.text, "%B %Y") for title in titles]
97
- except Exception: return []
98
-
99
- def _select_date_in_calendar(self, target_date):
100
- target_month_str = target_date.strftime("%B %Y")
101
- self.micro_status(f"Navigating calendar to {target_month_str}...")
102
- for _ in range(24):
103
- visible_months = self._get_calendar_months()
104
- if any(d.strftime("%B %Y") == target_month_str for d in visible_months):
105
- self.micro_status(f"Found month. Selecting day {target_date.day}.")
106
- day_format = "%#d" if os.name == 'nt' else "%-d"
107
- expected_aria_label = target_date.strftime(f"%A, %B {day_format}, %Y")
108
- day_xpath = f"//span[@aria-label='{expected_aria_label}']"
109
- day_element = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located((By.XPATH, day_xpath)))
110
- self.driver.execute_script("arguments[0].click();", day_element)
111
- return
112
- arrow_direction = 'is-left' if target_date < visible_months[0] else 'is-right'
113
- self.driver.find_element(By.XPATH, f"//div[contains(@class, 'vc-arrow') and contains(@class, '{arrow_direction}')]").click()
114
- self._wait_for_page_load(timeout=5)
115
- raise Exception(f"Could not navigate to date {target_date.strftime('%Y-%m-%d')}")
116
 
117
  def start_processing(self, process_type, patient_data, **kwargs):
118
  if process_type == 'void':
@@ -122,122 +94,138 @@ class QuantumBot:
122
  return []
123
 
124
  def process_void_list(self, patient_data):
125
- results = []; current_patient_index = 0
126
- while current_patient_index < len(patient_data):
127
  if self.termination_event.is_set(): print("[Bot Log] Termination detected."); break
128
- record = patient_data[current_patient_index]; patient_name = record['Name']; patient_prn = record.get('PRN', '')
129
  status = 'Skipped - No PRN'
130
  if patient_prn and str(patient_prn).strip():
131
- self.micro_status(f"Processing VOID for '{patient_name}' ({current_patient_index + 1}/{len(patient_data)})...")
132
  status = self._process_single_void(patient_name, patient_prn)
133
  else:
134
- self.micro_status(f"Skipping '{patient_name}' (No PRN)."); time.sleep(0.2)
135
-
136
- if status != "RETRY":
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
- current_patient_index += 1
142
  return results
143
 
144
  def process_refund_list(self, patient_data, start_date_str, end_date_str):
145
- results = []; current_patient_index = 0
146
- try:
147
- self._navigate_and_verify("https://gateway.quantumepay.com/credit-card/refund", "//button[.//span[contains(text(), '-')]]")
148
- self._set_date_range_on_page(start_date_str, end_date_str)
149
- except Exception as e:
150
- self.micro_status(f"FATAL: Could not set date range. Error: {e}")
151
- return [{'Name': r['Name'], 'PRN': r.get('PRN', ''), 'Status': 'Error - Date Setup Failed'} for r in patient_data]
152
-
153
- while current_patient_index < len(patient_data):
154
  if self.termination_event.is_set(): print("[Bot Log] Termination detected."); break
155
- record = patient_data[current_patient_index]; patient_name = record['Name']; patient_prn = record.get('PRN', '')
156
- status = 'Skipped - No PRN'
157
- if patient_prn and str(patient_prn).strip():
158
- self.micro_status(f"Processing REFUND for '{patient_name}' ({current_patient_index + 1}/{len(patient_data)})...")
159
- status = self._process_single_refund(patient_name, patient_prn)
160
- else:
161
- self.micro_status(f"Skipping '{patient_name}' (No PRN)."); time.sleep(0.2)
162
-
163
- if status != "RETRY":
164
- results.append({'Name': patient_name, 'PRN': patient_prn, 'Status': status})
165
- with self.app.app_context():
166
- self.socketio.emit('log_update', {'name': patient_name, 'prn': patient_prn, 'status': status})
167
- self.socketio.emit('stats_update', {'processed': len(results), 'remaining': len(patient_data) - len(results), 'status': status})
168
- current_patient_index += 1
169
  return results
170
 
171
  def _process_single_void(self, patient_name, patient_prn):
172
  try:
173
- self._navigate_and_verify("https://gateway.quantumepay.com/credit-card/void", "//input[@placeholder='Search']")
174
- self.micro_status(f"Searching for '{patient_name}'...")
175
- search_box = WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']")))
176
- time.sleep(1); search_box.click(); search_box.clear(); time.sleep(0.5); search_box.send_keys(patient_name)
177
- self._wait_for_page_load()
 
 
 
 
 
 
 
 
178
  self.micro_status("Opening transaction details...")
179
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, f"//tr[contains(., \"{patient_name}\")]//button[@data-v-b6b33fa0]"))).click(); self._wait_for_page_load(5)
180
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.LINK_TEXT, "Transaction Detail"))).click(); self._wait_for_page_load()
181
  self.micro_status("Adding to Vault...")
182
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Add to Vault']"))).click(); self._wait_for_page_load()
183
- 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._wait_for_page_load()
184
  try:
185
  self.micro_status("Verifying success and saving...")
186
  company_input = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.NAME, "company_name")))
187
  company_input.clear(); company_input.send_keys(patient_name)
188
  contact_input = WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable((By.NAME, "company_contact")))
189
  contact_input.click(); contact_input.send_keys(Keys.CONTROL + "a"); contact_input.send_keys(Keys.BACK_SPACE)
190
- self.micro_status(f"Entering PRN: {patient_prn}..."); contact_input.send_keys(str(patient_prn))
191
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Save Changes']"))).click(); self._wait_for_page_load()
192
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Confirm']]"))).click(); time.sleep(2); self._wait_for_page_load()
193
- return 'Done'
 
194
  except TimeoutException:
195
  self.micro_status(f"'{patient_name}' is in a bad state, cancelling.")
196
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Cancel']]"))).click(); self._wait_for_page_load()
197
  return 'Bad'
198
  except Exception as e:
199
- print(f"An error occurred during VOID for {patient_name}: {e}"); return 'RETRY'
200
 
201
- def _process_single_refund(self, patient_name, patient_prn):
202
  try:
203
- self.micro_status(f"Searching for '{patient_name}' in date range...")
204
- search_box = WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']")))
205
- time.sleep(1); search_box.click(); search_box.clear(); time.sleep(0.5); search_box.send_keys(patient_name)
206
- self._wait_for_page_load()
207
- self.micro_status("Opening transaction details for Refund...")
208
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, f"//tr[contains(., \"{patient_name}\")]//button"))).click(); self._wait_for_page_load(5)
209
- self.micro_status("Initiating Refund...")
210
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Refund']"))).click(); self._wait_for_page_load()
211
- try:
212
- self.micro_status("Confirming Refund...")
213
- WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Confirm']]"))).click(); time.sleep(2); self._wait_for_page_load()
214
- return "Done"
215
- except TimeoutException:
216
- self.micro_status(f"'{patient_name}' is in a bad state for refund, cancelling.")
217
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Cancel']]"))).click(); self._wait_for_page_load()
218
- return 'Bad'
219
- except Exception as e:
220
- print(f"An error occurred during REFUND for {patient_name}: {e}"); return 'RETRY'
 
 
 
 
 
221
 
222
  def _set_date_range_on_page(self, start_date_str, end_date_str):
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  try:
224
- self.micro_status("Opening calendar...")
225
- date_button = WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[contains(text(), '-')]]")))
226
- time.sleep(2)
227
- date_button.click()
228
- self._wait_for_page_load(timeout=10)
229
- start_date = datetime.strptime(start_date_str, "%Y-%m-%d"); end_date = datetime.strptime(end_date_str, "%Y-%m-%d")
230
- self._select_date_in_calendar(start_date)
231
- self._wait_for_page_load(timeout=10)
232
- self._select_date_in_calendar(end_date)
 
 
 
 
 
 
 
 
 
233
  time.sleep(2)
234
- self.driver.find_element(By.TAG_NAME, "body").click()
235
- self.micro_status("Date range set. Waiting for data to filter...")
236
- self._wait_for_page_load()
237
- return True
238
  except Exception as e:
239
- self.micro_status(f"Error setting date range: {e}")
240
- return False
241
 
242
  def shutdown(self):
243
  try:
 
2
  import threading
3
  import subprocess
4
  import pandas as pd
5
+ import os
6
+ from datetime import datetime
7
  from selenium import webdriver
8
  from selenium.webdriver.chrome.service import Service as ChromeService
9
  from selenium.webdriver.chrome.options import Options as ChromeOptions
 
12
  from selenium.webdriver.support.ui import WebDriverWait
13
  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):
 
28
  try:
29
  self.micro_status("Initializing headless browser...")
30
  self._kill_chrome_processes()
31
+ options = ChromeOptions()
32
  options.add_argument("--headless=new"); options.add_argument("--no-sandbox")
33
  options.add_argument("--disable-dev-shm-usage"); options.add_argument("--disable-gpu")
34
  options.add_argument("--window-size=1920,1080")
35
  user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
36
  options.add_argument(f"--user-agent={user_agent}")
37
+
38
+ # Smartly choose between production and local driver
39
+ if os.path.exists("/usr/bin/chromedriver"):
40
+ options.binary_location = "/usr/bin/chromium"
41
+ service = ChromeService(executable_path="/usr/bin/chromedriver")
42
+ else:
43
+ from webdriver_manager.chrome import ChromeDriverManager
44
+ service = ChromeService(ChromeDriverManager().install())
45
+
46
  self.driver = webdriver.Chrome(service=service, options=options)
47
  self.driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
48
  'source': "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
 
63
  def login(self, username, password):
64
  try:
65
  self.micro_status("Navigating to login page..."); self.driver.get("https://gateway.quantumepay.com/")
66
+ time.sleep(2); self.micro_status("Entering credentials...")
 
67
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "Username"))).send_keys(username)
68
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "Password"))).send_keys(password)
69
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "login"))).click()
 
79
  self.micro_status(f"Submitting OTP..."); otp_digits = list(otp)
80
  for i in range(6): self.driver.find_element(By.ID, f"code{i+1}").send_keys(otp_digits[i])
81
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "login"))).click()
 
82
  self.micro_status("Verifying login success...")
83
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//span[text()='Payments']")))
84
  return True, None
85
  except Exception as e:
86
  error_message = f"Error during OTP submission: {str(e)}"; print(f"[Bot Log] ERROR during OTP submission: {error_message}")
87
  return False, error_message
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
  def start_processing(self, process_type, patient_data, **kwargs):
90
  if process_type == 'void':
 
94
  return []
95
 
96
  def process_void_list(self, patient_data):
97
+ results = []
98
+ for index, record in enumerate(patient_data):
99
  if self.termination_event.is_set(): print("[Bot Log] Termination detected."); break
100
+ patient_name = record['Name']; patient_prn = record.get('PRN', '')
101
  status = 'Skipped - No PRN'
102
  if patient_prn and str(patient_prn).strip():
103
+ self.micro_status(f"Processing VOID for '{patient_name}' ({index + 1}/{len(patient_data)})...")
104
  status = self._process_single_void(patient_name, patient_prn)
105
  else:
106
+ self.micro_status(f"Skipping '{patient_name}' (No PRN)."); time.sleep(0.5)
107
+ results.append({'Name': patient_name, 'PRN': patient_prn, 'Status': status})
108
+ with self.app.app_context():
109
+ self.socketio.emit('log_update', {'name': patient_name, 'prn': patient_prn, 'status': status})
110
+ self.socketio.emit('stats_update', {'processed': index + 1, 'remaining': len(patient_data) - (index + 1), 'status': status})
 
 
 
111
  return results
112
 
113
  def process_refund_list(self, patient_data, start_date_str, end_date_str):
114
+ results = []
115
+ for index, record in enumerate(patient_data):
 
 
 
 
 
 
 
116
  if self.termination_event.is_set(): print("[Bot Log] Termination detected."); break
117
+ patient_name = record['Name']; patient_prn = record.get('PRN', '')
118
+ self.micro_status(f"Processing REFUND for '{patient_name}' ({index + 1}/{len(patient_data)})...")
119
+ status = self._process_single_refund(patient_name, patient_prn, start_date_str, end_date_str)
120
+ results.append({'Name': patient_name, 'PRN': patient_prn, 'Status': status})
121
+ with self.app.app_context():
122
+ self.socketio.emit('log_update', {'name': patient_name, 'prn': patient_prn, 'status': status})
123
+ self.socketio.emit('stats_update', {'processed': index + 1, 'remaining': len(patient_data) - (index + 1), 'status': status})
 
 
 
 
 
 
 
124
  return results
125
 
126
  def _process_single_void(self, patient_name, patient_prn):
127
  try:
128
+ self.micro_status(f"Navigating to Void page for '{patient_name}'")
129
+ self.driver.get("https://gateway.quantumepay.com/credit-card/void")
130
+ search_successful = False
131
+ for attempt in range(15):
132
+ try:
133
+ self.micro_status(f"Searching... (Attempt {attempt + 1})")
134
+ WebDriverWait(self.driver, 2).until(EC.presence_of_element_located((By.XPATH, "//div[contains(@class, 'table-wrapper')]")))
135
+ search_box = WebDriverWait(self.driver, 2).until(EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']")))
136
+ search_box.click(); time.sleep(0.5); search_box.clear(); time.sleep(0.5)
137
+ search_box.send_keys(patient_name); search_successful = True; break
138
+ except Exception: time.sleep(1)
139
+ if not search_successful: raise Exception("Failed to search for patient.")
140
+ time.sleep(3)
141
  self.micro_status("Opening transaction details...")
142
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, f"//tr[contains(., \"{patient_name}\")]//button[@data-v-b6b33fa0]"))).click()
143
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.LINK_TEXT, "Transaction Detail"))).click()
144
  self.micro_status("Adding to Vault...")
145
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Add to Vault']"))).click()
146
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//div[@class='modal-footer']//button/span[normalize-space()='Confirm']"))).click()
147
  try:
148
  self.micro_status("Verifying success and saving...")
149
  company_input = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.NAME, "company_name")))
150
  company_input.clear(); company_input.send_keys(patient_name)
151
  contact_input = WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable((By.NAME, "company_contact")))
152
  contact_input.click(); contact_input.send_keys(Keys.CONTROL + "a"); contact_input.send_keys(Keys.BACK_SPACE)
153
+ self.micro_status(f"Entering PRN: {patient_prn}...")
154
+ contact_input.send_keys(str(patient_prn))
155
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Save Changes']"))).click()
156
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Confirm']]"))).click()
157
+ time.sleep(5); return 'Done'
158
  except TimeoutException:
159
  self.micro_status(f"'{patient_name}' is in a bad state, cancelling.")
160
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Cancel']]"))).click()
161
  return 'Bad'
162
  except Exception as e:
163
+ print(f"An error occurred during VOID for {patient_name}: {e}"); return 'Error'
164
 
165
+ def _get_calendar_months(self):
166
  try:
167
+ titles = self.driver.find_elements(By.XPATH, "//div[contains(@class, 'vc-title')]")
168
+ return [datetime.strptime(title.text, "%B %Y") for title in titles]
169
+ except Exception: return []
170
+
171
+ def _select_date_in_calendar(self, target_date):
172
+ target_month_str = target_date.strftime("%B %Y")
173
+ self.micro_status(f"Navigating calendar to {target_month_str}...")
174
+ for _ in range(24):
175
+ visible_months = self._get_calendar_months()
176
+ if any(d.strftime("%B %Y") == target_month_str for d in visible_months):
177
+ self.micro_status(f"Found month. Selecting day {target_date.day}.")
178
+ day_format = "%#d" if os.name == 'nt' else "%-d"
179
+ expected_aria_label = target_date.strftime(f"%A, %B {day_format}, %Y")
180
+ day_xpath = f"//span[@aria-label='{expected_aria_label}']"
181
+ day_element = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located((By.XPATH, day_xpath)))
182
+ self.driver.execute_script("arguments[0].click();", day_element)
183
+ return
184
+ if target_date < visible_months[0]:
185
+ self.driver.find_element(By.XPATH, "//div[contains(@class, 'vc-arrow') and contains(@class, 'is-left')]").click()
186
+ else:
187
+ self.driver.find_element(By.XPATH, "//div[contains(@class, 'vc-arrow') and contains(@class, 'is-right')]").click()
188
+ time.sleep(0.5)
189
+ raise Exception(f"Could not navigate to date {target_date.strftime('%Y-%m-%d')}")
190
 
191
  def _set_date_range_on_page(self, start_date_str, end_date_str):
192
+ self.micro_status("Opening calendar...")
193
+ date_button = WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[contains(text(), '-')]]")))
194
+ time.sleep(1); date_button.click()
195
+ start_date = datetime.strptime(start_date_str, "%Y-%m-%d"); end_date = datetime.strptime(end_date_str, "%Y-%m-%d")
196
+ self._select_date_in_calendar(start_date)
197
+ time.sleep(1)
198
+ self._select_date_in_calendar(end_date)
199
+ time.sleep(1)
200
+ self.driver.find_element(By.TAG_NAME, "body").click()
201
+ self.micro_status("Date range set. Waiting for data to filter...")
202
+ time.sleep(3)
203
+
204
+ def _process_single_refund(self, patient_name, patient_prn, start_date_str, end_date_str):
205
  try:
206
+ self.micro_status(f"Navigating to Refund page for '{patient_name}'")
207
+ self.driver.get("https://gateway.quantumepay.com/credit-card/refund")
208
+ self._set_date_range_on_page(start_date_str, end_date_str)
209
+
210
+ search_successful = False
211
+ for attempt in range(5):
212
+ try:
213
+ self.micro_status(f"Searching... (Attempt {attempt + 1})")
214
+ search_box = WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']")))
215
+ search_box.click(); time.sleep(0.5); search_box.clear(); time.sleep(0.5)
216
+ search_box.send_keys(patient_name); search_successful = True; break
217
+ except Exception: time.sleep(1)
218
+ if not search_successful: raise Exception("Failed to search for patient.")
219
+ time.sleep(3)
220
+
221
+ # This is a placeholder for the actual refund action.
222
+ # You would find the correct row and click the 'Refund' button.
223
+ self.micro_status("Placeholder: Performing refund action...")
224
  time.sleep(2)
225
+
226
+ return 'Done'
 
 
227
  except Exception as e:
228
+ print(f"An error occurred while processing REFUND for {patient_name}: {e}"); return 'Error'
 
229
 
230
  def shutdown(self):
231
  try: