sonuprasad23 commited on
Commit
0806440
·
1 Parent(s): 6de3d2d

Trying again

Browse files
Files changed (2) hide show
  1. server.py +27 -92
  2. worker.py +64 -14
server.py CHANGED
@@ -44,69 +44,10 @@ class GmailApiService:
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
-
51
- html_template = f"""
52
- <!DOCTYPE html>
53
- <html lang="en">
54
- <head>
55
- <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>{subject}</title>
56
- <style>
57
- * {{ margin: 0; padding: 0; box-sizing: border-box; }}
58
- body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #2c2c2c; background-color: #f8f8f8; }}
59
- .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; }}
60
- .header {{ background: linear-gradient(135deg, #8A0303 0%, #4c00ff 100%); color: white; padding: 40px 30px; text-align: center; }}
61
- .header h1 {{ font-size: 32px; font-weight: 700; margin-bottom: 8px; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); }}
62
- .header p {{ font-size: 18px; opacity: 0.95; font-weight: 300; letter-spacing: 1px; }}
63
- .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; }}
64
- .content {{ padding: 40px 30px; }}
65
- .report-info {{ background: #fdfdff; border-left: 6px solid var(--violet); padding: 25px; margin-bottom: 35px; border-radius: 8px; box-shadow: 0 4px 12px rgba(76, 0, 255, 0.1); }}
66
- .info-grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 15px 25px; }}
67
- .info-item {{ display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #f0f0f0; }}
68
- .stats-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 20px; margin: 35px 0; }}
69
- .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); }}
70
- .stat-number {{ font-size: 36px; font-weight: 700; color: var(--violet); margin-bottom: 10px; }}
71
- .attachments-section {{ background: #f8f5ff; border: 1px solid #e0e0e0; border-radius: 12px; padding: 25px; margin: 35px 0; }}
72
- .footer {{ background: #2c2c2c; color: white; padding: 35px 30px; text-align: center; }}
73
- h3 {{ color: var(--blood-red); margin-bottom: 20px; font-size: 20px; font-weight: 600; }}
74
- </style>
75
- </head>
76
- <body>
77
- <div class="email-container">
78
- <div class="header"><h1>Hillside's Quantum Automation</h1><p>Patient Processing Report</p></div>
79
- <div class="status-banner">Process {status_text}</div>
80
- <div class="content">
81
- <div class="report-info">
82
- <h3>Report Summary</h3>
83
- <div class="info-grid">
84
- <div class="info-item"><span><b>Report Name</b></span><span>{custom_name}</span></div>
85
- <div class="info-item"><span><b>Generated</b></span><span>{current_date}</span></div>
86
- </div>
87
- </div>
88
- <h3>Processing Results</h3>
89
- <div class="stats-grid">
90
- <div class="stat-card"> <div class="stat-number">{stats['total']}</div> <div>Total in File</div> </div>
91
- <div class="stat-card"> <div class="stat-number">{stats['processed']}</div> <div>Processed</div> </div>
92
- <div class="stat-card"> <div class="stat-number">{stats['successful']}</div> <div>Done</div> </div>
93
- <div class="stat-card"> <div class="stat-number">{stats['bad']}</div> <div>Bad State</div> </div>
94
- <div class="stat-card"> <div class="stat-number">{stats['skipped']}</div> <div>Skipped (No PRN)</div> </div>
95
- </div>
96
- <div class="attachments-section">
97
- <h3>Attached Reports</h3>
98
- <p>The following files are attached to this email and have been uploaded to Google Drive:</p>
99
- <ul>
100
- <li><b>{custom_name}_Full.csv:</b> The complete report with the final status for every patient.</li>
101
- <li><b>{custom_name}_Bad.csv:</b> A filtered list of patients that resulted in a "Bad" state.</li>
102
- <li><b>{custom_name}_Skipped.csv:</b> A filtered list of patients that were skipped due to having no PRN.</li>
103
- </ul>
104
- </div>
105
- </div>
106
- <div class="footer"><h4>Hillside Automation</h4><p>This is an automated report. Please do not reply.</p></div>
107
- </div>
108
- </body></html>
109
- """
110
  return html_template
111
 
112
  def send_report(self, recipients, subject, body, attachments=None):
@@ -160,10 +101,11 @@ def run_automation_process(session_id):
160
  global bot_instance
161
  results = []; is_terminated = False; is_crash = False
162
  try:
163
- data = session_data.get(session_id, {}); patient_data = data.get('patient_data')
 
164
  if not patient_data: raise ValueError("No patient data prepared for automation.")
165
  socketio.emit('initial_stats', {'total': len(patient_data)})
166
- results = bot_instance.process_patient_list(patient_data)
167
  is_terminated = bot_instance.termination_event.is_set()
168
  except Exception as e:
169
  print(f"[Server Log] Fatal error in automation thread: {e}"); is_crash = True
@@ -175,30 +117,22 @@ def run_automation_process(session_id):
175
  if session_id in session_data: del session_data[session_id]
176
 
177
  def generate_and_send_reports(session_id, results, is_crash_report=False, is_terminated=False):
178
- print("[Server Log] Preparing final reports...")
179
- data = session_data.get(session_id, {})
180
- if not data: print("[Server Log] Session data not found, cannot generate report."); return
181
-
182
- full_df = pd.DataFrame(data.get('patient_data_for_report'))
183
 
 
184
  if results:
185
- result_df = pd.DataFrame(results)
186
- if not result_df.empty:
187
- result_df.set_index('Name', inplace=True)
188
- full_df.set_index('Name', inplace=True)
189
- full_df.update(result_df)
190
- full_df.reset_index(inplace=True)
191
-
192
  full_df['Status'].fillna('Not Processed', inplace=True)
193
 
194
- # --- Definitive Reporting Fix: Select ONLY the required columns ---
195
  final_report_df = full_df[['Name', 'PRN', 'Status']]
196
  bad_df = final_report_df[final_report_df['Status'] == 'Bad']
197
  skipped_df = final_report_df[final_report_df['Status'] == 'Skipped - No PRN']
198
 
199
  timestamp = datetime.now().strftime("%d_%b_%Y"); custom_name = data.get('filename') or timestamp
200
  full_report_name = f"{custom_name}_Full.csv"; bad_report_name = f"{custom_name}_Bad.csv"; skipped_report_name = f"{custom_name}_Skipped.csv"
201
-
202
  full_report_content = final_report_df.to_csv(index=False)
203
  drive_service.upload_file(full_report_name, full_report_content)
204
 
@@ -207,19 +141,17 @@ def generate_and_send_reports(session_id, results, is_crash_report=False, is_ter
207
  bad_report_name: bad_df.to_csv(index=False),
208
  skipped_report_name: skipped_df.to_csv(index=False)
209
  }
210
- status_text = "terminated by user" if is_terminated else "crashed" if is_crash_report else "completed successfully"
211
 
212
  stats = {
213
- 'total': len(full_df),
214
- 'processed': len(results),
215
  'successful': len(full_df[full_df['Status'] == 'Done']),
216
- 'bad': len(bad_df),
217
- 'skipped': len(skipped_df)
218
  }
 
 
219
 
220
- subject = f"Automation Report [{status_text.upper()}]: {custom_name}"
221
-
222
- professional_body = email_service.create_professional_email_template(subject, status_text, stats, custom_name)
223
 
224
  email_service.send_report(data.get('emails'), subject, professional_body, attachments)
225
  socketio.emit('process_complete', {'message': f'Process {status_text}. Report sent.'})
@@ -234,10 +166,11 @@ def extract_patient_name(raw_name):
234
  name_only = raw_name.split('DOB')[0].strip()
235
  return re.sub(r'[:\d\-\s]+$', '', name_only).strip()
236
 
237
- @app.route('/process_files_for_automation', methods=['POST'])
238
- def handle_file_processing():
239
  session_id = 'user_session'
240
  try:
 
241
  if 'app_data' not in request.files or 'quantum_data' not in request.files:
242
  return jsonify({"error": "Both files are required."}), 400
243
 
@@ -255,11 +188,10 @@ def handle_file_processing():
255
  master_df = df_quantum.copy()
256
  master_df['Status'] = ''
257
 
258
- session_data[session_id]['patient_data_for_report'] = master_df
259
- session_data[session_id]['patient_data'] = master_df.to_dict('records')
260
 
261
  socketio.emit('data_processed')
262
- print(f"[Server Log] Data prepared. Total records: {len(master_df)}")
263
  return jsonify({"message": "Data processed successfully."})
264
  except Exception as e:
265
  print(f"[Server Log] ERROR during file processing: {e}")
@@ -273,7 +205,10 @@ def handle_connect():
273
  @socketio.on('initialize_session')
274
  def handle_init(data):
275
  session_id = 'user_session'
276
- session_data[session_id] = {'emails': data['emails'], 'filename': data['filename']}
 
 
 
277
 
278
  @socketio.on('start_login')
279
  def handle_login(credentials):
@@ -304,7 +239,7 @@ def handle_terminate():
304
 
305
  if __name__ == '__main__':
306
  print("====================================================================")
307
- print(" 🤗 Hillside Automation - Definitive Version")
308
  print(f" Frontend URL: {FRONTEND_ORIGIN}")
309
  print(f" Port: {os.getenv('PORT', 7860)}")
310
  print("====================================================================")
 
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, 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 in File</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):
 
101
  global bot_instance
102
  results = []; is_terminated = False; is_crash = False
103
  try:
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:
111
  print(f"[Server Log] Fatal error in automation thread: {e}"); is_crash = True
 
117
  if session_id in session_data: del session_data[session_id]
118
 
119
  def generate_and_send_reports(session_id, results, is_crash_report=False, is_terminated=False):
120
+ data = session_data.get(session_id, {});
121
+ if not data: print("[Server Log] Session data not found for reporting."); return
 
 
 
122
 
123
+ full_df = pd.DataFrame(data.get('patient_data_for_report'))
124
  if results:
125
+ result_df = pd.DataFrame(results).set_index('Name')
126
+ full_df.set_index('Name', inplace=True)
127
+ full_df.update(result_df); full_df.reset_index(inplace=True)
 
 
 
 
128
  full_df['Status'].fillna('Not Processed', inplace=True)
129
 
 
130
  final_report_df = full_df[['Name', 'PRN', 'Status']]
131
  bad_df = final_report_df[final_report_df['Status'] == 'Bad']
132
  skipped_df = final_report_df[final_report_df['Status'] == 'Skipped - No PRN']
133
 
134
  timestamp = datetime.now().strftime("%d_%b_%Y"); custom_name = data.get('filename') or timestamp
135
  full_report_name = f"{custom_name}_Full.csv"; bad_report_name = f"{custom_name}_Bad.csv"; skipped_report_name = f"{custom_name}_Skipped.csv"
 
136
  full_report_content = final_report_df.to_csv(index=False)
137
  drive_service.upload_file(full_report_name, full_report_content)
138
 
 
141
  bad_report_name: bad_df.to_csv(index=False),
142
  skipped_report_name: skipped_df.to_csv(index=False)
143
  }
144
+ status_text = "Terminated by User" if is_terminated else "Crashed" if is_crash_report else "Completed Successfully"
145
 
146
  stats = {
147
+ 'total': len(full_df), 'processed': len(results),
 
148
  'successful': len(full_df[full_df['Status'] == 'Done']),
149
+ 'bad': len(bad_df), 'skipped': len(skipped_df)
 
150
  }
151
+ process_type_str = data.get('mode', 'Unknown').title()
152
+ subject = f"{process_type_str} Automation Report [{status_text.upper()}]: {custom_name}"
153
 
154
+ professional_body = email_service.create_professional_email_template(subject, status_text, stats, custom_name, process_type_str)
 
 
155
 
156
  email_service.send_report(data.get('emails'), subject, professional_body, attachments)
157
  socketio.emit('process_complete', {'message': f'Process {status_text}. Report sent.'})
 
166
  name_only = raw_name.split('DOB')[0].strip()
167
  return re.sub(r'[:\d\-\s]+$', '', name_only).strip()
168
 
169
+ @app.route('/process_and_initialize', methods=['POST'])
170
+ def handle_file_processing_and_init():
171
  session_id = 'user_session'
172
  try:
173
+ data = session_data.get(session_id, {})
174
  if 'app_data' not in request.files or 'quantum_data' not in request.files:
175
  return jsonify({"error": "Both files are required."}), 400
176
 
 
188
  master_df = df_quantum.copy()
189
  master_df['Status'] = ''
190
 
191
+ data['patient_data_for_report'] = master_df
192
+ data['patient_data'] = master_df.to_dict('records')
193
 
194
  socketio.emit('data_processed')
 
195
  return jsonify({"message": "Data processed successfully."})
196
  except Exception as e:
197
  print(f"[Server Log] ERROR during file processing: {e}")
 
205
  @socketio.on('initialize_session')
206
  def handle_init(data):
207
  session_id = 'user_session'
208
+ session_data[session_id] = {
209
+ 'emails': data['emails'], 'filename': data['filename'], 'mode': data.get('mode'),
210
+ 'start_date': data.get('start_date'), 'end_date': data.get('end_date')
211
+ }
212
 
213
  @socketio.on('start_login')
214
  def handle_login(credentials):
 
239
 
240
  if __name__ == '__main__':
241
  print("====================================================================")
242
+ print(" 🤗 Hillside Automation - Definitive Multi-Workflow Platform")
243
  print(f" Frontend URL: {FRONTEND_ORIGIN}")
244
  print(f" Port: {os.getenv('PORT', 7860)}")
245
  print("====================================================================")
worker.py CHANGED
@@ -76,28 +76,51 @@ class QuantumBot:
76
  error_message = f"Error during OTP submission: {str(e)}"; print(f"[Bot Log] ERROR during OTP submission: {error_message}")
77
  return False, error_message
78
 
79
- def process_patient_list(self, patient_data):
 
 
 
 
 
 
 
 
80
  results = []
81
  for index, record in enumerate(patient_data):
82
  if self.termination_event.is_set(): print("[Bot Log] Termination detected."); break
83
  patient_name = record['Name']; patient_prn = record.get('PRN', '')
84
-
85
- # --- Definitive Conditional Logic ---
86
- if not patient_prn or not str(patient_prn).strip():
 
 
87
  self.micro_status(f"Skipping '{patient_name}' (No PRN).")
88
- status = 'Skipped - No PRN'
89
- time.sleep(0.5) # A brief pause to make the skip visible on the UI
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  else:
91
- self.micro_status(f"Processing '{patient_name}' ({index + 1}/{len(patient_data)})...")
92
- status = self._process_single_patient(patient_name, patient_prn)
93
-
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_patient(self, patient_name, patient_prn):
101
  try:
102
  self.micro_status(f"Navigating to Void page for '{patient_name}'")
103
  self.driver.get("https://gateway.quantumepay.com/credit-card/void")
@@ -122,13 +145,11 @@ class QuantumBot:
122
  self.micro_status("Verifying success and saving...")
123
  company_input = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.NAME, "company_name")))
124
  company_input.clear(); company_input.send_keys(patient_name)
125
-
126
  contact_input = WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable((By.NAME, "company_contact")))
127
  self.micro_status("Clearing Contact Name field...")
128
  contact_input.click(); contact_input.send_keys(Keys.CONTROL + "a"); contact_input.send_keys(Keys.BACK_SPACE)
129
  self.micro_status(f"Entering PRN: {patient_prn}...")
130
  contact_input.send_keys(str(patient_prn))
131
-
132
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Save Changes']"))).click()
133
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Confirm']]"))).click()
134
  time.sleep(5); return 'Done'
@@ -137,7 +158,36 @@ class QuantumBot:
137
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Cancel']]"))).click()
138
  return 'Bad'
139
  except Exception as e:
140
- print(f"An error occurred while processing {patient_name}: {e}"); return 'Error'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
142
  def shutdown(self):
143
  try:
 
76
  error_message = f"Error during OTP submission: {str(e)}"; print(f"[Bot Log] ERROR during OTP submission: {error_message}")
77
  return False, error_message
78
 
79
+ def start_processing(self, process_type, patient_data, **kwargs):
80
+ if process_type == 'void':
81
+ return self.process_void_list(patient_data)
82
+ elif process_type == 'refund':
83
+ return self.process_refund_list(patient_data, kwargs.get('start_date'), kwargs.get('end_date'))
84
+ else:
85
+ return []
86
+
87
+ def process_void_list(self, patient_data):
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
+ status = 'Skipped - No PRN'
93
+ if patient_prn and str(patient_prn).strip():
94
+ self.micro_status(f"Processing VOID for '{patient_name}' ({index + 1}/{len(patient_data)})...")
95
+ status = self._process_single_void(patient_name, patient_prn)
96
+ else:
97
  self.micro_status(f"Skipping '{patient_name}' (No PRN).")
98
+ 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, end_date):
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
+ status = 'Skipped - No PRN'
111
+ if patient_prn and str(patient_prn).strip():
112
+ self.micro_status(f"Processing REFUND for '{patient_name}' ({index + 1}/{len(patient_data)})...")
113
+ status = self._process_single_refund(patient_name, patient_prn, start_date, end_date)
114
  else:
115
+ self.micro_status(f"Skipping '{patient_name}' (No PRN).")
116
+ time.sleep(0.5)
 
117
  results.append({'Name': patient_name, 'PRN': patient_prn, 'Status': status})
118
  with self.app.app_context():
119
  self.socketio.emit('log_update', {'name': patient_name, 'prn': patient_prn, 'status': status})
120
+ self.socketio.emit('stats_update', {'processed': index + 1, 'remaining': len(patient_data) - (index + 1), 'status': status})
121
  return results
122
 
123
+ def _process_single_void(self, patient_name, patient_prn):
124
  try:
125
  self.micro_status(f"Navigating to Void page for '{patient_name}'")
126
  self.driver.get("https://gateway.quantumepay.com/credit-card/void")
 
145
  self.micro_status("Verifying success and saving...")
146
  company_input = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.NAME, "company_name")))
147
  company_input.clear(); company_input.send_keys(patient_name)
 
148
  contact_input = WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable((By.NAME, "company_contact")))
149
  self.micro_status("Clearing Contact Name field...")
150
  contact_input.click(); contact_input.send_keys(Keys.CONTROL + "a"); contact_input.send_keys(Keys.BACK_SPACE)
151
  self.micro_status(f"Entering PRN: {patient_prn}...")
152
  contact_input.send_keys(str(patient_prn))
 
153
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Save Changes']"))).click()
154
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Confirm']]"))).click()
155
  time.sleep(5); return 'Done'
 
158
  WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Cancel']]"))).click()
159
  return 'Bad'
160
  except Exception as e:
161
+ print(f"An error occurred during VOID for {patient_name}: {e}"); return 'Error'
162
+
163
+ def _process_single_refund(self, patient_name, patient_prn, start_date, end_date):
164
+ try:
165
+ self.micro_status(f"Navigating to Refund page for '{patient_name}'")
166
+ self.driver.get("https://gateway.quantumepay.com/credit-card/refund")
167
+
168
+ # This is a placeholder for your real date selection logic
169
+ # This needs to be implemented based on the actual calendar widget's HTML
170
+ self.micro_status(f"Setting date range: {start_date} to {end_date}...")
171
+ time.sleep(2)
172
+
173
+ search_successful = False
174
+ for attempt in range(15):
175
+ try:
176
+ self.micro_status(f"Searching... (Attempt {attempt + 1})")
177
+ search_box = WebDriverWait(self.driver, 2).until(EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']")))
178
+ search_box.click(); time.sleep(0.5); search_box.clear(); time.sleep(0.5)
179
+ search_box.send_keys(patient_name); search_successful = True; break
180
+ except Exception: time.sleep(1)
181
+ if not search_successful: raise Exception("Failed to search for patient in Refund.")
182
+
183
+ time.sleep(3)
184
+ # This is a placeholder for clicking the refund button
185
+ self.micro_status("Refunding transaction...")
186
+ time.sleep(2)
187
+
188
+ return "Done"
189
+ except Exception as e:
190
+ print(f"An error occurred during REFUND for {patient_name}: {e}"); return 'Error'
191
 
192
  def shutdown(self):
193
  try: