sonuprasad23 commited on
Commit
0f47fe3
·
1 Parent(s): 1675488

Almost Done

Browse files
Files changed (2) hide show
  1. server.py +65 -46
  2. worker.py +78 -82
server.py CHANGED
@@ -43,9 +43,44 @@ class GmailApiService:
43
  self.service = build('gmail', 'v1', credentials=credentials)
44
  print("[Server Log] Gmail API Service initialized successfully")
45
  except Exception as e: print(f"[Server Log] Gmail API Error: {e}")
46
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  def send_report(self, recipients, subject, body, attachments=None):
48
- # Using the provided beautiful email template logic
49
  if not self.service or not recipients: return False
50
  try:
51
  from googleapiclient.errors import HttpError
@@ -58,7 +93,7 @@ class GmailApiService:
58
  message.attach(part)
59
  raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode('utf-8')
60
  sent_message = self.service.users().messages().send(userId='me', body={'raw': raw_message}).execute()
61
- print(f"[Server Log] Email sent successfully! Message ID: {sent_message['id']}"); return True
62
  except Exception as e: print(f"[Server Log] Failed to send email: {e}"); return False
63
 
64
  class GoogleDriveService:
@@ -71,9 +106,9 @@ class GoogleDriveService:
71
  creds_json = base64.b64decode(base64_creds).decode('utf-8'); creds_dict = json.loads(creds_json)
72
  self.creds = service_account.Credentials.from_service_account_info(creds_dict, scopes=['https://www.googleapis.com/auth/drive'])
73
  self.service = build('drive', 'v3', credentials=self.creds)
74
- print("[Server Log] Google Drive Service initialized successfully.")
75
- except Exception as e: print(f"[Server Log] G-Drive ERROR: Could not initialize service: {e}")
76
-
77
  def upload_file(self, filename, file_content):
78
  if not self.service: return False
79
  try:
@@ -81,8 +116,8 @@ class GoogleDriveService:
81
  file_metadata = {'name': filename, 'parents': [self.folder_id]}
82
  media = MediaIoBaseUpload(io.BytesIO(file_content.encode('utf-8')), mimetype='text/csv', resumable=True)
83
  self.service.files().create(body=file_metadata, media_body=media, fields='id').execute()
84
- print(f"[Server Log] File '{filename}' uploaded to Google Drive."); return True
85
- except Exception as e: print(f"[Server Log] G-Drive ERROR: File upload failed: {e}"); return False
86
 
87
  email_service = GmailApiService()
88
  drive_service = GoogleDriveService()
@@ -96,17 +131,13 @@ def run_automation_process(session_id):
96
  global bot_instance
97
  results = []; is_terminated = False; is_crash = False
98
  try:
99
- data = session_data.get(session_id, {}); patient_data = data.get('patient_data')
100
- if not patient_data: raise ValueError("No patient data prepared for automation.")
101
  socketio.emit('initial_stats', {'total': len(patient_data)})
102
-
103
- if data.get('mode') == 'void':
104
  results = bot_instance.process_void_list(patient_data)
105
- elif data.get('mode') == 'refund':
106
- results = bot_instance.process_refund_list(patient_data, data['startDate'], data['endDate'])
107
- else:
108
- raise ValueError("Invalid automation mode specified.")
109
-
110
  is_terminated = bot_instance.termination_event.is_set()
111
  except Exception as e:
112
  print(f"[Server Log] Fatal error in automation thread: {e}"); is_crash = True
@@ -118,21 +149,21 @@ def run_automation_process(session_id):
118
  if session_id in session_data: del session_data[session_id]
119
 
120
  def generate_and_send_reports(session_id, results, is_crash_report=False, is_terminated=False):
121
- # This function uses the provided beautiful email template from the other script
122
- print("[Server Log] Preparing final reports...")
123
  data = session_data.get(session_id, {})
124
- if not data: print("[Server Log] Session data not found."); return
125
 
126
  full_df = pd.DataFrame(data.get('patient_data_for_report'))
127
  if results:
128
  result_df = pd.DataFrame(results)
129
  if not result_df.empty:
130
- result_df.set_index('Name', inplace=True); full_df.set_index('Name', inplace=True)
131
- full_df.update(result_df); full_df.reset_index(inplace=True)
 
 
132
  full_df['Status'].fillna('Not Processed', inplace=True)
133
 
134
  final_report_df = full_df[['Name', 'PRN', 'Status']]
135
- bad_df = final_report_df[final_report_df['Status'].isin(['Bad', 'Not Done'])]
136
  skipped_df = final_report_df[final_report_df['Status'] == 'Skipped - No PRN']
137
 
138
  timestamp = datetime.now().strftime("%d_%b_%Y"); custom_name = data.get('filename') or timestamp
@@ -147,23 +178,20 @@ def generate_and_send_reports(session_id, results, is_crash_report=False, is_ter
147
  skipped_report_name: skipped_df.to_csv(index=False)
148
  }
149
  status_text = "terminated by user" if is_terminated else "crashed due to an error" if is_crash_report else "completed successfully"
150
-
151
  stats = {
152
  'total': len(full_df), 'processed': len(results),
153
  'successful': len(full_df[full_df['Status'] == 'Done']),
154
  'bad': len(bad_df), 'skipped': len(skipped_df)
155
  }
156
  subject = f"Automation Report [{status_text.upper()}]: {custom_name}"
157
-
158
- # Using the provided beautiful email template logic
159
  professional_body = email_service.create_professional_email_template(subject, status_text, stats, custom_name)
160
-
161
  email_service.send_report(data.get('emails'), subject, professional_body, attachments)
162
  socketio.emit('process_complete', {'message': f'Process {status_text}. Report sent.'})
163
 
164
  @app.route('/')
165
  def status_page():
166
- return "<h1>Hillside Automation Backend is running.</h1>"
 
167
 
168
  def extract_patient_name(raw_name):
169
  if not isinstance(raw_name, str): return ""
@@ -171,11 +199,11 @@ def extract_patient_name(raw_name):
171
  return re.sub(r'[:\d\-\s]+$', '', name_only).strip()
172
 
173
  @app.route('/process_files_for_automation', methods=['POST'])
174
- def handle_void_file_processing():
175
  session_id = 'user_session'
176
  try:
177
  if 'app_data' not in request.files or 'quantum_data' not in request.files:
178
- return jsonify({"error": "Both files are required for Void process."}), 400
179
 
180
  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'])
181
  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'])
@@ -190,12 +218,14 @@ def handle_void_file_processing():
190
  master_df['PRN'] = master_df['Name'].apply(lambda name: prn_lookup_dict.get(name, ""))
191
  master_df['Status'] = ''
192
 
193
- session_data[session_id]['patient_data_for_report'] = master_df
194
  session_data[session_id]['patient_data'] = master_df.to_dict('records')
195
 
196
  socketio.emit('data_processed')
197
- return jsonify({"message": "Void data processed successfully."})
 
198
  except Exception as e:
 
199
  return jsonify({"error": str(e)}), 500
200
 
201
  @socketio.on('connect')
@@ -206,19 +236,8 @@ def handle_connect():
206
  @socketio.on('initialize_session')
207
  def handle_init(data):
208
  session_id = 'user_session'
209
- session_data[session_id] = data
210
- if data.get('mode') == 'refund':
211
- try:
212
- file_content = base64.b64decode(data['fileContent']).decode('utf-8')
213
- df = pd.read_csv(io.StringIO(file_content))
214
- if 'Name' not in df.columns: raise ValueError("Refund file must contain 'Name' column.")
215
- if 'PRN' not in df.columns: df['PRN'] = ''
216
- session_data[session_id]['patient_data'] = df.to_dict('records')
217
- session_data[session_id]['patient_data_for_report'] = df
218
- except Exception as e:
219
- socketio.emit('error', {'message': f"Error processing refund file: {e}"})
220
- return
221
-
222
  @socketio.on('start_login')
223
  def handle_login(credentials):
224
  global bot_instance
@@ -248,7 +267,7 @@ def handle_terminate():
248
 
249
  if __name__ == '__main__':
250
  print("====================================================================")
251
- print(" 🤗 Hillside Automation - Definitive Multi-Workflow")
252
  print(f" Frontend URL: {FRONTEND_ORIGIN}")
253
  print(f" Port: {os.getenv('PORT', 7860)}")
254
  print("====================================================================")
 
43
  self.service = build('gmail', 'v1', credentials=credentials)
44
  print("[Server Log] Gmail API Service initialized successfully")
45
  except Exception as e: print(f"[Server Log] Gmail API Error: {e}")
46
+
47
+ def create_professional_email_template(self, subject, status_text, stats, custom_name):
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"""
51
+ <!DOCTYPE html>
52
+ <html><head><title>{subject}</title><style>
53
+ body{{font-family: 'Segoe UI', sans-serif; background-color: #f8f8f8; color: #2c2c2c;}}
54
+ .email-container{{max-width: 700px; margin: 20px auto; background-color: #ffffff; border-radius: 12px; overflow: hidden; box-shadow: 0 8px 32px rgba(139, 0, 0, 0.15);}}
55
+ .header{{background: linear-gradient(135deg, #8A0303 0%, #4c00ff 100%); color: white; padding: 40px 30px; text-align: center;}}
56
+ .header h1{{font-size: 32px;}} .header p{{font-size: 18px; opacity: 0.95;}}
57
+ .status-banner{{background: {status_color}; color: white; text-align: center; padding: 18px; font-size: 16px; font-weight: 600; text-transform: uppercase;}}
58
+ .content{{padding: 40px 30px;}} h3{{color: #8A0303; margin-bottom: 20px; font-size: 20px;}}
59
+ .stats-grid{{display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 20px; margin: 35px 0;}}
60
+ .stat-card{{background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 12px; padding: 25px 15px; text-align: center;}}
61
+ .stat-number{{font-size: 36px; font-weight: 700; color: #4c00ff; margin-bottom: 10px;}}
62
+ .attachments-section{{background: #f8f5ff; border: 1px solid #e0e0e0; border-radius: 12px; padding: 25px; margin: 35px 0;}}
63
+ .footer{{background: #2c2c2c; color: white; padding: 35px 30px; text-align: center;}}
64
+ </style></head>
65
+ <body><div class="email-container"><div class="header"><h1>Hillside's Quantum Automation</h1><p>Patient Processing Report</p></div>
66
+ <div class="status-banner">Process {status_text}</div>
67
+ <div class="content"><h3>Processing Results</h3>
68
+ <div class="stats-grid">
69
+ <div class="stat-card"><div class="stat-number">{stats['total']}</div><div>Total in File</div></div>
70
+ <div class="stat-card"><div class="stat-number">{stats['processed']}</div><div>Attempted</div></div>
71
+ <div class="stat-card"><div class="stat-number" style="color: var(--success);">{stats['successful']}</div><div>Done</div></div>
72
+ <div class="stat-card"><div class="stat-number" style="color: var(--warning);">{stats['bad']}</div><div>Bad State</div></div>
73
+ <div class="stat-card"><div class="stat-number" style="color: var(--skipped);">{stats['skipped']}</div><div>Skipped</div></div>
74
+ </div>
75
+ <div class="attachments-section"><h3>Attached Reports</h3>
76
+ <ul><li><b>{custom_name}_Full.csv:</b> The complete report with the final status for every patient.</li>
77
+ <li><b>{custom_name}_Bad.csv:</b> A filtered list of patients that resulted in a "Bad" state.</li>
78
+ <li><b>{custom_name}_Skipped.csv:</b> A filtered list of patients that were skipped due to having no PRN.</li></ul></div></div>
79
+ <div class="footer"><p>&copy; {datetime.now().year} Hillside Automation. This is an automated report.</p></div></div></body></html>
80
+ """
81
+ return html_template
82
+
83
  def send_report(self, recipients, subject, body, attachments=None):
 
84
  if not self.service or not recipients: return False
85
  try:
86
  from googleapiclient.errors import HttpError
 
93
  message.attach(part)
94
  raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode('utf-8')
95
  sent_message = self.service.users().messages().send(userId='me', body={'raw': raw_message}).execute()
96
+ print(f"[Server Log] Email sent! ID: {sent_message['id']}"); return True
97
  except Exception as e: print(f"[Server Log] Failed to send email: {e}"); return False
98
 
99
  class GoogleDriveService:
 
106
  creds_json = base64.b64decode(base64_creds).decode('utf-8'); creds_dict = json.loads(creds_json)
107
  self.creds = service_account.Credentials.from_service_account_info(creds_dict, scopes=['https://www.googleapis.com/auth/drive'])
108
  self.service = build('drive', 'v3', credentials=self.creds)
109
+ print("[Server Log] Google Drive Service initialized.")
110
+ except Exception as e: print(f"[Server Log] G-Drive ERROR: {e}")
111
+
112
  def upload_file(self, filename, file_content):
113
  if not self.service: return False
114
  try:
 
116
  file_metadata = {'name': filename, 'parents': [self.folder_id]}
117
  media = MediaIoBaseUpload(io.BytesIO(file_content.encode('utf-8')), mimetype='text/csv', resumable=True)
118
  self.service.files().create(body=file_metadata, media_body=media, fields='id').execute()
119
+ print(f"[Server Log] File '{filename}' uploaded to G-Drive."); return True
120
+ except Exception as e: print(f"[Server Log] G-Drive ERROR: {e}"); return False
121
 
122
  email_service = GmailApiService()
123
  drive_service = GoogleDriveService()
 
131
  global bot_instance
132
  results = []; is_terminated = False; is_crash = False
133
  try:
134
+ data = session_data.get(session_id, {}); patient_data = data.get('patient_data'); workflow = data.get('workflow')
135
+ if not patient_data: raise ValueError("No patient data prepared.")
136
  socketio.emit('initial_stats', {'total': len(patient_data)})
137
+ if workflow == 'void':
 
138
  results = bot_instance.process_void_list(patient_data)
139
+ elif workflow == 'refund':
140
+ results = bot_instance.process_refund_list(patient_data, data.get('start_date'), data.get('end_date'))
 
 
 
141
  is_terminated = bot_instance.termination_event.is_set()
142
  except Exception as e:
143
  print(f"[Server Log] Fatal error in automation thread: {e}"); is_crash = True
 
149
  if session_id in session_data: del session_data[session_id]
150
 
151
  def generate_and_send_reports(session_id, results, is_crash_report=False, is_terminated=False):
 
 
152
  data = session_data.get(session_id, {})
153
+ if not data: return
154
 
155
  full_df = pd.DataFrame(data.get('patient_data_for_report'))
156
  if results:
157
  result_df = pd.DataFrame(results)
158
  if not result_df.empty:
159
+ result_df.set_index('Name', inplace=True)
160
+ full_df.set_index('Name', inplace=True)
161
+ full_df.update(result_df)
162
+ full_df.reset_index(inplace=True)
163
  full_df['Status'].fillna('Not Processed', inplace=True)
164
 
165
  final_report_df = full_df[['Name', 'PRN', 'Status']]
166
+ bad_df = final_report_df[final_report_df['Status'] == 'Bad']
167
  skipped_df = final_report_df[final_report_df['Status'] == 'Skipped - No PRN']
168
 
169
  timestamp = datetime.now().strftime("%d_%b_%Y"); custom_name = data.get('filename') or timestamp
 
178
  skipped_report_name: skipped_df.to_csv(index=False)
179
  }
180
  status_text = "terminated by user" if is_terminated else "crashed due to an error" if is_crash_report else "completed successfully"
 
181
  stats = {
182
  'total': len(full_df), 'processed': len(results),
183
  'successful': len(full_df[full_df['Status'] == 'Done']),
184
  'bad': len(bad_df), 'skipped': len(skipped_df)
185
  }
186
  subject = f"Automation Report [{status_text.upper()}]: {custom_name}"
 
 
187
  professional_body = email_service.create_professional_email_template(subject, status_text, stats, custom_name)
 
188
  email_service.send_report(data.get('emails'), subject, professional_body, attachments)
189
  socketio.emit('process_complete', {'message': f'Process {status_text}. Report sent.'})
190
 
191
  @app.route('/')
192
  def status_page():
193
+ 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>"""
194
+ return Response(APP_STATUS_HTML)
195
 
196
  def extract_patient_name(raw_name):
197
  if not isinstance(raw_name, str): return ""
 
199
  return re.sub(r'[:\d\-\s]+$', '', name_only).strip()
200
 
201
  @app.route('/process_files_for_automation', methods=['POST'])
202
+ def handle_file_processing():
203
  session_id = 'user_session'
204
  try:
205
  if 'app_data' not in request.files or 'quantum_data' not in request.files:
206
+ return jsonify({"error": "Both files are required."}), 400
207
 
208
  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'])
209
  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'])
 
218
  master_df['PRN'] = master_df['Name'].apply(lambda name: prn_lookup_dict.get(name, ""))
219
  master_df['Status'] = ''
220
 
221
+ session_data[session_id]['patient_data_for_report'] = master_df.copy()
222
  session_data[session_id]['patient_data'] = master_df.to_dict('records')
223
 
224
  socketio.emit('data_processed')
225
+ print(f"[Server Log] Data prepared. Total records: {len(master_df)}")
226
+ return jsonify({"message": "Data processed successfully."})
227
  except Exception as e:
228
+ print(f"[Server Log] ERROR during file processing: {e}")
229
  return jsonify({"error": str(e)}), 500
230
 
231
  @socketio.on('connect')
 
236
  @socketio.on('initialize_session')
237
  def handle_init(data):
238
  session_id = 'user_session'
239
+ session_data[session_id] = {'emails': data['emails'], 'filename': data['filename'], 'workflow': data['workflow'], 'start_date': data.get('start_date'), 'end_date': data.get('end_date')}
240
+
 
 
 
 
 
 
 
 
 
 
 
241
  @socketio.on('start_login')
242
  def handle_login(credentials):
243
  global bot_instance
 
267
 
268
  if __name__ == '__main__':
269
  print("====================================================================")
270
+ print(" 🤗 Hillside Automation - Definitive Dual-Workflow Version")
271
  print(f" Frontend URL: {FRONTEND_ORIGIN}")
272
  print(f" Port: {os.getenv('PORT', 7860)}")
273
  print("====================================================================")
worker.py CHANGED
@@ -2,6 +2,7 @@ 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
@@ -11,7 +12,6 @@ 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):
@@ -36,12 +36,10 @@ class QuantumBot:
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})"
41
- })
42
  return True, None
43
  except Exception as e:
44
- error_message = f"Message: {str(e)}"; print(f"CRITICAL ERROR in WebDriver Initialization: {error_message}")
45
  return False, error_message
46
 
47
  def micro_status(self, message):
@@ -63,7 +61,7 @@ class QuantumBot:
63
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "code1")))
64
  return True, None
65
  except Exception as e:
66
- error_message = f"Error during login: {str(e)}"; print(f"[Bot Log] ERROR during login: {error_message}")
67
  return False, error_message
68
 
69
  def submit_otp(self, otp):
@@ -75,37 +73,63 @@ class QuantumBot:
75
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//span[text()='Payments']")))
76
  return True, None
77
  except Exception as e:
78
- error_message = f"Error during OTP submission: {str(e)}"; print(f"[Bot Log] ERROR during OTP submission: {error_message}")
79
  return False, error_message
80
-
81
- def process_void_list(self, patient_data):
82
- return self._process_list(patient_data, self._process_single_void_patient)
83
 
84
- def process_refund_list(self, patient_data, start_date, end_date):
85
- return self._process_list(patient_data, self._process_single_refund_patient, start_date=start_date, end_date=end_date)
 
 
 
86
 
87
- def _process_list(self, patient_data, processing_function, **kwargs):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  results = []
89
  for index, record in enumerate(patient_data):
90
  if self.termination_event.is_set(): print("[Bot Log] Termination detected."); break
91
  patient_name = record['Name']; patient_prn = record.get('PRN', '')
92
- self.micro_status(f"Processing '{patient_name}' ({index + 1}/{len(patient_data)})...")
93
- status = processing_function(patient_name, patient_prn, **kwargs)
 
 
 
 
94
  results.append({'Name': patient_name, 'PRN': patient_prn, 'Status': status})
95
  with self.app.app_context():
96
  self.socketio.emit('log_update', {'name': patient_name, 'prn': patient_prn, 'status': status})
97
  self.socketio.emit('stats_update', {'processed': len(results), 'remaining': len(patient_data) - len(results), 'status': status})
98
  return results
99
 
100
- def _process_single_void_patient(self, patient_name, patient_prn):
101
- if not patient_prn or not str(patient_prn).strip():
102
- self.micro_status(f"Skipping '{patient_name}' (No PRN).")
103
- time.sleep(0.5)
104
- return 'Skipped - No PRN'
105
  try:
106
  self.micro_status(f"Navigating to Void page for '{patient_name}'")
107
  self.driver.get("https://gateway.quantumepay.com/credit-card/void")
108
- # ... (The rest of the robust void logic goes here) ...
109
  search_successful = False
110
  for attempt in range(15):
111
  try:
@@ -128,6 +152,7 @@ class QuantumBot:
128
  company_input = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.NAME, "company_name")))
129
  company_input.clear(); company_input.send_keys(patient_name)
130
  contact_input = WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable((By.NAME, "company_contact")))
 
131
  contact_input.click(); contact_input.send_keys(Keys.CONTROL + "a"); contact_input.send_keys(Keys.BACK_SPACE)
132
  self.micro_status(f"Entering PRN: {patient_prn}...")
133
  contact_input.send_keys(str(patient_prn))
@@ -141,16 +166,33 @@ class QuantumBot:
141
  except Exception as e:
142
  print(f"An error occurred while processing {patient_name}: {e}"); return 'Error'
143
 
144
- def _process_single_refund_patient(self, patient_name, patient_prn, start_date, end_date):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  try:
146
  self.micro_status(f"Navigating to Refund page for '{patient_name}'")
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, end_date)
149
- # ... (The rest of the robust refund logic from your script goes here) ...
150
  self.micro_status(f"Searching for '{patient_name}'...")
151
  search_box = WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']")))
152
- time.sleep(1); search_box.click(); search_box.clear(); time.sleep(0.5); search_box.send_keys(patient_name)
153
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.XPATH, f"//tr[contains(., \"{patient_name}\")]")))
 
154
  self.micro_status("Opening transaction details...")
155
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, f"//tr[contains(., \"{patient_name}\")]//button[@data-v-b6b33fa0]"))).click()
156
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.LINK_TEXT, "Transaction Detail"))).click()
@@ -161,67 +203,21 @@ class QuantumBot:
161
  self.micro_status("Verifying success and saving...")
162
  company_input = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.NAME, "company_name")))
163
  company_input.clear(); company_input.send_keys(patient_name)
164
- if patient_prn and str(patient_prn).strip() and str(patient_prn).lower() != 'nan':
165
- contact_input = WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable((By.NAME, "company_contact")))
166
- if contact_input.get_attribute('value') == 'N/A': contact_input.clear()
167
- contact_input.send_keys(str(patient_prn))
 
168
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Save Changes']"))).click()
169
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Confirm']]"))).click()
170
- time.sleep(2)
171
- return 'Done'
172
  except TimeoutException:
173
  self.micro_status(f"'{patient_name}' is in a bad state, cancelling.")
174
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Cancel']]"))).click()
175
- return 'Not Done'
176
  except Exception as e:
177
- print(f"An error occurred while processing {patient_name}: {e}")
178
- self.micro_status(f"An error occurred for '{patient_name}'. Retrying...")
179
- return 'RETRY' # This special status is not used in the final version but is good practice
180
-
181
- def _get_calendar_months(self):
182
- try:
183
- titles = self.driver.find_elements(By.XPATH, "//div[contains(@class, 'vc-title')]")
184
- return [datetime.strptime(title.text, "%B %Y") for title in titles]
185
- except: return []
186
-
187
- def _select_date_in_calendar(self, target_date):
188
- target_month_str = target_date.strftime("%B %Y")
189
- self.micro_status(f"Navigating calendar to {target_month_str}...")
190
- for _ in range(24):
191
- visible_months = self._get_calendar_months()
192
- if any(d.strftime("%B %Y") == target_month_str for d in visible_months):
193
- day_format = "%#d" if os.name == 'nt' else "%-d"
194
- day_xpath = f"//span[@aria-label='{target_date.strftime(f'%A, %B {day_format}, %Y')}']"
195
- day_element = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located((By.XPATH, day_xpath)))
196
- self.driver.execute_script("arguments[0].click();", day_element)
197
- return
198
- arrow_direction = 'is-left' if target_date < visible_months[0] else 'is-right'
199
- self.driver.find_element(By.XPATH, f"//div[contains(@class, 'vc-arrow') and contains(@class, '{arrow_direction}')]").click()
200
- time.sleep(0.5)
201
- raise Exception(f"Could not navigate to date {target_date.strftime('%Y-%m-%d')}")
202
-
203
- def _set_date_range_on_page(self, start_date_str, end_date_str):
204
- self.micro_status("Opening calendar to set date range...")
205
- date_button = WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[contains(text(), '-')]]")))
206
- time.sleep(1); date_button.click()
207
- start_date = datetime.strptime(start_date_str, "%Y-%m-%d"); end_date = datetime.strptime(end_date_str, "%Y-%m-%d")
208
- self._select_date_in_calendar(start_date)
209
- time.sleep(1)
210
- self._select_date_in_calendar(end_date)
211
- time.sleep(1)
212
- self.driver.find_element(By.TAG_NAME, "body").click()
213
- self.micro_status("Date range set.")
214
- time.sleep(2)
215
-
216
- def _navigate_and_verify(self, url, verification_xpath):
217
- self.micro_status(f"Navigating to page...")
218
- self.driver.get(url)
219
- time.sleep(3)
220
- try: WebDriverWait(self.driver, 15).until(EC.element_to_be_clickable((By.XPATH, verification_xpath)))
221
- except TimeoutException:
222
- self.micro_status("Page not ready, refreshing..."); self.driver.refresh(); time.sleep(3)
223
- WebDriverWait(self.driver, 15).until(EC.element_to_be_clickable((By.XPATH, verification_xpath)))
224
-
225
  def shutdown(self):
226
  try:
227
  if self.driver: self.driver.quit()
 
2
  import threading
3
  import subprocess
4
  import pandas as pd
5
+ import os
6
  from selenium import webdriver
7
  from selenium.webdriver.chrome.service import Service as ChromeService
8
  from selenium.webdriver.chrome.options import Options as ChromeOptions
 
12
  from selenium.webdriver.support import expected_conditions as EC
13
  from selenium.common.exceptions import TimeoutException
14
  from datetime import datetime
 
15
 
16
  class QuantumBot:
17
  def __init__(self, socketio, app):
 
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', {'source': "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"})
 
 
40
  return True, None
41
  except Exception as e:
42
+ error_message = f"Message: {str(e)}"; print(f"CRITICAL ERROR: {error_message}")
43
  return False, error_message
44
 
45
  def micro_status(self, message):
 
61
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "code1")))
62
  return True, None
63
  except Exception as e:
64
+ error_message = f"Error during login: {str(e)}"; print(f"[Bot Log] ERROR: {error_message}")
65
  return False, error_message
66
 
67
  def submit_otp(self, otp):
 
73
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//span[text()='Payments']")))
74
  return True, None
75
  except Exception as e:
76
+ error_message = f"Error during OTP submission: {str(e)}"; print(f"[Bot Log] ERROR: {error_message}")
77
  return False, error_message
 
 
 
78
 
79
+ def _get_calendar_months(self):
80
+ try:
81
+ titles = self.driver.find_elements(By.XPATH, "//div[contains(@class, 'vc-title')]")
82
+ return [datetime.strptime(title.text, "%B %Y") for title in titles]
83
+ except Exception: return []
84
 
85
+ def _select_date_in_calendar(self, target_date):
86
+ target_month_str = target_date.strftime("%B %Y")
87
+ self.micro_status(f"Navigating calendar to {target_month_str}...")
88
+ for _ in range(24):
89
+ visible_months = self._get_calendar_months()
90
+ if any(d.strftime("%B %Y") == target_month_str for d in visible_months):
91
+ self.micro_status(f"Found month. Selecting day {target_date.day}.")
92
+ day_format = "%#d" if os.name == 'nt' else "%-d"
93
+ expected_aria_label = target_date.strftime(f"%A, %B {day_format}, %Y")
94
+ day_xpath = f"//span[@aria-label='{expected_aria_label}']"
95
+ day_element = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located((By.XPATH, day_xpath)))
96
+ self.driver.execute_script("arguments[0].click();", day_element); return
97
+ if target_date < visible_months[0]: self.driver.find_element(By.XPATH, "//div[contains(@class, 'vc-arrow') and contains(@class, 'is-left')]").click()
98
+ else: self.driver.find_element(By.XPATH, "//div[contains(@class, 'vc-arrow') and contains(@class, 'is-right')]").click()
99
+ time.sleep(0.5)
100
+ raise Exception(f"Could not navigate to date {target_date.strftime('%Y-%m-%d')}")
101
+
102
+ def _set_date_range(self, start_date_str, end_date_str):
103
+ self.micro_status("Opening calendar to set date range...")
104
+ date_button = WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[contains(text(), '-')]]")))
105
+ time.sleep(1); date_button.click()
106
+ start_date = datetime.strptime(start_date_str, "%Y-%m-%d"); end_date = datetime.strptime(end_date_str, "%Y-%m-%d")
107
+ self._select_date_in_calendar(start_date); time.sleep(1)
108
+ self._select_date_in_calendar(end_date); time.sleep(1)
109
+ self.driver.find_element(By.TAG_NAME, "body").click() # Click away to close calendar
110
+ time.sleep(3)
111
+
112
+ def process_void_list(self, patient_data):
113
  results = []
114
  for index, record in enumerate(patient_data):
115
  if self.termination_event.is_set(): print("[Bot Log] Termination detected."); break
116
  patient_name = record['Name']; patient_prn = record.get('PRN', '')
117
+ if not patient_prn or not str(patient_prn).strip():
118
+ self.micro_status(f"Skipping '{patient_name}' (No PRN).")
119
+ status = 'Skipped - No PRN'; time.sleep(0.5)
120
+ else:
121
+ self.micro_status(f"Processing '{patient_name}' ({index + 1}/{len(patient_data)})...")
122
+ status = self._process_single_void(patient_name, patient_prn)
123
  results.append({'Name': patient_name, 'PRN': patient_prn, 'Status': status})
124
  with self.app.app_context():
125
  self.socketio.emit('log_update', {'name': patient_name, 'prn': patient_prn, 'status': status})
126
  self.socketio.emit('stats_update', {'processed': len(results), 'remaining': len(patient_data) - len(results), 'status': status})
127
  return results
128
 
129
+ def _process_single_void(self, patient_name, patient_prn):
 
 
 
 
130
  try:
131
  self.micro_status(f"Navigating to Void page for '{patient_name}'")
132
  self.driver.get("https://gateway.quantumepay.com/credit-card/void")
 
133
  search_successful = False
134
  for attempt in range(15):
135
  try:
 
152
  company_input = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.NAME, "company_name")))
153
  company_input.clear(); company_input.send_keys(patient_name)
154
  contact_input = WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable((By.NAME, "company_contact")))
155
+ self.micro_status("Clearing Contact Name field...")
156
  contact_input.click(); contact_input.send_keys(Keys.CONTROL + "a"); contact_input.send_keys(Keys.BACK_SPACE)
157
  self.micro_status(f"Entering PRN: {patient_prn}...")
158
  contact_input.send_keys(str(patient_prn))
 
166
  except Exception as e:
167
  print(f"An error occurred while processing {patient_name}: {e}"); return 'Error'
168
 
169
+ def process_refund_list(self, patient_data, start_date, end_date):
170
+ results = []
171
+ for index, record in enumerate(patient_data):
172
+ if self.termination_event.is_set(): print("[Bot Log] Termination detected."); break
173
+ patient_name = record['Name']; patient_prn = record.get('PRN', '')
174
+ if not patient_prn or not str(patient_prn).strip():
175
+ self.micro_status(f"Skipping '{patient_name}' (No PRN).")
176
+ status = 'Skipped - No PRN'; time.sleep(0.5)
177
+ else:
178
+ self.micro_status(f"Processing '{patient_name}' ({index + 1}/{len(patient_data)})...")
179
+ status = self._process_single_refund(patient_name, patient_prn, start_date, end_date)
180
+ results.append({'Name': patient_name, 'PRN': patient_prn, 'Status': status})
181
+ with self.app.app_context():
182
+ self.socketio.emit('log_update', {'name': patient_name, 'prn': patient_prn, 'status': status})
183
+ self.socketio.emit('stats_update', {'processed': len(results), 'remaining': len(patient_data) - len(results), 'status': status})
184
+ return results
185
+
186
+ def _process_single_refund(self, patient_name, patient_prn, start_date, end_date):
187
  try:
188
  self.micro_status(f"Navigating to Refund page for '{patient_name}'")
189
+ self.driver.get("https://gateway.quantumepay.com/credit-card/refund")
190
+ self._set_date_range(start_date, end_date)
 
191
  self.micro_status(f"Searching for '{patient_name}'...")
192
  search_box = WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']")))
193
+ search_box.click(); time.sleep(0.5); search_box.clear(); time.sleep(0.5)
194
+ search_box.send_keys(patient_name)
195
+ time.sleep(3)
196
  self.micro_status("Opening transaction details...")
197
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, f"//tr[contains(., \"{patient_name}\")]//button[@data-v-b6b33fa0]"))).click()
198
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.LINK_TEXT, "Transaction Detail"))).click()
 
203
  self.micro_status("Verifying success and saving...")
204
  company_input = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.NAME, "company_name")))
205
  company_input.clear(); company_input.send_keys(patient_name)
206
+ contact_input = WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable((By.NAME, "company_contact")))
207
+ self.micro_status("Clearing Contact Name field...")
208
+ contact_input.click(); contact_input.send_keys(Keys.CONTROL + "a"); contact_input.send_keys(Keys.BACK_SPACE)
209
+ self.micro_status(f"Entering PRN: {patient_prn}...")
210
+ contact_input.send_keys(str(patient_prn))
211
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Save Changes']"))).click()
212
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Confirm']]"))).click()
213
+ time.sleep(5); return 'Done'
 
214
  except TimeoutException:
215
  self.micro_status(f"'{patient_name}' is in a bad state, cancelling.")
216
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Cancel']]"))).click()
217
+ return 'Bad'
218
  except Exception as e:
219
+ print(f"An error occurred while processing {patient_name}: {e}"); return 'Error'
220
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  def shutdown(self):
222
  try:
223
  if self.driver: self.driver.quit()