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

Added new feature

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