sonuprasad23 commited on
Commit
47657a7
·
1 Parent(s): 7dde815

Fixing this

Browse files
Files changed (2) hide show
  1. server.py +19 -11
  2. worker.py +71 -76
server.py CHANGED
@@ -47,7 +47,7 @@ class GmailApiService:
47
  def create_professional_email_template(self, subject, status_text, stats, custom_name, process_type):
48
  status_color = "#28a745" if "completed" in status_text else "#ffc107" if "terminated" in status_text else "#dc3545"
49
  current_date = datetime.now().strftime("%B %d, %Y at %I:%M %p")
50
- html_template = f"""<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>{subject}</title><style> * {{ margin: 0; padding: 0; box-sizing: border-box; }} body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #2c2c2c; background-color: #f8f8f8; }} .email-container {{ max-width: 700px; margin: 20px auto; background-color: #ffffff; box-shadow: 0 8px 32px rgba(139, 0, 0, 0.15); border-radius: 12px; overflow: hidden; }} .header {{ background: linear-gradient(135deg, #8A0303 0%, #4c00ff 100%); color: white; padding: 40px 30px; text-align: center; }} .header h1 {{ font-size: 32px; font-weight: 700; margin-bottom: 8px; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); }} .header p {{ font-size: 18px; opacity: 0.95; font-weight: 300; letter-spacing: 1px; }} .status-banner {{ background: {status_color}; color: white; text-align: center; padding: 18px; font-size: 16px; font-weight: 600; text-transform: uppercase; letter-spacing: 1.5px; }} .content {{ padding: 40px 30px; }} .report-info {{ background: #fdfdff; border-left: 6px solid #4c00ff; padding: 25px; margin-bottom: 35px; border-radius: 8px; box-shadow: 0 4px 12px rgba(76, 0, 255, 0.1); }} .info-grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 15px 25px; }} .info-item {{ display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #f0f0f0; }} .stats-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 20px; margin: 35px 0; }} .stat-card {{ background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 12px; padding: 25px 15px; text-align: center; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); }} .stat-number {{ font-size: 36px; font-weight: 700; color: #4c00ff; margin-bottom: 10px; }} .attachments-section {{ background: #f8f5ff; border: 1px solid #e0e0e0; border-radius: 12px; padding: 25px; margin: 35px 0; }} .footer {{ background: #2c2c2c; color: white; padding: 35px 30px; text-align: center; }} h3 {{ color: #8A0303; margin-bottom: 20px; font-size: 20px; font-weight: 600; }} </style></head><body> <div class="email-container"> <div class="header"><h1>Hillside's Quantum Automation</h1><p>Patient Processing Report</p></div> <div class="status-banner">Process {status_text}</div> <div class="content"> <div class="report-info"> <h3>Report Summary</h3> <div class="info-grid"> <div class="info-item"><span><b>Report Name</b></span><span>{custom_name}</span></div> <div class="info-item"><span><b>Process Type</b></span><span>{process_type}</span></div> <div class="info-item"><span><b>Generated</b></span><span>{current_date}</span></div> <div class="info-item"><span><b>Status</b></span><span style="font-weight: bold; color: {status_color};">{status_text}</span></div> </div> </div> <h3>Processing Results</h3> <div class="stats-grid"> <div class="stat-card"><div class="stat-number">{stats['total']}</div><div>Total Patients</div></div> <div class="stat-card"><div class="stat-number">{stats['processed']}</div><div>Attempted</div></div> <div class="stat-card"><div class="stat-number">{stats['successful']}</div><div>Done</div></div> <div class="stat-card"><div class="stat-number">{stats['bad']}</div><div>Bad State</div></div> <div class="stat-card"><div class="stat-number">{stats['skipped']}</div><div>Skipped</div></div> </div> <div class="attachments-section"> <h3>Attached Reports</h3> <p style="margin-bottom: 1rem;">The following files are attached and have been uploaded to Google Drive:</p> <ul style="list-style-position: inside; padding-left: 0;"> <li><b>{custom_name}_Full.csv:</b> The complete report with the final status for every patient.</li> <li><b>{custom_name}_Bad.csv:</b> A filtered list of patients that resulted in a "Bad" state.</li> <li><b>{custom_name}_Skipped.csv:</b> A filtered list of patients that were skipped.</li> </ul> </div> </div> <div class="footer"><h4>Hillside Automation</h4><p>This is an automated report. Please do not reply.</p></div> </div> </body></html>"""
51
  return html_template
52
 
53
  def send_report(self, recipients, subject, body, attachments=None):
@@ -104,7 +104,6 @@ def run_automation_process(session_id):
104
  data = session_data.get(session_id, {});
105
  patient_data = data.get('patient_data'); mode = data.get('mode')
106
  if not patient_data: raise ValueError("No patient data prepared for automation.")
107
- socketio.emit('initial_stats', {'total': len(patient_data)})
108
  results = bot_instance.start_processing(mode, patient_data, start_date=data.get('start_date'), end_date=data.get('end_date'))
109
  is_terminated = bot_instance.termination_event.is_set()
110
  except Exception as e:
@@ -158,7 +157,7 @@ def generate_and_send_reports(session_id, results, is_crash_report=False, is_ter
158
 
159
  @app.route('/')
160
  def status_page():
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):
@@ -166,11 +165,10 @@ def extract_patient_name(raw_name):
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
- # Load session data from the custom header
174
  session_data_from_header = json.loads(request.headers.get('X-Session-Data'))
175
  session_data[session_id] = session_data_from_header
176
 
@@ -191,8 +189,10 @@ def handle_file_processing_and_init():
191
  master_df = df_quantum.copy()
192
  master_df['Status'] = ''
193
 
 
194
  session_data[session_id]['patient_data_for_report'] = master_df
195
- session_data[session_id]['patient_data'] = master_df.to_dict('records')
 
196
 
197
  socketio.emit('data_processed')
198
  return jsonify({"message": "Data processed successfully."})
@@ -205,19 +205,27 @@ def handle_connect():
205
  print(f'Frontend connected.')
206
  emit('email_list', {'emails': get_email_list()})
207
 
208
- @socketio.on('start_login')
209
- def handle_login(credentials):
 
 
 
210
  global bot_instance
211
  if bot_instance: bot_instance.shutdown()
212
  bot_instance = QuantumBot(socketio, app)
213
  is_success, error_message = bot_instance.initialize_driver()
214
  if is_success:
215
- is_login_success, login_error = bot_instance.login(credentials['username'], credentials['password'])
216
- if is_login_success: emit('otp_required')
217
- else: emit('error', {'message': f'Login failed: {login_error}'})
218
  else:
219
  emit('error', {'message': f'Failed to initialize bot: {error_message}'})
220
 
 
 
 
 
 
 
 
221
  @socketio.on('submit_otp')
222
  def handle_otp(data):
223
  if not bot_instance: return emit('error', {'message': 'Bot not initialized.'})
 
47
  def create_professional_email_template(self, subject, status_text, stats, custom_name, process_type):
48
  status_color = "#28a745" if "completed" in status_text else "#ffc107" if "terminated" in status_text else "#dc3545"
49
  current_date = datetime.now().strftime("%B %d, %Y at %I:%M %p")
50
+ html_template = f"""<!DOCTYPE html><html><head><title>{subject}</title><style>body{{font-family:sans-serif;}}</style></head><body><h2>Hillside's Quantum Automation Report</h2><p><b>Status:</b> {status_text}</p><div>...</div></body></html>"""
51
  return html_template
52
 
53
  def send_report(self, recipients, subject, body, attachments=None):
 
104
  data = session_data.get(session_id, {});
105
  patient_data = data.get('patient_data'); mode = data.get('mode')
106
  if not patient_data: raise ValueError("No patient data prepared for automation.")
 
107
  results = bot_instance.start_processing(mode, patient_data, start_date=data.get('start_date'), end_date=data.get('end_date'))
108
  is_terminated = bot_instance.termination_event.is_set()
109
  except Exception as e:
 
157
 
158
  @app.route('/')
159
  def status_page():
160
+ APP_STATUS_HTML = """<!DOCTYPE html><html><head><title>API Status</title></head><body><h1>Hillside Automation API is Active</h1></body></html>"""
161
  return Response(APP_STATUS_HTML)
162
 
163
  def extract_patient_name(raw_name):
 
165
  name_only = raw_name.split('DOB')[0].strip()
166
  return re.sub(r'[:\d\-\s]+$', '', name_only).strip()
167
 
168
+ @app.route('/process_files_for_automation', methods=['POST'])
169
  def handle_file_processing_and_init():
170
  session_id = 'user_session'
171
  try:
 
172
  session_data_from_header = json.loads(request.headers.get('X-Session-Data'))
173
  session_data[session_id] = session_data_from_header
174
 
 
189
  master_df = df_quantum.copy()
190
  master_df['Status'] = ''
191
 
192
+ # We save the full, merged dataframe for the final report
193
  session_data[session_id]['patient_data_for_report'] = master_df
194
+ # We only process records that have a PRN
195
+ session_data[session_id]['patient_data'] = master_df[master_df['PRN'] != ''].to_dict('records')
196
 
197
  socketio.emit('data_processed')
198
  return jsonify({"message": "Data processed successfully."})
 
205
  print(f'Frontend connected.')
206
  emit('email_list', {'emails': get_email_list()})
207
 
208
+ @socketio.on('initialize_session')
209
+ def handle_init(data):
210
+ session_id = 'user_session'
211
+ session_data[session_id] = data # Store mode, dates, emails etc.
212
+
213
  global bot_instance
214
  if bot_instance: bot_instance.shutdown()
215
  bot_instance = QuantumBot(socketio, app)
216
  is_success, error_message = bot_instance.initialize_driver()
217
  if is_success:
218
+ emit('bot_initialized')
 
 
219
  else:
220
  emit('error', {'message': f'Failed to initialize bot: {error_message}'})
221
 
222
+ @socketio.on('start_login')
223
+ def handle_login(credentials):
224
+ if not bot_instance: return emit('error', {'message': 'Bot not initialized.'})
225
+ is_success, error_message = bot_instance.login(credentials['username'], credentials['password'])
226
+ if is_success: emit('otp_required')
227
+ else: emit('error', {'message': f'Login failed: {error_message}'})
228
+
229
  @socketio.on('submit_otp')
230
  def handle_otp(data):
231
  if not bot_instance: return emit('error', {'message': 'Bot not initialized.'})
worker.py CHANGED
@@ -12,29 +12,26 @@ from selenium.webdriver.common.keys import Keys
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):
18
  self.socketio = socketio; self.app = app; self.driver = None
19
  self.DEFAULT_TIMEOUT = 30; self.termination_event = threading.Event()
20
-
21
- def _kill_chrome_processes(self):
22
- try:
23
- subprocess.run(['pkill', '-f', 'chromium'], check=True, timeout=5)
24
- time.sleep(1)
25
- except Exception: pass
26
 
27
  def initialize_driver(self):
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})"
@@ -82,7 +79,7 @@ class QuantumBot:
82
  if process_type == 'void':
83
  return self.process_void_list(patient_data)
84
  elif process_type == 'refund':
85
- return self.process_refund_list(patient_data, kwargs.get('start_date'), kwargs.get('end_date'))
86
  return []
87
 
88
  def process_void_list(self, patient_data):
@@ -90,67 +87,73 @@ class QuantumBot:
90
  for index, record in enumerate(patient_data):
91
  if self.termination_event.is_set(): print("[Bot Log] Termination detected."); break
92
  patient_name = record['Name']; patient_prn = record.get('PRN', '')
93
- status = 'Skipped - No PRN'
94
- if patient_prn and str(patient_prn).strip():
95
- self.micro_status(f"Processing VOID for '{patient_name}' ({index + 1}/{len(patient_data)})...")
96
- status = self._process_single_void(patient_name, patient_prn)
97
- else:
98
- self.micro_status(f"Skipping '{patient_name}' (No PRN)."); time.sleep(0.5)
99
  results.append({'Name': patient_name, 'PRN': patient_prn, 'Status': status})
100
  with self.app.app_context():
101
  self.socketio.emit('log_update', {'name': patient_name, 'prn': patient_prn, 'status': status})
102
  self.socketio.emit('stats_update', {'processed': index + 1, 'remaining': len(patient_data) - (index + 1), 'status': status})
103
  return results
104
 
105
- def process_refund_list(self, patient_data, start_date_str, end_date_str):
106
- results = []
107
- self._navigate_and_verify("https://gateway.quantumepay.com/credit-card/refund", "//button[.//span[contains(text(), '-')]]")
108
- self._set_date_range_on_page(start_date_str, end_date_str)
109
 
 
 
 
 
110
  for index, record in enumerate(patient_data):
111
  if self.termination_event.is_set(): print("[Bot Log] Termination detected."); break
112
  patient_name = record['Name']; patient_prn = record.get('PRN', '')
113
- status = 'Skipped - No PRN'
114
- if patient_prn and str(patient_prn).strip():
115
- self.micro_status(f"Processing REFUND for '{patient_name}' ({index + 1}/{len(patient_data)})...")
116
- status = self._process_single_refund(patient_name, patient_prn)
117
- else:
118
- self.micro_status(f"Skipping '{patient_name}' (No PRN)."); time.sleep(0.5)
119
  results.append({'Name': patient_name, 'PRN': patient_prn, 'Status': status})
120
  with self.app.app_context():
121
  self.socketio.emit('log_update', {'name': patient_name, 'prn': patient_prn, 'status': status})
122
  self.socketio.emit('stats_update', {'processed': index + 1, 'remaining': len(patient_data) - (index + 1), 'status': status})
123
  return results
124
 
125
- def _process_single_void(self, patient_name, patient_prn):
126
  try:
127
- self.micro_status(f"Navigating to Void page for '{patient_name}'")
128
- self.driver.get("https://gateway.quantumepay.com/credit-card/void")
 
 
 
 
 
 
129
  search_successful = False
130
- for attempt in range(15):
131
  try:
132
  self.micro_status(f"Searching... (Attempt {attempt + 1})")
133
- WebDriverWait(self.driver, 2).until(EC.presence_of_element_located((By.XPATH, "//div[contains(@class, 'table-wrapper')]")))
134
- search_box = WebDriverWait(self.driver, 2).until(EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']")))
135
  search_box.click(); time.sleep(0.5); search_box.clear(); time.sleep(0.5)
136
  search_box.send_keys(patient_name); search_successful = True; break
137
  except Exception: time.sleep(1)
138
  if not search_successful: raise Exception("Failed to search for patient.")
139
  time.sleep(3)
 
140
  self.micro_status("Opening transaction details...")
141
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, f"//tr[contains(., \"{patient_name}\")]//button[@data-v-b6b33fa0]"))).click()
142
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.LINK_TEXT, "Transaction Detail"))).click()
 
143
  self.micro_status("Adding to Vault...")
144
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Add to Vault']"))).click()
145
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//div[@class='modal-footer']//button/span[normalize-space()='Confirm']"))).click()
 
146
  try:
147
  self.micro_status("Verifying success and saving...")
148
  company_input = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.NAME, "company_name")))
149
  company_input.clear(); company_input.send_keys(patient_name)
150
- contact_input = WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable((By.NAME, "company_contact")))
151
- contact_input.click(); contact_input.send_keys(Keys.CONTROL + "a"); contact_input.send_keys(Keys.BACK_SPACE)
152
- self.micro_status(f"Entering PRN: {patient_prn}...")
153
- contact_input.send_keys(str(patient_prn))
 
154
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Save Changes']"))).click()
155
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Confirm']]"))).click()
156
  time.sleep(5); return 'Done'
@@ -159,7 +162,7 @@ class QuantumBot:
159
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Cancel']]"))).click()
160
  return 'Bad'
161
  except Exception as e:
162
- print(f"An error occurred during VOID for {patient_name}: {e}"); return 'Error'
163
 
164
  def _get_calendar_months(self):
165
  try:
@@ -192,49 +195,41 @@ class QuantumBot:
192
  date_button = WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[contains(text(), '-')]]")))
193
  time.sleep(1); date_button.click()
194
  start_date = datetime.strptime(start_date_str, "%Y-%m-%d"); end_date = datetime.strptime(end_date_str, "%Y-%m-%d")
195
- self._select_date_in_calendar(start_date)
196
- time.sleep(1)
197
- self._select_date_in_calendar(end_date)
198
- time.sleep(1)
199
  self.driver.find_element(By.TAG_NAME, "body").click()
200
  self.micro_status("Date range set. Waiting for data to filter...")
201
  time.sleep(3)
202
 
203
- def _navigate_and_verify(self, url, verification_xpath):
204
- self.micro_status(f"Navigating and verifying page...")
205
- self.driver.get(url)
206
- time.sleep(3)
207
  try:
208
- WebDriverWait(self.driver, 15).until(EC.presence_of_element_located((By.XPATH, verification_xpath)))
209
- except TimeoutException:
210
- self.micro_status("Page did not load correctly. Refreshing...")
211
- self.driver.refresh(); time.sleep(3)
212
- WebDriverWait(self.driver, 15).until(EC.presence_of_element_located((By.XPATH, verification_xpath)))
213
- self.micro_status("Page loaded.")
214
-
215
- def _process_single_refund(self, patient_name, patient_prn):
216
- try:
217
- self.micro_status(f"Searching for '{patient_name}' in date range...")
218
- search_box = WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']")))
219
- time.sleep(1); search_box.click(); search_box.clear(); time.sleep(0.5); search_box.send_keys(patient_name)
220
- time.sleep(3)
221
 
222
- self.micro_status("Opening transaction details...")
223
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, f"//tr[contains(., \"{patient_name}\")]//button[@data-v-b6b33fa0]"))).click()
224
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.LINK_TEXT, "Transaction Detail"))).click()
225
 
226
- self.micro_status("Refunding...")
227
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Refund']"))).click()
228
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//div[@class='modal-footer']//button/span[normalize-space()='Confirm']"))).click()
229
- time.sleep(5)
230
- return 'Done'
 
 
 
 
 
231
  except Exception as e:
232
- print(f"An error occurred during REFUND for {patient_name}: {e}")
233
- return 'Error'
234
 
235
  def shutdown(self):
236
  try:
237
  if self.driver: self.driver.quit()
238
- self._kill_chrome_processes()
239
- print("[Bot Log] Chrome session closed and cleaned up.")
 
 
240
  except Exception as e: print(f"[Bot Log] Error during shutdown: {e}")
 
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
+ from webdriver_manager.chrome import ChromeDriverManager
16
 
17
  class QuantumBot:
18
  def __init__(self, socketio, app):
19
  self.socketio = socketio; self.app = app; self.driver = None
20
  self.DEFAULT_TIMEOUT = 30; self.termination_event = threading.Event()
21
+ self.download_dir = os.path.join(os.getcwd(), "temp_downloads")
 
 
 
 
 
22
 
23
  def initialize_driver(self):
24
  try:
25
+ self.micro_status("Initializing browser...")
26
+ options = ChromeOptions()
27
+ options.add_argument("--start-maximized")
28
+ options.add_argument('--disable-blink-features=AutomationControlled')
29
+ options.add_experimental_option("excludeSwitches", ["enable-automation"])
30
+ options.add_experimental_option('useAutomationExtension', False)
31
+ os.makedirs(self.download_dir, exist_ok=True)
32
+ prefs = {"download.default_directory": self.download_dir}
33
+ options.add_experimental_option("prefs", prefs)
34
+ service = ChromeService(ChromeDriverManager().install())
35
  self.driver = webdriver.Chrome(service=service, options=options)
36
  self.driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
37
  'source': "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
 
79
  if process_type == 'void':
80
  return self.process_void_list(patient_data)
81
  elif process_type == 'refund':
82
+ return self.process_refund_list(kwargs.get('start_date'), kwargs.get('end_date'))
83
  return []
84
 
85
  def process_void_list(self, patient_data):
 
87
  for index, record in enumerate(patient_data):
88
  if self.termination_event.is_set(): print("[Bot Log] Termination detected."); break
89
  patient_name = record['Name']; patient_prn = record.get('PRN', '')
90
+ self.micro_status(f"Processing VOID for '{patient_name}' ({index + 1}/{len(patient_data)})...")
91
+ status = self._process_single_void_or_refund(patient_name, patient_prn, "void")
 
 
 
 
92
  results.append({'Name': patient_name, 'PRN': patient_prn, 'Status': status})
93
  with self.app.app_context():
94
  self.socketio.emit('log_update', {'name': patient_name, 'prn': patient_prn, 'status': status})
95
  self.socketio.emit('stats_update', {'processed': index + 1, 'remaining': len(patient_data) - (index + 1), 'status': status})
96
  return results
97
 
98
+ def process_refund_list(self, start_date_str, end_date_str):
99
+ self.micro_status("Starting Refund Report Download process...")
100
+ downloaded_df = self._download_report(start_date_str, end_date_str, "refund")
101
+ if downloaded_df is None: raise Exception("Failed to download the refund report.")
102
 
103
+ patient_data = downloaded_df.to_dict('records')
104
+ self.socketio.emit('initial_stats', {'total': len(patient_data)})
105
+
106
+ results = []
107
  for index, record in enumerate(patient_data):
108
  if self.termination_event.is_set(): print("[Bot Log] Termination detected."); break
109
  patient_name = record['Name']; patient_prn = record.get('PRN', '')
110
+ self.micro_status(f"Processing REFUND for '{patient_name}' ({index + 1}/{len(patient_data)})...")
111
+ status = self._process_single_void_or_refund(patient_name, patient_prn, "refund", start_date_str, end_date_str, is_first_record=(index==0))
 
 
 
 
112
  results.append({'Name': patient_name, 'PRN': patient_prn, 'Status': status})
113
  with self.app.app_context():
114
  self.socketio.emit('log_update', {'name': patient_name, 'prn': patient_prn, 'status': status})
115
  self.socketio.emit('stats_update', {'processed': index + 1, 'remaining': len(patient_data) - (index + 1), 'status': status})
116
  return results
117
 
118
+ def _process_single_void_or_refund(self, patient_name, patient_prn, mode, start_date_str=None, end_date_str=None, is_first_record=False):
119
  try:
120
+ page_url = f"https://gateway.quantumepay.com/credit-card/{mode}"
121
+ self.micro_status(f"Navigating to {mode.title()} page for '{patient_name}'")
122
+ self.driver.get(page_url)
123
+ time.sleep(3)
124
+
125
+ if mode == 'refund' and not is_first_record:
126
+ self._set_date_range_on_page(start_date_str, end_date_str)
127
+
128
  search_successful = False
129
+ for attempt in range(5):
130
  try:
131
  self.micro_status(f"Searching... (Attempt {attempt + 1})")
132
+ WebDriverWait(self.driver, 5).until(EC.presence_of_element_located((By.XPATH, "//div[contains(@class, 'table-wrapper')]")))
133
+ search_box = WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']")))
134
  search_box.click(); time.sleep(0.5); search_box.clear(); time.sleep(0.5)
135
  search_box.send_keys(patient_name); search_successful = True; break
136
  except Exception: time.sleep(1)
137
  if not search_successful: raise Exception("Failed to search for patient.")
138
  time.sleep(3)
139
+
140
  self.micro_status("Opening transaction details...")
141
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, f"//tr[contains(., \"{patient_name}\")]//button[@data-v-b6b33fa0]"))).click()
142
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.LINK_TEXT, "Transaction Detail"))).click()
143
+
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
+
148
  try:
149
  self.micro_status("Verifying success and saving...")
150
  company_input = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.NAME, "company_name")))
151
  company_input.clear(); company_input.send_keys(patient_name)
152
+ if patient_prn and str(patient_prn).strip() and str(patient_prn).lower() != 'nan':
153
+ self.micro_status(f"Entering PRN: {patient_prn}...")
154
+ contact_input = WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable((By.NAME, "company_contact")))
155
+ contact_input.click(); contact_input.send_keys(Keys.CONTROL + "a"); contact_input.send_keys(Keys.BACK_SPACE)
156
+ contact_input.send_keys(str(patient_prn))
157
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Save Changes']"))).click()
158
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Confirm']]"))).click()
159
  time.sleep(5); return 'Done'
 
162
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Cancel']]"))).click()
163
  return 'Bad'
164
  except Exception as e:
165
+ print(f"An error occurred during {mode.upper()} for {patient_name}: {e}"); return 'Error'
166
 
167
  def _get_calendar_months(self):
168
  try:
 
195
  date_button = WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[contains(text(), '-')]]")))
196
  time.sleep(1); date_button.click()
197
  start_date = datetime.strptime(start_date_str, "%Y-%m-%d"); end_date = datetime.strptime(end_date_str, "%Y-%m-%d")
198
+ self._select_date_in_calendar(start_date); time.sleep(1)
199
+ self._select_date_in_calendar(end_date); 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 _download_report(self, start_date_str, end_date_str, mode):
 
 
 
205
  try:
206
+ self.micro_status(f"Navigating to {mode.title()} page to download report...")
207
+ self.driver.get(f"https://gateway.quantumepay.com/credit-card/{mode}")
208
+ self._set_date_range_on_page(start_date_str, end_date_str)
 
 
 
 
 
 
 
 
 
 
209
 
210
+ self.micro_status("Exporting report to CSV...")
211
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[@class='icon']]"))).click()
212
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.LINK_TEXT, "Export CSV"))).click()
213
 
214
+ self.micro_status("Waiting for download to complete...")
215
+ time.sleep(10)
216
+
217
+ files = os.listdir(self.download_dir)
218
+ if not files: raise Exception("CSV file was not downloaded.")
219
+
220
+ downloaded_file_path = os.path.join(self.download_dir, files[0])
221
+ df = pd.read_csv(downloaded_file_path)
222
+ os.remove(downloaded_file_path)
223
+ return df
224
  except Exception as e:
225
+ print(f"Error downloading report: {e}")
226
+ return None
227
 
228
  def shutdown(self):
229
  try:
230
  if self.driver: self.driver.quit()
231
+ if os.path.exists(self.download_dir):
232
+ import shutil
233
+ shutil.rmtree(self.download_dir)
234
+ print("[Bot Log] Chrome session and downloads cleaned up.")
235
  except Exception as e: print(f"[Bot Log] Error during shutdown: {e}")