sonuprasad23 commited on
Commit
f85a3f2
·
verified ·
1 Parent(s): 762f497

Update worker.py

Browse files
Files changed (1) hide show
  1. worker.py +123 -295
worker.py CHANGED
@@ -1,11 +1,10 @@
1
- import os
2
- import re
3
  import time
4
  import threading
5
  import subprocess
6
- from datetime import datetime
7
-
8
  import pandas as pd
 
 
 
9
  from selenium import webdriver
10
  from selenium.webdriver.chrome.service import Service as ChromeService
11
  from selenium.webdriver.chrome.options import Options as ChromeOptions
@@ -15,7 +14,6 @@ from selenium.webdriver.support.ui import WebDriverWait
15
  from selenium.webdriver.support import expected_conditions as EC
16
  from selenium.common.exceptions import TimeoutException, NoSuchElementException
17
 
18
-
19
  class QuantumBot:
20
  def __init__(self, socketio, app, logger):
21
  self.socketio = socketio
@@ -26,20 +24,17 @@ class QuantumBot:
26
  self.logger = logger
27
 
28
  def _kill_chrome_processes(self):
29
- """Attempts to terminate lingering chromium processes."""
30
  try:
31
- self.logger.info("Attempting to kill lingering chromium processes...")
32
  subprocess.run(['pkill', '-f', 'chromium'], check=True, timeout=5)
33
  time.sleep(1)
34
  except Exception as e:
35
- self.logger.warning(f"Could not kill chrome processes (often normal): {e}")
36
 
37
  def initialize_driver(self):
38
- """Sets up the Selenium WebDriver with headless options."""
39
  try:
40
  self.micro_status("Initializing headless browser...")
41
  self._kill_chrome_processes()
42
-
43
  options = ChromeOptions()
44
  options.binary_location = "/usr/bin/chromium"
45
  options.add_argument("--headless=new")
@@ -47,394 +42,227 @@ class QuantumBot:
47
  options.add_argument("--disable-dev-shm-usage")
48
  options.add_argument("--disable-gpu")
49
  options.add_argument("--window-size=1920,1080")
50
-
51
- user_agent = (
52
- "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
53
- "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
54
- )
55
  options.add_argument(f"--user-agent={user_agent}")
56
-
57
  service = ChromeService(executable_path="/usr/bin/chromedriver")
58
  self.driver = webdriver.Chrome(service=service, options=options)
59
-
60
- # Bypass bot detection
61
  self.driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
62
  'source': "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
63
  })
64
-
65
  self.logger.info("WebDriver initialized successfully.")
66
  return True, None
67
  except Exception:
68
  self.logger.exception("CRITICAL ERROR in WebDriver Initialization")
69
- return False, "WebDriver initialization failed. See backend logs."
70
 
71
  def micro_status(self, message):
72
- """Sends status updates to the frontend via SocketIO."""
73
  self.logger.info(message)
74
  with self.app.app_context():
75
  self.socketio.emit('micro_status_update', {'message': message})
76
 
77
  def stop(self):
78
- """Signals the bot to terminate."""
79
  self.micro_status("Termination signal received...")
80
  self.termination_event.set()
81
 
82
  def login(self, username, password):
83
- """Handles the initial login credentials screen."""
84
  try:
85
  self.micro_status("Navigating to login page...")
86
  self.driver.get("https://gateway.quantumepay.com/")
87
  time.sleep(2)
88
-
89
- self.micro_status("Entering credentials...")
90
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
91
- EC.presence_of_element_located((By.ID, "Username"))
92
- ).send_keys(username)
93
-
94
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
95
- EC.presence_of_element_located((By.ID, "Password"))
96
- ).send_keys(password)
97
-
98
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
99
- EC.element_to_be_clickable((By.ID, "login"))
100
- ).click()
101
-
102
  self.micro_status("Waiting for OTP screen...")
103
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
104
- EC.presence_of_element_located((By.ID, "code1"))
105
- )
106
-
107
- self.logger.info("Login successful, OTP screen is visible.")
108
  return True, None
109
  except Exception:
110
  self.logger.exception("ERROR during login process")
111
- return False, "An error occurred during login."
112
 
113
  def submit_otp(self, otp):
114
- """Submits the 6-digit OTP code."""
115
  try:
116
  self.micro_status("Submitting OTP...")
117
  otp_digits = list(otp)
118
  for i in range(6):
119
  self.driver.find_element(By.ID, f"code{i+1}").send_keys(otp_digits[i])
120
-
121
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
122
- EC.element_to_be_clickable((By.ID, "login"))
123
- ).click()
124
-
125
  self.micro_status("Verifying login success...")
126
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
127
- EC.element_to_be_clickable((By.XPATH, "//span[text()='Payments']"))
128
- )
129
-
130
- self.logger.info("OTP submission successful, dashboard visible.")
131
  return True, None
132
  except Exception:
133
  self.logger.exception("ERROR during OTP submission")
134
  return False, "OTP submission failed."
135
 
136
  def _wait_for_page_load(self, timeout=None):
137
- """Waits for the site's loading overlay to disappear."""
138
- if timeout is None:
139
- timeout = self.DEFAULT_TIMEOUT
140
- loading_overlay_xpath = "//div[contains(@class, 'vld-background')]"
141
  try:
142
- WebDriverWait(self.driver, timeout).until(
143
- EC.invisibility_of_element_located((By.XPATH, loading_overlay_xpath))
144
- )
145
- except TimeoutException:
146
- pass
147
 
148
  def _navigate_and_verify(self, url, verification_xpath):
149
- """Navigates to a URL and verifies the page loaded via an element."""
150
- self.micro_status(f"Navigating to {url.split('/')[-1]} page...")
151
  self.driver.get(url)
152
  time.sleep(3)
153
  self._wait_for_page_load()
154
-
155
  try:
156
- WebDriverWait(self.driver, 15).until(
157
- EC.element_to_be_clickable((By.XPATH, verification_xpath))
158
- )
159
- except TimeoutException:
160
- self.micro_status("Page not verified. Refreshing...")
161
  self.driver.refresh()
162
  time.sleep(5)
163
  self._wait_for_page_load()
164
- WebDriverWait(self.driver, 15).until(
165
- EC.element_to_be_clickable((By.XPATH, verification_xpath))
166
- )
167
 
168
  def _get_calendar_months(self):
169
- """Extracts visible months from the calendar widget."""
170
  try:
171
  titles = self.driver.find_elements(By.XPATH, "//div[contains(@class, 'vc-title')]")
172
  return [datetime.strptime(title.text, "%B %Y") for title in titles] if titles else []
173
- except Exception as e:
174
- self.logger.warning(f"Could not get calendar months: {e}")
175
- return []
176
 
177
  def _select_date_in_calendar(self, target_date):
178
- """Navigates the calendar UI to pick a specific date."""
179
  target_month_str = target_date.strftime("%B %Y")
180
- for _ in range(24): # Look up to 2 years ahead/back
181
  visible_months = self._get_calendar_months()
182
- if not visible_months:
183
- raise Exception("Calendar months not visible.")
184
-
185
  if any(d.strftime("%B %Y") == target_month_str for d in visible_months):
186
- day_format = "%-d"
187
- day_label = target_date.strftime(f'%A, %B {day_format}, %Y')
188
- day_xpath = f"//span[@aria-label='{day_label}']"
189
- day_element = WebDriverWait(self.driver, 10).until(
190
- EC.element_to_be_clickable((By.XPATH, day_xpath))
191
- )
192
- self.driver.execute_script("arguments[0].click();", day_element)
193
  return
194
-
195
- # Determine direction to click
196
- if target_date < visible_months[0]:
197
- arrow = "//div[contains(@class, 'vc-arrow') and contains(@class, 'is-left')]"
198
- else:
199
- arrow = "//div[contains(@class, 'vc-arrow') and contains(@class, 'is-right')]"
200
-
201
  self.driver.find_element(By.XPATH, arrow).click()
202
  time.sleep(0.5)
203
 
204
- raise Exception(f"Could not navigate to date {target_date.strftime('%Y-%m-%d')}")
205
-
206
  def _set_date_range(self, start_date_str, end_date_str):
207
- """Sets the date range filter on the current page."""
208
  self.micro_status("Setting date range...")
209
- date_btn_xpath = "//button[.//span[contains(text(), '-')]]"
210
- date_button = WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
211
- EC.element_to_be_clickable((By.XPATH, date_btn_xpath))
212
- )
213
- time.sleep(1)
214
  date_button.click()
215
  self._wait_for_page_load()
216
-
217
- start_date = datetime.strptime(start_date_str, "%Y-%m-%d")
218
- end_date = datetime.strptime(end_date_str, "%Y-%m-%d")
219
-
220
- self._select_date_in_calendar(start_date)
221
  self._wait_for_page_load()
222
- self._select_date_in_calendar(end_date)
223
  self._wait_for_page_load()
224
-
225
- # Close calendar by clicking away
226
  self.driver.find_element(By.TAG_NAME, "body").click()
227
- self._wait_for_page_load()
228
 
229
- def _clear_input_robust(self, locator, timeout=10):
230
- """Clears an input field using standard keys and JS fallback."""
231
- el = WebDriverWait(self.driver, timeout).until(
232
- EC.element_to_be_clickable(locator)
233
- )
234
  self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", el)
235
  el.click()
236
  el.send_keys(Keys.CONTROL, "a")
237
  el.send_keys(Keys.BACK_SPACE)
238
- self._wait_for_page_load(timeout=2)
239
-
240
- val = (el.get_attribute('value') or '').strip()
241
- if val != '':
242
- js_clear = (
243
- "arguments[0].value='';"
244
- "arguments[0].dispatchEvent(new Event('input',{bubbles:true}));"
245
- )
246
- self.driver.execute_script(js_clear, el)
247
  return el
248
 
249
  def _type_text_robust(self, el, text):
250
- """Types text into an element if the value is valid."""
251
  s = '' if text is None or pd.isna(text) else str(text).strip()
252
- if s and s.lower() != 'nan':
253
- el.send_keys(s)
254
 
255
  def _find_action_button(self, patient_name):
256
- """Finds the kebab menu button for a row using multiple strategies."""
257
- action_button_xpaths = [
258
  f"//tr[contains(., \"{patient_name}\")]//td[contains(@class,'is-action')]//button",
259
- f"//tr[contains(., \"{patient_name}\")]//button[contains(@class,'q-button')]",
260
- f"//tr[contains(., \"{patient_name}\")]//td[last()]//button",
261
- "//td[contains(@class,'is-action')]//button",
262
- "//button[.//svg[contains(@data-icon,'more-vertical')]]",
263
- (
264
- "//td[contains(@class,'table-cell') and contains(@class,'is-action')]"
265
- "//button[contains(@class,'q-button') and .//svg[contains(@data-icon,'more-vertical')]]"
266
- ),
267
- "//button[contains(@class,'q-button') and .//svg]",
268
- "//td[last()]//button",
269
  ]
270
-
271
- for xpath in action_button_xpaths:
272
- try:
273
- button = WebDriverWait(self.driver, 5).until(
274
- EC.element_to_be_clickable((By.XPATH, xpath))
275
- )
276
- self.logger.info(f"Action button found with XPath: {xpath}")
277
- return button
278
- except TimeoutException:
279
- continue
280
- return None
281
-
282
- def _find_transaction_detail_link(self):
283
- """Finds the 'Transaction Detail' link in the dropdown menu."""
284
- detail_xpaths = [
285
- "//a[contains(@class,'dropdown-item') and normalize-space()='Transaction Detail']",
286
- "//a[normalize-space()='Transaction Detail']",
287
- "//div[contains(@class,'dropdown')]//a[contains(text(),'Transaction')]",
288
- "//a[contains(text(),'Transaction Detail')]",
289
- "//li//a[contains(text(),'Transaction')]",
290
- "//*[contains(@class,'dropdown-item') and contains(text(),'Transaction')]",
291
- ]
292
-
293
- for xpath in detail_xpaths:
294
- try:
295
- link = WebDriverWait(self.driver, 5).until(
296
- EC.element_to_be_clickable((By.XPATH, xpath))
297
- )
298
- self.logger.info(f"Transaction Detail link found with XPath: {xpath}")
299
- return link
300
- except TimeoutException:
301
- continue
302
  return None
303
 
304
  def _safe_click(self, element, description="element"):
305
- """Clicks an element normally, or via JS if normal click fails."""
306
- try:
307
- element.click()
308
- except Exception as e:
309
- self.logger.warning(f"Normal click on {description} failed ({e}), trying JS click...")
310
- self.driver.execute_script("arguments[0].click();", element)
311
 
312
- def _save_debug_screenshot(self, patient_name, context="unknown"):
313
- """Saves screenshot and logs partial source for debugging."""
314
  try:
315
- safe_name = patient_name.replace(' ', '_').replace('/', '_')
316
- screenshot_path = f"/tmp/debug_{safe_name}_{context}.png"
317
- self.driver.save_screenshot(screenshot_path)
318
- self.logger.error(f"Debug screenshot saved to {screenshot_path}")
319
-
320
- page_source = self.driver.page_source
321
- action_cells = re.findall(r'<td[^>]*action[^>]*>.*?</td>', page_source, re.DOTALL)
322
- if action_cells:
323
- self.logger.error(f"Action cell HTML sample: {action_cells[0][:500]}")
324
-
325
- buttons = re.findall(r'<button[^>]*>.*?</button>', page_source, re.DOTALL)
326
- if buttons:
327
- self.logger.error(f"Total buttons in page: {len(buttons)}")
328
- except Exception as debug_err:
329
- self.logger.warning(f"Debug logging failed: {debug_err}")
330
 
331
  def _perform_core_patient_processing(self, patient_name, patient_prn):
332
  try:
333
- # ============================================================
334
- # STEP 1: Search for the patient
335
- # ============================================================
336
  self.micro_status(f"Searching for '{patient_name}'...")
337
- search_box = WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
338
- EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']"))
339
- )
340
- search_box.click()
341
- time.sleep(0.5)
342
- search_box.clear()
343
- time.sleep(0.5)
344
- search_box.send_keys(patient_name)
345
- self._wait_for_page_load()
346
- time.sleep(2)
347
 
348
- # ============================================================
349
- # STEP 2: Verify patient row exists
350
- # ============================================================
351
- row_xpath = f"//tr[contains(., \"{patient_name}\")]"
352
- try:
353
- WebDriverWait(self.driver, 10).until(
354
- EC.presence_of_element_located((By.XPATH, row_xpath))
355
- )
356
- except TimeoutException:
357
- self.micro_status(f"Patient '{patient_name}' not found in table.")
358
  return 'Not Found'
359
 
360
- # ============================================================
361
- # STEP 3: Find and click the action button (kebab menu)
362
- # ============================================================
363
- self.micro_status("Opening transaction details...")
364
- action_button = self._find_action_button(patient_name)
365
-
366
- if action_button is None:
367
- self.logger.error(f"Could not find action button for '{patient_name}'")
368
- self._save_debug_screenshot(patient_name, "action_button")
369
- return 'Error'
370
-
371
- self._safe_click(action_button, "action button")
372
- time.sleep(1)
373
-
374
- # ============================================================
375
- # STEP 4: Find and click "Transaction Detail" link
376
- # ============================================================
377
- detail_link = self._find_transaction_detail_link()
378
-
379
- if detail_link is None:
380
- self.logger.error(f"Could not find 'Transaction Detail' link for '{patient_name}'.")
381
- self._save_debug_screenshot(patient_name, "transaction_detail")
382
- return 'Error'
383
-
384
- self._safe_click(detail_link, "Transaction Detail link")
385
  self._wait_for_page_load()
386
- time.sleep(1)
387
 
388
- # ============================================================
389
- # STEP 5: Add to Vault
390
- # ============================================================
391
- self.micro_status("Adding to Vault...")
392
- add_to_vault_btn = WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
393
- EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Add to Vault']"))
394
- )
395
- self._safe_click(add_to_vault_btn, "Add to Vault button")
396
  self._wait_for_page_load()
397
-
398
- # ============================================================
399
- # STEP 6: Confirm the vault action
400
- # ============================================================
401
- confirm_btn = WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
402
- EC.element_to_be_clickable((
403
- By.XPATH,
404
- "//div[@class='modal-footer']//button/span[normalize-space()='Confirm']"
405
- ))
406
- )
407
- self._safe_click(confirm_btn, "Confirm button")
408
  self._wait_for_page_load()
409
 
410
- # ============================================================
411
- # STEP 7: Save company name and contact (PRN)
412
- # ============================================================
413
- self.micro_status("Verifying success and saving data...")
414
-
415
- company_el = self._clear_input_robust((By.NAME, "company_name"))
416
- self._type_text_robust(company_el, patient_name)
417
-
418
- contact_el = self._clear_input_robust((By.NAME, "company_contact"))
419
- self._type_text_robust(contact_el, patient_prn)
 
 
 
 
420
 
421
- save_btn = WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
422
- EC.element_to_be_clickable((
423
- By.XPATH,
424
- "//button/span[normalize-space()='Save Changes']"
425
- ))
426
- )
427
- self._safe_click(save_btn, "Save Changes button")
428
-
429
- # Wait for save confirmation (adjust selector based on actual site behavior)
430
- self._wait_for_page_load()
431
- time.sleep(2)
 
 
 
 
 
432
 
433
- self.logger.info(f"Successfully processed patient: {patient_name}")
434
- return 'Success'
435
-
436
- except Exception as e:
437
- self.logger.exception(f"Unexpected error processing patient '{patient_name}': {e}")
438
- self._save_debug_screenshot(patient_name, "fatal_error")
439
- return 'Error'
440
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import time
2
  import threading
3
  import subprocess
 
 
4
  import pandas as pd
5
+ import os
6
+ import re
7
+ from datetime import datetime
8
  from selenium import webdriver
9
  from selenium.webdriver.chrome.service import Service as ChromeService
10
  from selenium.webdriver.chrome.options import Options as ChromeOptions
 
14
  from selenium.webdriver.support import expected_conditions as EC
15
  from selenium.common.exceptions import TimeoutException, NoSuchElementException
16
 
 
17
  class QuantumBot:
18
  def __init__(self, socketio, app, logger):
19
  self.socketio = socketio
 
24
  self.logger = logger
25
 
26
  def _kill_chrome_processes(self):
 
27
  try:
28
+ self.logger.info("Attempting to kill any lingering chromium processes...")
29
  subprocess.run(['pkill', '-f', 'chromium'], check=True, timeout=5)
30
  time.sleep(1)
31
  except Exception as e:
32
+ self.logger.warning(f"Could not kill chrome processes: {e}")
33
 
34
  def initialize_driver(self):
 
35
  try:
36
  self.micro_status("Initializing headless browser...")
37
  self._kill_chrome_processes()
 
38
  options = ChromeOptions()
39
  options.binary_location = "/usr/bin/chromium"
40
  options.add_argument("--headless=new")
 
42
  options.add_argument("--disable-dev-shm-usage")
43
  options.add_argument("--disable-gpu")
44
  options.add_argument("--window-size=1920,1080")
45
+ user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
 
 
 
 
46
  options.add_argument(f"--user-agent={user_agent}")
 
47
  service = ChromeService(executable_path="/usr/bin/chromedriver")
48
  self.driver = webdriver.Chrome(service=service, options=options)
 
 
49
  self.driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
50
  'source': "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
51
  })
 
52
  self.logger.info("WebDriver initialized successfully.")
53
  return True, None
54
  except Exception:
55
  self.logger.exception("CRITICAL ERROR in WebDriver Initialization")
56
+ return False, "WebDriver initialization failed."
57
 
58
  def micro_status(self, message):
59
+ """Updates the small status text on the frontend."""
60
  self.logger.info(message)
61
  with self.app.app_context():
62
  self.socketio.emit('micro_status_update', {'message': message})
63
 
64
  def stop(self):
 
65
  self.micro_status("Termination signal received...")
66
  self.termination_event.set()
67
 
68
  def login(self, username, password):
 
69
  try:
70
  self.micro_status("Navigating to login page...")
71
  self.driver.get("https://gateway.quantumepay.com/")
72
  time.sleep(2)
73
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "Username"))).send_keys(username)
74
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "Password"))).send_keys(password)
75
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "login"))).click()
 
 
 
 
 
 
 
 
 
 
 
76
  self.micro_status("Waiting for OTP screen...")
77
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "code1")))
 
 
 
 
78
  return True, None
79
  except Exception:
80
  self.logger.exception("ERROR during login process")
81
+ return False, "Login failed."
82
 
83
  def submit_otp(self, otp):
 
84
  try:
85
  self.micro_status("Submitting OTP...")
86
  otp_digits = list(otp)
87
  for i in range(6):
88
  self.driver.find_element(By.ID, f"code{i+1}").send_keys(otp_digits[i])
89
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "login"))).click()
 
 
 
 
90
  self.micro_status("Verifying login success...")
91
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//span[text()='Payments']")))
 
 
 
 
92
  return True, None
93
  except Exception:
94
  self.logger.exception("ERROR during OTP submission")
95
  return False, "OTP submission failed."
96
 
97
  def _wait_for_page_load(self, timeout=None):
98
+ if timeout is None: timeout = self.DEFAULT_TIMEOUT
 
 
 
99
  try:
100
+ WebDriverWait(self.driver, timeout).until(EC.invisibility_of_element_located((By.XPATH, "//div[contains(@class, 'vld-background')]")))
101
+ except: pass
 
 
 
102
 
103
  def _navigate_and_verify(self, url, verification_xpath):
104
+ self.micro_status(f"Navigating to {url.split('/')[-1]}...")
 
105
  self.driver.get(url)
106
  time.sleep(3)
107
  self._wait_for_page_load()
 
108
  try:
109
+ WebDriverWait(self.driver, 15).until(EC.element_to_be_clickable((By.XPATH, verification_xpath)))
110
+ except:
 
 
 
111
  self.driver.refresh()
112
  time.sleep(5)
113
  self._wait_for_page_load()
 
 
 
114
 
115
  def _get_calendar_months(self):
 
116
  try:
117
  titles = self.driver.find_elements(By.XPATH, "//div[contains(@class, 'vc-title')]")
118
  return [datetime.strptime(title.text, "%B %Y") for title in titles] if titles else []
119
+ except: return []
 
 
120
 
121
  def _select_date_in_calendar(self, target_date):
 
122
  target_month_str = target_date.strftime("%B %Y")
123
+ for _ in range(24):
124
  visible_months = self._get_calendar_months()
 
 
 
125
  if any(d.strftime("%B %Y") == target_month_str for d in visible_months):
126
+ day_xpath = f"//span[@aria-label='{target_date.strftime('%A, %B %-d, %Y')}']"
127
+ day_el = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.XPATH, day_xpath)))
128
+ self.driver.execute_script("arguments[0].click();", day_el)
 
 
 
 
129
  return
130
+ arrow = "//div[contains(@class, 'vc-arrow') and contains(@class, 'is-left')]" if target_date < visible_months[0] else "//div[contains(@class, 'vc-arrow') and contains(@class, 'is-right')]"
 
 
 
 
 
 
131
  self.driver.find_element(By.XPATH, arrow).click()
132
  time.sleep(0.5)
133
 
 
 
134
  def _set_date_range(self, start_date_str, end_date_str):
 
135
  self.micro_status("Setting date range...")
136
+ date_button = WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[contains(text(), '-')]]")))
 
 
 
 
137
  date_button.click()
138
  self._wait_for_page_load()
139
+ self._select_date_in_calendar(datetime.strptime(start_date_str, "%Y-%m-%d"))
 
 
 
 
140
  self._wait_for_page_load()
141
+ self._select_date_in_calendar(datetime.strptime(end_date_str, "%Y-%m-%d"))
142
  self._wait_for_page_load()
 
 
143
  self.driver.find_element(By.TAG_NAME, "body").click()
 
144
 
145
+ def _clear_input_robust(self, locator):
146
+ el = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable(locator))
 
 
 
147
  self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", el)
148
  el.click()
149
  el.send_keys(Keys.CONTROL, "a")
150
  el.send_keys(Keys.BACK_SPACE)
151
+ self.driver.execute_script("arguments[0].value=''; arguments[0].dispatchEvent(new Event('input',{bubbles:true}));", el)
 
 
 
 
 
 
 
 
152
  return el
153
 
154
  def _type_text_robust(self, el, text):
 
155
  s = '' if text is None or pd.isna(text) else str(text).strip()
156
+ if s and s.lower() != 'nan': el.send_keys(s)
 
157
 
158
  def _find_action_button(self, patient_name):
159
+ xpaths = [
 
160
  f"//tr[contains(., \"{patient_name}\")]//td[contains(@class,'is-action')]//button",
161
+ f"//tr[contains(., \"{patient_name}\")]//button",
162
+ "//td[contains(@class,'is-action')]//button"
 
 
 
 
 
 
 
 
163
  ]
164
+ for xpath in xpaths:
165
+ try: return WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable((By.XPATH, xpath)))
166
+ except: continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  return None
168
 
169
  def _safe_click(self, element, description="element"):
170
+ try: element.click()
171
+ except: self.driver.execute_script("arguments[0].click();", element)
 
 
 
 
172
 
173
+ def _save_debug_screenshot(self, patient_name, context):
 
174
  try:
175
+ path = f"/tmp/debug_{patient_name.replace(' ','_')}_{context}.png"
176
+ self.driver.save_screenshot(path)
177
+ except: pass
 
 
 
 
 
 
 
 
 
 
 
 
178
 
179
  def _perform_core_patient_processing(self, patient_name, patient_prn):
180
  try:
 
 
 
181
  self.micro_status(f"Searching for '{patient_name}'...")
182
+ search_box = WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']")))
183
+ search_box.click(); search_box.clear(); search_box.send_keys(patient_name)
184
+ self._wait_for_page_load(); time.sleep(2)
 
 
 
 
 
 
 
185
 
186
+ if not self.driver.find_elements(By.XPATH, f"//tr[contains(., \"{patient_name}\")]"):
 
 
 
 
 
 
 
 
 
187
  return 'Not Found'
188
 
189
+ btn = self._find_action_button(patient_name)
190
+ if not btn: return 'Error'
191
+ self._safe_click(btn)
192
+
193
+ detail_xpath = "//a[contains(@class,'dropdown-item') and normalize-space()='Transaction Detail']"
194
+ detail_link = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.XPATH, detail_xpath)))
195
+ self._safe_click(detail_link)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  self._wait_for_page_load()
 
197
 
198
+ self.micro_status("Vaulting and Saving...")
199
+ WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Add to Vault']"))).click()
 
 
 
 
 
 
200
  self._wait_for_page_load()
201
+ WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.XPATH, "//div[@class='modal-footer']//button/span[normalize-space()='Confirm']"))).click()
 
 
 
 
 
 
 
 
 
 
202
  self._wait_for_page_load()
203
 
204
+ try:
205
+ comp = self._clear_input_robust((By.NAME, "company_name"))
206
+ self._type_text_robust(comp, patient_name)
207
+ cont = self._clear_input_robust((By.NAME, "company_contact"))
208
+ self._type_text_robust(cont, patient_prn)
209
+ WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Save Changes']"))).click()
210
+ self._wait_for_page_load()
211
+ WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Confirm']]"))).click()
212
+ time.sleep(2); return 'Done'
213
+ except:
214
+ WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Cancel']]"))).click()
215
+ return 'Bad'
216
+ except Exception:
217
+ return 'Error'
218
 
219
+ def process_patient_list(self, patient_data, workflow, date_range=None):
220
+ """This is the main entry point called by server.py to update the frontend."""
221
+ self.logger.info(f"Starting {workflow} workflow...")
222
+ results = []
223
+ is_refund_setup_done = False
224
+
225
+ if workflow == 'refund' and date_range:
226
+ try:
227
+ self._navigate_and_verify("https://gateway.quantumepay.com/credit-card/refund", "//button[.//span[contains(text(), '-')]]")
228
+ self._set_date_range(date_range['start_date'], date_range['end_date'])
229
+ is_refund_setup_done = True
230
+ except: return []
231
+
232
+ for index, record in enumerate(patient_data):
233
+ if self.termination_event.is_set(): break
234
+ name = record['Name']; prn = record.get('PRN', '')
235
 
236
+ if not prn:
237
+ status = 'Skipped - No PRN'
238
+ else:
239
+ self.micro_status(f"Processing {index+1}/{len(patient_data)}: {name}")
240
+ if workflow == 'void':
241
+ self._navigate_and_verify("https://gateway.quantumepay.com/credit-card/void", "//input[@placeholder='Search']")
242
+ status = self._perform_core_patient_processing(name, prn)
243
+ elif workflow == 'refund' and is_refund_setup_done:
244
+ status = self._process_single_refund(name, prn, date_range)
245
+ else: status = 'Error'
246
+
247
+ results.append({'Name': name, 'PRN': prn, 'Status': status})
248
+ # IMPORTANT: Connects to Frontend
249
+ with self.app.app_context():
250
+ self.socketio.emit('log_update', {'name': name, 'prn': prn, 'status': status})
251
+ self.socketio.emit('stats_update', {'processed': len(results), 'remaining': len(patient_data)-len(results), 'status': status})
252
+
253
+ return results
254
+
255
+ def _process_single_refund(self, name, prn, date_range):
256
+ status = self._perform_core_patient_processing(name, prn)
257
+ if status == 'Not Found':
258
+ self._navigate_and_verify("https://gateway.quantumepay.com/credit-card/refund", "//button[.//span[contains(text(), '-')]]")
259
+ self._set_date_range(date_range['start_date'], date_range['end_date'])
260
+ status = self._perform_core_patient_processing(name, prn)
261
+ return status
262
+
263
+ def shutdown(self):
264
+ """Cleanup method called by server.py."""
265
+ try:
266
+ if self.driver: self.driver.quit()
267
+ self._kill_chrome_processes()
268
+ except: pass