sonuprasad23 commited on
Commit
ca62807
·
1 Parent(s): f03221e

Added new feature

Browse files
Files changed (2) hide show
  1. server.py +111 -562
  2. worker.py +57 -137
server.py CHANGED
@@ -7,8 +7,9 @@ import threading
7
  import os
8
  import base64
9
  import json
 
10
  from datetime import datetime
11
- from flask import Flask, Response
12
  from flask_socketio import SocketIO, emit
13
  from flask_cors import CORS
14
  from worker import QuantumBot
@@ -22,658 +23,206 @@ load_dotenv()
22
 
23
  app = Flask(__name__)
24
  app.config['SECRET_KEY'] = 'secret-key-for-hillside-automation'
25
-
26
  FRONTEND_ORIGIN = os.getenv('FRONTEND_URL', 'https://quantbot.netlify.app')
27
- CORS(app, resources={r"/*": {"origins": [FRONTEND_ORIGIN, "http://localhost:3000", "http://127.0.0.1:5500"]}})
28
- socketio = SocketIO(app, cors_allowed_origins=[FRONTEND_ORIGIN, "http://localhost:3000", "http://127.0.0.1:5500"], async_mode='eventlet')
29
 
30
  bot_instance = None
31
  session_data = {}
32
 
33
  class GmailApiService:
34
  def __init__(self):
35
- self.sender_email = os.getenv('EMAIL_SENDER')
36
- self.service = None
37
-
38
  try:
39
- from google.oauth2 import service_account
40
- from googleapiclient.discovery import build
41
-
42
  base64_creds = os.getenv('GDRIVE_SA_KEY_BASE64')
43
- if not base64_creds:
44
- print("[Gmail API] WARNING: GDRIVE_SA_KEY_BASE64 not found.")
45
- return
46
-
47
- creds_json = base64.b64decode(base64_creds).decode('utf-8')
48
- creds_dict = json.loads(creds_json)
49
-
50
- credentials = service_account.Credentials.from_service_account_info(
51
- creds_dict,
52
- scopes=['https://www.googleapis.com/auth/gmail.send']
53
- )
54
-
55
- if self.sender_email:
56
- credentials = credentials.with_subject(self.sender_email)
57
-
58
  self.service = build('gmail', 'v1', credentials=credentials)
59
  print("[Gmail API] Service initialized successfully")
60
-
61
- except Exception as e:
62
- print(f"[Gmail API] Error: {e}")
63
-
64
- def create_professional_email_template(self, subject, status_text, stats, custom_name):
65
- """Create a professional blood red, white & violet themed email template"""
66
-
67
- status_color = "#8B0000" if "successfully" in status_text else "#DC143C" if "error" in status_text else "#8A2BE2"
68
- current_date = datetime.now().strftime("%B %d, %Y at %I:%M %p")
69
-
70
- html_template = f"""
71
- <!DOCTYPE html>
72
- <html lang="en">
73
- <head>
74
- <meta charset="UTF-8">
75
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
76
- <title>{subject}</title>
77
- <style>
78
- * {{
79
- margin: 0;
80
- padding: 0;
81
- box-sizing: border-box;
82
- }}
83
-
84
- body {{
85
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
86
- line-height: 1.6;
87
- color: #2c2c2c;
88
- background-color: #f8f8f8;
89
- }}
90
-
91
- .email-container {{
92
- max-width: 700px;
93
- margin: 20px auto;
94
- background-color: #ffffff;
95
- box-shadow: 0 8px 32px rgba(139, 0, 0, 0.15);
96
- border-radius: 12px;
97
- overflow: hidden;
98
- }}
99
-
100
- .header {{
101
- background: linear-gradient(135deg, #8B0000 0%, #DC143C 50%, #8A2BE2 100%);
102
- color: white;
103
- padding: 40px 30px;
104
- text-align: center;
105
- position: relative;
106
- }}
107
-
108
- .header::before {{
109
- content: '';
110
- position: absolute;
111
- top: 0;
112
- left: 0;
113
- right: 0;
114
- bottom: 0;
115
- background: rgba(255, 255, 255, 0.1);
116
- backdrop-filter: blur(10px);
117
- }}
118
-
119
- .header-content {{
120
- position: relative;
121
- z-index: 2;
122
- }}
123
-
124
- .header h1 {{
125
- font-size: 32px;
126
- font-weight: 700;
127
- margin-bottom: 8px;
128
- text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
129
- }}
130
-
131
- .header p {{
132
- font-size: 18px;
133
- opacity: 0.95;
134
- font-weight: 300;
135
- letter-spacing: 1px;
136
- }}
137
-
138
- .status-banner {{
139
- background: {status_color};
140
- color: white;
141
- text-align: center;
142
- padding: 18px;
143
- font-size: 16px;
144
- font-weight: 600;
145
- text-transform: uppercase;
146
- letter-spacing: 1.5px;
147
- box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2);
148
- }}
149
-
150
- .content {{
151
- padding: 40px 30px;
152
- }}
153
-
154
- .report-info {{
155
- background: linear-gradient(135deg, #f8f8f8 0%, #ffffff 100%);
156
- border-left: 6px solid #8A2BE2;
157
- padding: 25px;
158
- margin-bottom: 35px;
159
- border-radius: 8px;
160
- box-shadow: 0 4px 12px rgba(138, 43, 226, 0.1);
161
- }}
162
-
163
- .report-info h3 {{
164
- color: #8B0000;
165
- margin-bottom: 20px;
166
- font-size: 20px;
167
- font-weight: 600;
168
- }}
169
-
170
- .info-grid {{
171
- display: grid;
172
- grid-template-columns: 1fr 1fr;
173
- gap: 20px;
174
- margin-bottom: 20px;
175
- }}
176
-
177
- .info-item {{
178
- display: flex;
179
- justify-content: space-between;
180
- align-items: center;
181
- padding: 15px 0;
182
- border-bottom: 2px solid #f0f0f0;
183
- }}
184
-
185
- .info-label {{
186
- font-weight: 600;
187
- color: #8B0000;
188
- font-size: 14px;
189
- }}
190
-
191
- .info-value {{
192
- font-weight: 700;
193
- color: #2c2c2c;
194
- font-size: 14px;
195
- }}
196
-
197
- .stats-section {{
198
- margin: 35px 0;
199
- }}
200
-
201
- .stats-section h3 {{
202
- color: #8B0000;
203
- margin-bottom: 25px;
204
- font-size: 20px;
205
- font-weight: 600;
206
- border-bottom: 3px solid #8A2BE2;
207
- padding-bottom: 10px;
208
- }}
209
-
210
- .stats-grid {{
211
- display: grid;
212
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
213
- gap: 20px;
214
- margin-bottom: 25px;
215
- }}
216
-
217
- .stat-card {{
218
- background: linear-gradient(135deg, #ffffff 0%, #f8f8f8 100%);
219
- border: 2px solid #e0e0e0;
220
- border-radius: 12px;
221
- padding: 25px 15px;
222
- text-align: center;
223
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
224
- transition: all 0.3s ease;
225
- }}
226
-
227
- .stat-card:hover {{
228
- transform: translateY(-2px);
229
- box-shadow: 0 8px 25px rgba(139, 0, 0, 0.15);
230
- }}
231
-
232
- .stat-number {{
233
- font-size: 36px;
234
- font-weight: 800;
235
- color: #8A2BE2;
236
- margin-bottom: 10px;
237
- text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
238
- }}
239
-
240
- .stat-label {{
241
- font-size: 12px;
242
- color: #666;
243
- text-transform: uppercase;
244
- letter-spacing: 1px;
245
- font-weight: 600;
246
- }}
247
-
248
- .success .stat-number {{ color: #8B0000; }}
249
- .warning .stat-number {{ color: #DC143C; }}
250
- .danger .stat-number {{ color: #8A2BE2; }}
251
-
252
- .attachments-section {{
253
- background: linear-gradient(135deg, #8A2BE2 0%, #9370DB 100%);
254
- color: white;
255
- border-radius: 12px;
256
- padding: 25px;
257
- margin: 35px 0;
258
- box-shadow: 0 6px 20px rgba(138, 43, 226, 0.3);
259
- }}
260
-
261
- .attachments-section h3 {{
262
- color: white;
263
- margin-bottom: 20px;
264
- font-size: 18px;
265
- font-weight: 600;
266
- }}
267
-
268
- .attachment-item {{
269
- display: flex;
270
- align-items: center;
271
- padding: 15px 0;
272
- border-bottom: 1px solid rgba(255, 255, 255, 0.2);
273
- }}
274
-
275
- .attachment-item:last-child {{
276
- border-bottom: none;
277
- }}
278
-
279
- .attachment-icon {{
280
- width: 28px;
281
- height: 28px;
282
- background-color: #8B0000;
283
- border-radius: 6px;
284
- margin-right: 15px;
285
- display: flex;
286
- align-items: center;
287
- justify-content: center;
288
- color: white;
289
- font-size: 12px;
290
- font-weight: bold;
291
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
292
- }}
293
-
294
- .attachment-name {{
295
- font-weight: 500;
296
- color: white;
297
- font-size: 14px;
298
- }}
299
-
300
- .footer {{
301
- background: linear-gradient(135deg, #2c2c2c 0%, #8B0000 100%);
302
- color: white;
303
- padding: 35px 30px;
304
- text-align: center;
305
- }}
306
-
307
- .footer-content {{
308
- max-width: 500px;
309
- margin: 0 auto;
310
- }}
311
-
312
- .footer h4 {{
313
- margin-bottom: 15px;
314
- font-size: 18px;
315
- font-weight: 700;
316
- }}
317
-
318
- .footer p {{
319
- font-size: 14px;
320
- opacity: 0.9;
321
- line-height: 1.6;
322
- }}
323
-
324
- .footer .timestamp {{
325
- margin-top: 25px;
326
- padding-top: 20px;
327
- border-top: 1px solid rgba(255, 255, 255, 0.3);
328
- font-size: 12px;
329
- opacity: 0.8;
330
- }}
331
-
332
- @media only screen and (max-width: 600px) {{
333
- .email-container {{
334
- margin: 10px;
335
- border-radius: 8px;
336
- }}
337
-
338
- .header {{
339
- padding: 30px 20px;
340
- }}
341
-
342
- .content {{
343
- padding: 25px 20px;
344
- }}
345
-
346
- .info-grid {{
347
- grid-template-columns: 1fr;
348
- gap: 15px;
349
- }}
350
-
351
- .stats-grid {{
352
- grid-template-columns: repeat(2, 1fr);
353
- }}
354
-
355
- .header h1 {{
356
- font-size: 26px;
357
- }}
358
-
359
- .stat-number {{
360
- font-size: 28px;
361
- }}
362
- }}
363
- </style>
364
- </head>
365
- <body>
366
- <div class="email-container">
367
- <div class="header">
368
- <div class="header-content">
369
- <h1>Hillside Medical Group</h1>
370
- <p>Patient Processing Report</p>
371
- </div>
372
- </div>
373
-
374
- <div class="status-banner">
375
- Process {status_text.title()}
376
- </div>
377
-
378
- <div class="content">
379
- <div class="report-info">
380
- <h3>Report Summary</h3>
381
- <div class="info-grid">
382
- <div class="info-item">
383
- <span class="info-label">Report Name</span>
384
- <span class="info-value">{custom_name}</span>
385
- </div>
386
- <div class="info-item">
387
- <span class="info-label">Generated</span>
388
- <span class="info-value">{current_date}</span>
389
- </div>
390
- <div class="info-item">
391
- <span class="info-label">Status</span>
392
- <span class="info-value" style="color: {status_color};">{status_text.title()}</span>
393
- </div>
394
- <div class="info-item">
395
- <span class="info-label">Processing Type</span>
396
- <span class="info-value">Automated</span>
397
- </div>
398
- </div>
399
- </div>
400
-
401
- <div class="stats-section">
402
- <h3>Processing Results</h3>
403
- <div class="stats-grid">
404
- <div class="stat-card">
405
- <div class="stat-number">{stats['total']}</div>
406
- <div class="stat-label">Total Records</div>
407
- </div>
408
- <div class="stat-card">
409
- <div class="stat-number">{stats['processed']}</div>
410
- <div class="stat-label">Processed</div>
411
- </div>
412
- <div class="stat-card success">
413
- <div class="stat-number">{stats['successful']}</div>
414
- <div class="stat-label">Completed</div>
415
- </div>
416
- <div class="stat-card danger">
417
- <div class="stat-number">{stats['failed']}</div>
418
- <div class="stat-label">Requires Review</div>
419
- </div>
420
- </div>
421
- </div>
422
-
423
- <div class="attachments-section">
424
- <h3>Report Files</h3>
425
- <div class="attachment-item">
426
- <div class="attachment-icon">CSV</div>
427
- <div class="attachment-name">{custom_name}_Full.csv - Complete patient processing report</div>
428
- </div>
429
- <div class="attachment-item">
430
- <div class="attachment-icon">CSV</div>
431
- <div class="attachment-name">{custom_name}_Bad.csv - Records requiring manual review</div>
432
- </div>
433
- </div>
434
- </div>
435
-
436
- <div class="footer">
437
- <div class="footer-content">
438
- <h4>Hillside Medical Group</h4>
439
- <p>
440
- Automated patient processing system ensuring accurate and efficient
441
- record management while maintaining compliance with healthcare data standards.
442
- </p>
443
- <div class="timestamp">
444
- Report generated {current_date}
445
- </div>
446
- </div>
447
- </div>
448
- </div>
449
- </body>
450
- </html>
451
- """
452
-
453
- return html_template
454
 
455
  def send_report(self, recipients, subject, body, attachments=None):
456
- if not self.service:
457
- print("[Gmail API] Service not initialized")
458
- return False
459
-
460
- if not recipients:
461
- print("[Gmail API] No recipients provided")
462
- return False
463
-
464
  try:
465
  from googleapiclient.errors import HttpError
466
-
467
- print(f"[Gmail API] Sending report to: {recipients}")
468
-
469
- message = MIMEMultipart()
470
- message['From'] = self.sender_email
471
- message['To'] = ', '.join(recipients)
472
- message['Subject'] = subject
473
-
474
  message.attach(MIMEText(body, 'html'))
475
-
476
  if attachments:
477
  for filename, content in attachments.items():
478
- part = MIMEBase('application', 'octet-stream')
479
- part.set_payload(content.encode('utf-8'))
480
- encoders.encode_base64(part)
481
- part.add_header('Content-Disposition', f'attachment; filename="{filename}"')
482
  message.attach(part)
483
-
484
  raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode('utf-8')
485
-
486
- gmail_message = {'raw': raw_message}
487
- sent_message = self.service.users().messages().send(
488
- userId='me',
489
- body=gmail_message
490
- ).execute()
491
-
492
- print(f"[Gmail API] Email sent successfully! Message ID: {sent_message['id']}")
493
- return True
494
-
495
- except HttpError as error:
496
- print(f"[Gmail API] HTTP Error: {error}")
497
- return False
498
- except Exception as e:
499
- print(f"[Gmail API] Error: {e}")
500
- return False
501
 
502
  class GoogleDriveService:
503
  def __init__(self):
504
- self.creds = None
505
- self.service = None
506
  self.folder_id = os.getenv('GOOGLE_DRIVE_FOLDER_ID')
507
  try:
508
- from google.oauth2 import service_account
509
- from googleapiclient.discovery import build
510
  base64_creds = os.getenv('GDRIVE_SA_KEY_BASE64')
511
- if not base64_creds or not self.folder_id:
512
- raise ValueError("Google Drive secrets not found.")
513
- creds_json = base64.b64decode(base64_creds).decode('utf-8')
514
- creds_dict = json.loads(creds_json)
515
  self.creds = service_account.Credentials.from_service_account_info(creds_dict, scopes=['https://www.googleapis.com/auth/drive'])
516
  self.service = build('drive', 'v3', credentials=self.creds)
517
  print("[G-Drive] Service initialized successfully.")
518
- except Exception as e:
519
- print(f"[G-Drive] ERROR: Could not initialize Google Drive service: {e}")
520
 
521
  def upload_file(self, filename, file_content):
522
- if not self.service:
523
- return False
524
  try:
525
  from googleapiclient.http import MediaIoBaseUpload
526
  file_metadata = {'name': filename, 'parents': [self.folder_id]}
527
  media = MediaIoBaseUpload(io.BytesIO(file_content.encode('utf-8')), mimetype='text/csv', resumable=True)
528
  self.service.files().create(body=file_metadata, media_body=media, fields='id').execute()
529
- print(f"[G-Drive] File '{filename}' uploaded successfully.")
530
- return True
531
- except Exception as e:
532
- print(f"[G-Drive] ERROR: File upload failed: {e}")
533
- return False
534
 
535
  email_service = GmailApiService()
536
  drive_service = GoogleDriveService()
537
 
538
  def get_email_list():
539
  try:
540
- with open('config/emails.conf', 'r') as f:
541
- return [line.strip() for line in f if line.strip()]
542
- except FileNotFoundError:
543
- return []
544
 
545
  def run_automation_process(session_id):
546
  global bot_instance
547
- results = []
548
- is_terminated = False
549
- is_crash = False
550
  try:
551
- data = session_data.get(session_id, {})
552
- csv_content = data.get('csv_content')
553
- df = pd.read_csv(io.StringIO(csv_content))
554
- if 'Status' not in df.columns:
555
- df.insert(1, 'Status', '')
556
- patient_list = df[(df['Status'] != 'Done') & (df['Status'] != 'Bad')]['Name'].tolist()
557
- socketio.emit('initial_stats', {'total': len(patient_list)})
558
- results = bot_instance.process_patient_list(patient_list)
559
  is_terminated = bot_instance.termination_event.is_set()
560
  except Exception as e:
561
- print(f"Fatal error in automation thread: {e}")
562
- is_crash = True
563
  socketio.emit('error', {'message': f'A fatal error occurred: {e}'})
564
  finally:
565
  socketio.emit('micro_status_update', {'message': 'Generating final reports...'})
566
  generate_and_send_reports(session_id, results, is_crash_report=is_crash, is_terminated=is_terminated)
567
- if bot_instance:
568
- bot_instance.shutdown()
569
- bot_instance = None
570
- if session_id in session_data:
571
- del session_data[session_id]
572
 
573
  def generate_and_send_reports(session_id, results, is_crash_report=False, is_terminated=False):
574
  if not results:
575
- socketio.emit('process_complete', {'message': 'No patients were processed.'})
576
- return
577
-
578
- data = session_data.get(session_id, {})
579
- original_df = pd.read_csv(io.StringIO(data.get('csv_content')))
580
- if 'Status' not in original_df.columns:
581
- original_df.insert(1, 'Status', '')
582
-
583
- result_df = pd.DataFrame(results).set_index('Name')
584
- original_df.set_index('Name', inplace=True)
585
- original_df.update(result_df)
586
- full_df = original_df.reset_index()
587
- bad_df = full_df[full_df['Status'] == 'Bad'][['Name', 'Status']]
588
 
589
- timestamp = datetime.now().strftime("%d_%b_%Y")
590
- custom_name = data.get('filename') or timestamp
591
- full_report_name = f"{custom_name}_Full.csv"
592
- bad_report_name = f"{custom_name}_Bad.csv"
 
 
 
 
 
 
 
 
 
593
  full_report_content = full_df.to_csv(index=False)
594
 
595
  drive_service.upload_file(full_report_name, full_report_content)
596
 
597
  attachments = {full_report_name: full_report_content, bad_report_name: bad_df.to_csv(index=False)}
598
- status_text = "terminated by user" if is_terminated else "crashed due to an error" if is_crash_report else "completed successfully"
599
-
600
- stats = {
601
- 'total': len(full_df),
602
- 'processed': len(results),
603
- 'successful': len([r for r in results if r['Status'] == 'Done']),
604
- 'failed': len([r for r in results if r['Status'] == 'Bad'])
605
- }
606
-
607
- subject = f"Patient Processing Report - {status_text.title()}: {custom_name}"
608
-
609
- professional_body = email_service.create_professional_email_template(
610
- subject, status_text, stats, custom_name
611
- )
612
 
613
- email_success = email_service.send_report(data.get('emails'), subject, professional_body, attachments)
614
-
615
- if email_success:
616
- socketio.emit('process_complete', {'message': f'Process {status_text}. Report sent successfully.'})
617
- else:
618
- socketio.emit('process_complete', {'message': f'Process {status_text}. Email sending failed.'})
619
 
620
  @app.route('/')
621
  def status_page():
622
- APP_STATUS_HTML = """<!DOCTYPE html><html lang="en"><head><title>Hillside Automation</title><style>body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:linear-gradient(135deg, #8B0000 0%, #DC143C 50%, #8A2BE2 100%);color:white;}.status-box{text-align:center;padding:40px 60px;background:rgba(255,255,255,0.15);border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,0.3);backdrop-filter:blur(10px);}h1{font-size:28px;margin-bottom:10px;} .indicator{font-size:18px;font-weight:600;padding:12px 20px;border-radius:25px;background:rgba(255,255,255,0.2);border:2px solid white;color:white;margin:15px 0;}.info{margin-top:20px;opacity:0.9;}</style></head><body><div class="status-box"><h1>Hillside Automation</h1><div class="indicator">Patient Processing System</div><div class="info">Chrome: Ready<br>Email: Gmail API<br>Drive: Integrated<br><br><a href="https://quantbot.netlify.app" style="color:#fff;">quantbot.netlify.app</a></div></div></body></html>"""
623
  return Response(APP_STATUS_HTML)
624
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
625
  @socketio.on('connect')
626
  def handle_connect():
627
- print(f'Frontend connected from: {FRONTEND_ORIGIN}')
628
  emit('email_list', {'emails': get_email_list()})
629
 
630
  @socketio.on('initialize_session')
631
  def handle_init(data):
632
  session_id = 'user_session'
633
- global bot_instance
634
- session_data[session_id] = {'csv_content': data['content'], 'emails': data['emails'], 'filename': data['filename']}
635
- if bot_instance:
636
- bot_instance.shutdown()
637
- bot_instance = QuantumBot(socketio, app)
638
-
639
- is_success, error_message = bot_instance.initialize_driver()
640
- if is_success:
641
- emit('bot_initialized')
642
- else:
643
- emit('error', {'message': f'Failed to initialize automation bot: {error_message}'})
644
 
645
  @socketio.on('start_login')
646
  def handle_login(credentials):
647
- if not bot_instance:
648
- return emit('error', {'message': 'Bot not initialized.'})
649
- is_success, error_message = bot_instance.login(credentials['username'], credentials['password'])
 
650
  if is_success:
651
- emit('otp_required')
 
 
652
  else:
653
- emit('error', {'message': f'Login failed: {error_message}'})
654
 
655
  @socketio.on('submit_otp')
656
  def handle_otp(data):
657
- if not bot_instance:
658
- return emit('error', {'message': 'Bot not initialized.'})
659
  is_success, error_message = bot_instance.submit_otp(data['otp'])
660
  if is_success:
661
  emit('login_successful')
662
  session_id = 'user_session'
663
  socketio.start_background_task(run_automation_process, session_id)
664
- else:
665
- emit('error', {'message': f'OTP failed: {error_message}'})
666
 
667
  @socketio.on('terminate_process')
668
  def handle_terminate():
669
- if bot_instance:
670
- print("Termination signal received.")
671
- bot_instance.stop()
672
 
673
  if __name__ == '__main__':
674
  print("====================================================================")
675
- print(" Hillside Medical Group - Patient Processing System")
676
- print(" Email: Professional Reports via Gmail API")
677
- print(f" Frontend: {FRONTEND_ORIGIN}")
678
  print("====================================================================")
679
- socketio.run(app, host='0.0.0.0', port=int(os.getenv('PORT', 7860)))
 
7
  import os
8
  import base64
9
  import json
10
+ import re
11
  from datetime import datetime
12
+ from flask import Flask, Response, request, jsonify
13
  from flask_socketio import SocketIO, emit
14
  from flask_cors import CORS
15
  from worker import QuantumBot
 
23
 
24
  app = Flask(__name__)
25
  app.config['SECRET_KEY'] = 'secret-key-for-hillside-automation'
 
26
  FRONTEND_ORIGIN = os.getenv('FRONTEND_URL', 'https://quantbot.netlify.app')
27
+ CORS(app, resources={r"/*": {"origins": [FRONTEND_ORIGIN, "http://localhost:3000", "http://127.0.0.1:5500", "null"]}})
28
+ socketio = SocketIO(app, cors_allowed_origins=[FRONTEND_ORIGIN, "http://localhost:3000", "http://127.0.0.1:5500", "null"], async_mode='eventlet')
29
 
30
  bot_instance = None
31
  session_data = {}
32
 
33
  class GmailApiService:
34
  def __init__(self):
35
+ self.sender_email = os.getenv('EMAIL_SENDER'); self.service = None
 
 
36
  try:
37
+ from google.oauth2 import service_account; from googleapiclient.discovery import build
 
 
38
  base64_creds = os.getenv('GDRIVE_SA_KEY_BASE64')
39
+ if not base64_creds: print("[Gmail API] WARNING: GDRIVE_SA_KEY_BASE64 not found."); return
40
+ creds_json = base64.b64decode(base64_creds).decode('utf-8'); creds_dict = json.loads(creds_json)
41
+ credentials = service_account.Credentials.from_service_account_info(creds_dict, scopes=['https://www.googleapis.com/auth/gmail.send'])
42
+ if self.sender_email: credentials = credentials.with_subject(self.sender_email)
 
 
 
 
 
 
 
 
 
 
 
43
  self.service = build('gmail', 'v1', credentials=credentials)
44
  print("[Gmail API] Service initialized successfully")
45
+ except Exception as e: print(f"[Gmail API] Error: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
  def send_report(self, recipients, subject, body, attachments=None):
48
+ if not self.service or not recipients: return False
 
 
 
 
 
 
 
49
  try:
50
  from googleapiclient.errors import HttpError
51
+ message = MIMEMultipart(); message['From'] = self.sender_email; message['To'] = ', '.join(recipients); message['Subject'] = subject
 
 
 
 
 
 
 
52
  message.attach(MIMEText(body, 'html'))
 
53
  if attachments:
54
  for filename, content in attachments.items():
55
+ part = MIMEBase('application', 'octet-stream'); part.set_payload(content.encode('utf-8'))
56
+ encoders.encode_base64(part); part.add_header('Content-Disposition', f'attachment; filename="{filename}"')
 
 
57
  message.attach(part)
 
58
  raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode('utf-8')
59
+ sent_message = self.service.users().messages().send(userId='me', body={'raw': raw_message}).execute()
60
+ print(f"[Gmail API] Email sent successfully! Message ID: {sent_message['id']}"); return True
61
+ except Exception as e: print(f"[Gmail API] Error: {e}"); return False
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
  class GoogleDriveService:
64
  def __init__(self):
65
+ self.creds = None; self.service = None
 
66
  self.folder_id = os.getenv('GOOGLE_DRIVE_FOLDER_ID')
67
  try:
68
+ from google.oauth2 import service_account; from googleapiclient.discovery import build
 
69
  base64_creds = os.getenv('GDRIVE_SA_KEY_BASE64')
70
+ if not base64_creds or not self.folder_id: raise ValueError("Google Drive secrets not found.")
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("[G-Drive] Service initialized successfully.")
75
+ except Exception as e: print(f"[G-Drive] ERROR: Could not initialize Google Drive service: {e}")
 
76
 
77
  def upload_file(self, filename, file_content):
78
+ if not self.service: return False
 
79
  try:
80
  from googleapiclient.http import MediaIoBaseUpload
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"[G-Drive] File '{filename}' uploaded successfully."); return True
85
+ except Exception as e: print(f"[G-Drive] ERROR: File upload failed: {e}"); return False
 
 
 
86
 
87
  email_service = GmailApiService()
88
  drive_service = GoogleDriveService()
89
 
90
  def get_email_list():
91
  try:
92
+ with open('config/emails.conf', 'r') as f: return [line.strip() for line in f if line.strip()]
93
+ except FileNotFoundError: return []
 
 
94
 
95
  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
+ socketio.emit('initial_stats', {'total': len(patient_data)})
101
+ results = bot_instance.process_patient_list(patient_data)
 
 
 
 
 
102
  is_terminated = bot_instance.termination_event.is_set()
103
  except Exception as e:
104
+ print(f"Fatal error in automation thread: {e}"); is_crash = True
 
105
  socketio.emit('error', {'message': f'A fatal error occurred: {e}'})
106
  finally:
107
  socketio.emit('micro_status_update', {'message': 'Generating final reports...'})
108
  generate_and_send_reports(session_id, results, is_crash_report=is_crash, is_terminated=is_terminated)
109
+ if bot_instance: bot_instance.shutdown(); bot_instance = None
110
+ if session_id in session_data: del session_data[session_id]
 
 
 
111
 
112
  def generate_and_send_reports(session_id, results, is_crash_report=False, is_terminated=False):
113
  if not results:
114
+ socketio.emit('process_complete', {'message': 'No patients were processed.'}); return
115
+ data = session_data.get(session_id, {});
 
 
 
 
 
 
 
 
 
 
 
116
 
117
+ # Use the prepared data as the base for the final report
118
+ full_df = pd.DataFrame(data.get('patient_data_for_report'))
119
+ result_df = pd.DataFrame(results)
120
+
121
+ if not result_df.empty:
122
+ result_df = result_df.set_index(['Name', 'PRN'])
123
+ full_df = full_df.set_index(['Name', 'PRN'])
124
+ full_df.update(result_df)
125
+ full_df.reset_index(inplace=True)
126
+
127
+ bad_df = full_df[full_df['Status'] == 'Bad'][['Name', 'PRN', 'Status']]
128
+ timestamp = datetime.now().strftime("%d_%b_%Y"); custom_name = data.get('filename') or timestamp
129
+ full_report_name = f"{custom_name}_Full.csv"; bad_report_name = f"{custom_name}_Bad.csv"
130
  full_report_content = full_df.to_csv(index=False)
131
 
132
  drive_service.upload_file(full_report_name, full_report_content)
133
 
134
  attachments = {full_report_name: full_report_content, bad_report_name: bad_df.to_csv(index=False)}
135
+ status_text = "terminated by user" if is_terminated else "crashed" if is_crash_report else "completed"
136
+ subject = f"Automation Report [{status_text.upper()}]: {custom_name}"
137
+ body = f"The process was {status_text}. The full report is in Google Drive and also attached."
 
 
 
 
 
 
 
 
 
 
 
138
 
139
+ email_service.send_report(data.get('emails'), subject, body, attachments)
140
+ socketio.emit('process_complete', {'message': f'Process {status_text}. Report sent.'})
 
 
 
 
141
 
142
  @app.route('/')
143
  def status_page():
144
+ 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>"""
145
  return Response(APP_STATUS_HTML)
146
 
147
+ def standardize_name(name):
148
+ if not isinstance(name, str): return ""
149
+ match = re.match(r'^([\w\s,-]+?)\s*(?:ID|$)', name)
150
+ if not match: return ""
151
+ clean_name = match.group(1).strip().lower()
152
+ if ',' in clean_name:
153
+ parts = [p.strip() for p in clean_name.split(',')]
154
+ clean_name = "".join(reversed(parts))
155
+ return re.sub(r'[^a-z0-9]', '', clean_name)
156
+
157
+ @app.route('/process_files_for_automation', methods=['POST'])
158
+ def process_files_for_automation():
159
+ session_id = 'user_session'
160
+ if 'app_data' not in request.files or 'quantum_data' not in request.files:
161
+ return jsonify({"error": "Both files are required."}), 400
162
+ try:
163
+ 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'])
164
+ 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'])
165
+
166
+ df_app_filtered = df_app.dropna(subset=['PRN']); df_app_filtered = df_app_filtered[df_app_filtered['PRN'].astype(str).str.strip() != '']
167
+ prn_lookup = {standardize_name(row['Patient Name']): row['PRN'] for _, row in df_app_filtered.iterrows()}
168
+
169
+ patient_data_for_automation = []
170
+ for _, row in df_quantum.iterrows():
171
+ prn = prn_lookup.get(standardize_name(row['Name']), "")
172
+ if prn:
173
+ patient_data_for_automation.append({'Name': row['Name'], 'PRN': prn})
174
+
175
+ # Save the original quantum data for the final report
176
+ session_data[session_id]['patient_data_for_report'] = df_quantum[['Name']].to_dict('records')
177
+ session_data[session_id]['patient_data'] = patient_data_for_automation
178
+
179
+ socketio.emit('data_processed')
180
+ return jsonify({"message": "Data processed successfully."})
181
+ except Exception as e:
182
+ return jsonify({"error": str(e)}), 500
183
+
184
  @socketio.on('connect')
185
  def handle_connect():
186
+ print(f'Frontend connected.')
187
  emit('email_list', {'emails': get_email_list()})
188
 
189
  @socketio.on('initialize_session')
190
  def handle_init(data):
191
  session_id = 'user_session'
192
+ session_data[session_id] = {'emails': data['emails'], 'filename': data['filename']}
193
+ # Bot creation is now handled by the /process endpoint
 
 
 
 
 
 
 
 
 
194
 
195
  @socketio.on('start_login')
196
  def handle_login(credentials):
197
+ global bot_instance
198
+ if bot_instance: bot_instance.shutdown()
199
+ bot_instance = QuantumBot(socketio, app)
200
+ is_success, error_message = bot_instance.initialize_driver()
201
  if is_success:
202
+ is_login_success, login_error = bot_instance.login(credentials['username'], credentials['password'])
203
+ if is_login_success: emit('otp_required')
204
+ else: emit('error', {'message': f'Login failed: {login_error}'})
205
  else:
206
+ emit('error', {'message': f'Failed to initialize bot: {error_message}'})
207
 
208
  @socketio.on('submit_otp')
209
  def handle_otp(data):
210
+ if not bot_instance: return emit('error', {'message': 'Bot not initialized.'})
 
211
  is_success, error_message = bot_instance.submit_otp(data['otp'])
212
  if is_success:
213
  emit('login_successful')
214
  session_id = 'user_session'
215
  socketio.start_background_task(run_automation_process, session_id)
216
+ else: emit('error', {'message': f'OTP failed: {error_message}'})
 
217
 
218
  @socketio.on('terminate_process')
219
  def handle_terminate():
220
+ if bot_instance: print("Termination signal received."); bot_instance.stop()
 
 
221
 
222
  if __name__ == '__main__':
223
  print("====================================================================")
224
+ print(" 🤗 Hillside Automation - Two-Phase Processing")
225
+ print(f" Frontend URL: {FRONTEND_ORIGIN}")
226
+ print(f" Port: {os.getenv('PORT', 7860)}")
227
  print("====================================================================")
228
+ socketio.run(app, host='0.0.0.0', port=int(os.getenv('PORT', 7860)))
worker.py CHANGED
@@ -1,7 +1,6 @@
1
  import time
2
  import threading
3
  import subprocess
4
-
5
  from selenium import webdriver
6
  from selenium.webdriver.chrome.service import Service as ChromeService
7
  from selenium.webdriver.chrome.options import Options as ChromeOptions
@@ -10,64 +9,36 @@ from selenium.webdriver.support.ui import WebDriverWait
10
  from selenium.webdriver.support import expected_conditions as EC
11
  from selenium.common.exceptions import TimeoutException
12
 
13
-
14
  class QuantumBot:
15
  def __init__(self, socketio, app):
16
- self.socketio = socketio
17
- self.app = app
18
- self.driver = None
19
- self.DEFAULT_TIMEOUT = 30
20
- self.termination_event = threading.Event()
21
 
22
  def _kill_chrome_processes(self):
23
- """Kill any existing Chrome processes to prevent conflicts"""
24
  try:
25
- subprocess.run(['pkill', '-f', 'chrome'], capture_output=True, text=True, timeout=5)
26
- subprocess.run(['pkill', '-f', 'chromium'], capture_output=True, text=True, timeout=5)
27
  time.sleep(1)
28
- except:
29
  pass
30
 
31
  def initialize_driver(self):
32
  try:
33
  self.micro_status("Initializing headless browser...")
34
-
35
- # Kill any existing Chrome processes first
36
  self._kill_chrome_processes()
37
-
38
- options = ChromeOptions()
39
- options.binary_location = "/usr/bin/chromium"
40
-
41
- # DON'T set --user-data-dir - let Chrome handle it automatically
42
- # This prevents the "already in use" error
43
-
44
- # Minimal required flags for HuggingFace Spaces
45
- options.add_argument("--headless=new")
46
- options.add_argument("--no-sandbox")
47
- options.add_argument("--disable-dev-shm-usage")
48
- options.add_argument("--disable-gpu")
49
- options.add_argument("--remote-debugging-port=0")
50
  options.add_argument("--window-size=1920,1080")
51
- options.add_argument("--no-first-run")
52
- options.add_argument("--disable-extensions")
53
-
54
- # User agent for better compatibility
55
  user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
56
  options.add_argument(f"--user-agent={user_agent}")
57
-
58
  service = ChromeService(executable_path="/usr/bin/chromedriver")
59
  self.driver = webdriver.Chrome(service=service, options=options)
60
-
61
- # Anti-detection
62
  self.driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
63
  'source': "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
64
  })
65
-
66
  return True, None
67
-
68
  except Exception as e:
69
- error_message = f"Message: {str(e)}"
70
- print(f"CRITICAL ERROR in WebDriver Initialization: {error_message}")
71
  return False, error_message
72
 
73
  def micro_status(self, message):
@@ -76,144 +47,93 @@ class QuantumBot:
76
  self.socketio.emit('micro_status_update', {'message': message})
77
 
78
  def stop(self):
79
- self.micro_status("Termination signal received. Finishing current patient...")
80
- self.termination_event.set()
81
 
82
  def login(self, username, password):
83
  try:
84
- self.micro_status("Navigating to login page...")
85
- self.driver.get("https://gateway.quantumepay.com/")
86
- time.sleep(2)
87
- self.micro_status("Entering credentials...")
88
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
89
- EC.presence_of_element_located((By.ID, "Username"))
90
- ).send_keys(username)
91
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
92
- EC.presence_of_element_located((By.ID, "Password"))
93
- ).send_keys(password)
94
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
95
- EC.element_to_be_clickable((By.ID, "login"))
96
- ).click()
97
  self.micro_status("Waiting for OTP screen...")
98
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
99
- EC.presence_of_element_located((By.ID, "code1"))
100
- )
101
  return True, None
102
  except Exception as e:
103
- error_message = f"Error during login: {str(e)}"
104
- print(f"[Bot] ERROR during login: {error_message}")
105
  return False, error_message
106
 
107
  def submit_otp(self, otp):
108
  try:
109
- self.micro_status(f"Submitting OTP...")
110
- otp_digits = list(otp)
111
- for i in range(6):
112
- self.driver.find_element(By.ID, f"code{i+1}").send_keys(otp_digits[i])
113
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
114
- EC.element_to_be_clickable((By.ID, "login"))
115
- ).click()
116
  self.micro_status("Verifying login success...")
117
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
118
- EC.element_to_be_clickable((By.XPATH, "//span[text()='Payments']"))
119
- )
120
  return True, None
121
  except Exception as e:
122
- error_message = f"Error during OTP submission: {str(e)}"
123
- print(f"[Bot] ERROR during OTP submission: {error_message}")
124
  return False, error_message
125
 
126
- def process_patient_list(self, patient_list):
127
  results = []
128
- for index, patient_name in enumerate(patient_list):
129
- if self.termination_event.is_set():
130
- print("[Bot] Termination detected. Stopping process.")
131
- break
 
 
132
  with self.app.app_context():
 
133
  self.socketio.emit('stats_update', {
134
- 'processed': len(results),
135
- 'remaining': len(patient_list) - len(results)
136
  })
137
- self.micro_status(f"Processing '{patient_name}' ({index + 1}/{len(patient_list)})...")
138
- status = self._process_single_patient(patient_name)
139
- results.append({'Name': patient_name, 'Status': status})
140
- with self.app.app_context():
141
- self.socketio.emit('log_update', {'name': patient_name, 'status': status})
142
  return results
143
 
144
- def _process_single_patient(self, patient_name):
145
  try:
146
  self.micro_status(f"Navigating to Void page for '{patient_name}'")
147
  self.driver.get("https://gateway.quantumepay.com/credit-card/void")
148
-
149
  search_successful = False
150
  for attempt in range(15):
151
  try:
152
- self.micro_status(f"Searching for patient (Attempt {attempt + 1})...")
153
- WebDriverWait(self.driver, 2).until(
154
- EC.presence_of_element_located((By.XPATH, "//div[contains(@class, 'table-wrapper')]"))
155
- )
156
- search_box = WebDriverWait(self.driver, 2).until(
157
- EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']"))
158
- )
159
- search_box.click(); time.sleep(0.5)
160
- search_box.clear(); time.sleep(0.5)
161
- search_box.send_keys(patient_name)
162
- search_successful = True
163
- break
164
- except Exception:
165
- time.sleep(1)
166
-
167
- if not search_successful:
168
- raise Exception("Failed to search for patient.")
169
-
170
  time.sleep(3)
171
  self.micro_status("Opening transaction details...")
172
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
173
- EC.element_to_be_clickable((By.XPATH, f"//tr[contains(., \"{patient_name}\")]//button[@data-v-b6b33fa0]"))
174
- ).click()
175
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
176
- EC.element_to_be_clickable((By.LINK_TEXT, "Transaction Detail"))
177
- ).click()
178
-
179
  self.micro_status("Adding to Vault...")
180
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
181
- EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Add to Vault']"))
182
- ).click()
183
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
184
- EC.element_to_be_clickable((By.XPATH, "//div[@class='modal-footer']//button/span[normalize-space()='Confirm']"))
185
- ).click()
186
-
187
  try:
188
  self.micro_status("Verifying success and saving...")
189
- company_input = WebDriverWait(self.driver, 10).until(
190
- EC.element_to_be_clickable((By.NAME, "company_name"))
191
- )
192
- company_input.clear()
193
- company_input.send_keys(patient_name)
194
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
195
- EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Save Changes']"))
196
- ).click()
197
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
198
- EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Confirm']]"))
199
- ).click()
200
- time.sleep(5)
201
- return 'Done'
202
  except TimeoutException:
203
  self.micro_status(f"'{patient_name}' is in a bad state, cancelling.")
204
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
205
- EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Cancel']]"))
206
- ).click()
207
  return 'Bad'
208
  except Exception as e:
209
- print(f"An error occurred while processing {patient_name}: {e}")
210
- return 'Error'
211
 
212
  def shutdown(self):
213
  try:
214
- if self.driver:
215
- self.driver.quit()
216
  self._kill_chrome_processes()
217
  print("[Bot] Chrome session closed and cleaned up.")
218
- except Exception as e:
219
- print(f"[Bot] Error during shutdown: {e}")
 
1
  import time
2
  import threading
3
  import subprocess
 
4
  from selenium import webdriver
5
  from selenium.webdriver.chrome.service import Service as ChromeService
6
  from selenium.webdriver.chrome.options import Options as ChromeOptions
 
9
  from selenium.webdriver.support import expected_conditions as EC
10
  from selenium.common.exceptions import TimeoutException
11
 
 
12
  class QuantumBot:
13
  def __init__(self, socketio, app):
14
+ self.socketio = socketio; self.app = app; self.driver = None
15
+ self.DEFAULT_TIMEOUT = 30; self.termination_event = threading.Event()
 
 
 
16
 
17
  def _kill_chrome_processes(self):
 
18
  try:
19
+ subprocess.run(['pkill', '-f', 'chromium'], check=True, timeout=5)
 
20
  time.sleep(1)
21
+ except Exception:
22
  pass
23
 
24
  def initialize_driver(self):
25
  try:
26
  self.micro_status("Initializing headless browser...")
 
 
27
  self._kill_chrome_processes()
28
+ options = ChromeOptions(); options.binary_location = "/usr/bin/chromium"
29
+ options.add_argument("--headless=new"); options.add_argument("--no-sandbox")
30
+ options.add_argument("--disable-dev-shm-usage"); options.add_argument("--disable-gpu")
 
 
 
 
 
 
 
 
 
 
31
  options.add_argument("--window-size=1920,1080")
 
 
 
 
32
  user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
33
  options.add_argument(f"--user-agent={user_agent}")
 
34
  service = ChromeService(executable_path="/usr/bin/chromedriver")
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})"
38
  })
 
39
  return True, None
 
40
  except Exception as e:
41
+ error_message = f"Message: {str(e)}"; print(f"CRITICAL ERROR in WebDriver Initialization: {error_message}")
 
42
  return False, error_message
43
 
44
  def micro_status(self, message):
 
47
  self.socketio.emit('micro_status_update', {'message': message})
48
 
49
  def stop(self):
50
+ self.micro_status("Termination signal received..."); self.termination_event.set()
 
51
 
52
  def login(self, username, password):
53
  try:
54
+ self.micro_status("Navigating to login page..."); self.driver.get("https://gateway.quantumepay.com/")
55
+ time.sleep(2); self.micro_status("Entering credentials...")
56
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "Username"))).send_keys(username)
57
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "Password"))).send_keys(password)
58
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "login"))).click()
 
 
 
 
 
 
 
 
59
  self.micro_status("Waiting for OTP screen...")
60
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "code1")))
 
 
61
  return True, None
62
  except Exception as e:
63
+ error_message = f"Error during login: {str(e)}"; print(f"[Bot] ERROR during login: {error_message}")
 
64
  return False, error_message
65
 
66
  def submit_otp(self, otp):
67
  try:
68
+ self.micro_status(f"Submitting OTP..."); otp_digits = list(otp)
69
+ for i in range(6): self.driver.find_element(By.ID, f"code{i+1}").send_keys(otp_digits[i])
70
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "login"))).click()
 
 
 
 
71
  self.micro_status("Verifying login success...")
72
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//span[text()='Payments']")))
 
 
73
  return True, None
74
  except Exception as e:
75
+ error_message = f"Error during OTP submission: {str(e)}"; print(f"[Bot] ERROR during OTP submission: {error_message}")
 
76
  return False, error_message
77
 
78
+ def process_patient_list(self, patient_data):
79
  results = []
80
+ for index, record in enumerate(patient_data):
81
+ if self.termination_event.is_set(): print("[Bot] Termination detected."); break
82
+ patient_name = record['Name']; patient_prn = record['PRN']
83
+ self.micro_status(f"Processing '{patient_name}' ({index + 1}/{len(patient_data)})...")
84
+ status = self._process_single_patient(patient_name, patient_prn)
85
+ results.append({'Name': patient_name, 'PRN': patient_prn, 'Status': status})
86
  with self.app.app_context():
87
+ self.socketio.emit('log_update', {'name': patient_name, 'prn': patient_prn, 'status': status})
88
  self.socketio.emit('stats_update', {
89
+ 'processed': len(results), 'remaining': len(patient_data) - len(results), 'status': status
 
90
  })
 
 
 
 
 
91
  return results
92
 
93
+ def _process_single_patient(self, patient_name, patient_prn):
94
  try:
95
  self.micro_status(f"Navigating to Void page for '{patient_name}'")
96
  self.driver.get("https://gateway.quantumepay.com/credit-card/void")
 
97
  search_successful = False
98
  for attempt in range(15):
99
  try:
100
+ self.micro_status(f"Searching... (Attempt {attempt + 1})")
101
+ WebDriverWait(self.driver, 2).until(EC.presence_of_element_located((By.XPATH, "//div[contains(@class, 'table-wrapper')]")))
102
+ search_box = WebDriverWait(self.driver, 2).until(EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']")))
103
+ search_box.click(); time.sleep(0.5); search_box.clear(); time.sleep(0.5)
104
+ search_box.send_keys(patient_name); search_successful = True; break
105
+ except Exception: time.sleep(1)
106
+ if not search_successful: raise Exception("Failed to search for patient.")
 
 
 
 
 
 
 
 
 
 
 
107
  time.sleep(3)
108
  self.micro_status("Opening transaction details...")
109
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, f"//tr[contains(., \"{patient_name}\")]//button[@data-v-b6b33fa0]"))).click()
110
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.LINK_TEXT, "Transaction Detail"))).click()
 
 
 
 
 
111
  self.micro_status("Adding to Vault...")
112
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Add to Vault']"))).click()
113
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//div[@class='modal-footer']//button/span[normalize-space()='Confirm']"))).click()
 
 
 
 
 
114
  try:
115
  self.micro_status("Verifying success and saving...")
116
+ company_input = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.NAME, "company_name")))
117
+ company_input.clear(); company_input.send_keys(patient_name)
118
+
119
+ if patient_prn and pd.notna(patient_prn):
120
+ self.micro_status(f"Entering PRN: {patient_prn}...")
121
+ contact_input = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.NAME, "last_name")))
122
+ contact_input.clear(); contact_input.send_keys(patient_prn)
123
+
124
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Save Changes']"))).click()
125
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Confirm']]"))).click()
126
+ time.sleep(5); return 'Done'
 
 
127
  except TimeoutException:
128
  self.micro_status(f"'{patient_name}' is in a bad state, cancelling.")
129
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Cancel']]"))).click()
 
 
130
  return 'Bad'
131
  except Exception as e:
132
+ print(f"An error occurred while processing {patient_name}: {e}"); return 'Error'
 
133
 
134
  def shutdown(self):
135
  try:
136
+ if self.driver: self.driver.quit()
 
137
  self._kill_chrome_processes()
138
  print("[Bot] Chrome session closed and cleaned up.")
139
+ except Exception as e: print(f"[Bot] Error during shutdown: {e}")