sonuprasad23 commited on
Commit
63e7b53
·
1 Parent(s): 778337f

Porject Uploaded

Browse files
Files changed (5) hide show
  1. Dockerfile +13 -0
  2. config/emails.conf +4 -0
  3. requirements.txt +11 -0
  4. server.py +190 -0
  5. worker.py +150 -0
Dockerfile ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+ WORKDIR /app
3
+ COPY . .
4
+ RUN apt-get update && apt-get install -y --no-install-recommends \
5
+ build-essential \
6
+ libssl-dev \
7
+ libffi-dev \
8
+ python3-dev \
9
+ ca-certificates \
10
+ && rm -rf /var/lib/apt/lists/*
11
+ RUN pip install --no-cache-dir -r requirements.txt
12
+ EXPOSE 7860
13
+ CMD ["python", "server.py"]
config/emails.conf ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ derinpatel@hillsidemedicalgroup.com
2
+ bruno.carvalho@iglobalservices.net
3
+ arajendra@hillsidemedicalgroup.com
4
+ smahato@hillsidemedicalgroup.com
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ pandas
2
+ Flask
3
+ Flask-SocketIO
4
+ Flask-Cors
5
+ eventlet
6
+ python-dotenv
7
+ selenium
8
+ webdriver-manager
9
+ google-api-python-client
10
+ google-auth-httplib2
11
+ google-auth-oauthlib
server.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import eventlet
2
+ eventlet.monkey_patch()
3
+
4
+ import pandas as pd
5
+ import io
6
+ 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
15
+ import smtplib
16
+ from email.mime.multipart import MIMEMultipart
17
+ from email.mime.text import MIMEText
18
+ from email.mime.base import MIMEBase
19
+ from email import encoders
20
+ from dotenv import load_dotenv
21
+
22
+ load_dotenv()
23
+
24
+ app = Flask(__name__)
25
+ app.config['SECRET_KEY'] = 'secret-key-for-hillside-automation'
26
+ CORS(app, resources={r"/*": {"origins": "*"}})
27
+ socketio = SocketIO(app, cors_allowed_origins="*", async_mode='eventlet')
28
+
29
+ bot_instance = None
30
+ session_data = {}
31
+ automation_thread = None
32
+
33
+ class GoogleDriveService:
34
+ def __init__(self):
35
+ self.creds = None
36
+ self.service = None
37
+ self.folder_id = os.getenv('GOOGLE_DRIVE_FOLDER_ID')
38
+
39
+ try:
40
+ from google.oauth2 import service_account
41
+ from googleapiclient.discovery import build
42
+
43
+ # --- Definitive Secure Credential Loading ---
44
+ base64_creds = os.getenv('GDRIVE_SA_KEY_BASE64')
45
+ if not base64_creds:
46
+ raise ValueError("GDRIVE_SA_KEY_BASE64 secret not found.")
47
+ if not self.folder_id:
48
+ raise ValueError("GOOGLE_DRIVE_FOLDER_ID secret not found.")
49
+
50
+ creds_json = base64.b64decode(base64_creds).decode('utf-8')
51
+ creds_dict = json.loads(creds_json)
52
+
53
+ self.creds = service_account.Credentials.from_service_account_info(creds_dict, scopes=['https://www.googleapis.com/auth/drive'])
54
+ self.service = build('drive', 'v3', credentials=self.creds)
55
+ print("[G-Drive] Service initialized securely from secrets.")
56
+ except Exception as e:
57
+ print(f"[G-Drive] CRITICAL ERROR: Could not initialize Google Drive service: {e}")
58
+
59
+ def upload_file(self, filename, file_content):
60
+ if not self.service: return False
61
+ try:
62
+ from googleapiclient.http import MediaIoBaseUpload
63
+ file_metadata = {'name': filename, 'parents': [self.folder_id]}
64
+ media = MediaIoBaseUpload(io.BytesIO(file_content.encode('utf-8')), mimetype='text/csv', resumable=True)
65
+ self.service.files().create(body=file_metadata, media_body=media, fields='id').execute()
66
+ print(f"[G-Drive] File '{filename}' uploaded successfully.")
67
+ return True
68
+ except Exception as e:
69
+ print(f"[G-Drive] ERROR: File upload failed: {e}"); return False
70
+
71
+ class EmailService:
72
+ def __init__(self):
73
+ self.sender_email = os.getenv('EMAIL_SENDER')
74
+ self.password = os.getenv('EMAIL_PASSWORD')
75
+ self.smtp_server = "smtp.gmail.com"; self.smtp_port = 587
76
+ if not self.sender_email or not self.password: print("[Email] WARNING: Email credentials not found in secrets.")
77
+ def send_report(self, recipients, subject, body, attachments=None):
78
+ if not self.sender_email or not self.password: return False
79
+ try:
80
+ msg = MIMEMultipart(); msg['From'] = self.sender_email; msg['To'] = ", ".join(recipients); msg['Subject'] = subject
81
+ msg.attach(MIMEText(body, 'html'))
82
+ if attachments:
83
+ for filename, content in attachments.items():
84
+ part = MIMEBase('application', 'octet-stream'); part.set_payload(content.encode('utf-8'))
85
+ encoders.encode_base64(part); part.add_header('Content-Disposition', f'attachment; filename="{filename}"')
86
+ msg.attach(part)
87
+ with smtplib.SMTP(self.smtp_server, self.smtp_port) as server:
88
+ server.starttls(); server.login(self.sender_email, self.password); server.send_message(msg)
89
+ print(f"Email sent successfully to {', '.join(recipients)}"); return True
90
+ except Exception as e: print(f"Failed to send email: {e}"); return False
91
+
92
+ email_service = EmailService()
93
+ drive_service = GoogleDriveService()
94
+
95
+ def get_email_list():
96
+ try:
97
+ with open('config/emails.conf', 'r') as f: return [line.strip() for line in f if line.strip()]
98
+ except FileNotFoundError: return []
99
+
100
+ def run_automation_process(session_id):
101
+ global bot_instance
102
+ results = []; is_terminated = False; is_crash = False
103
+ try:
104
+ data = session_data.get(session_id, {}); csv_content = data.get('csv_content')
105
+ df = pd.read_csv(io.StringIO(csv_content));
106
+ if 'Status' not in df.columns: df.insert(1, 'Status', '')
107
+ patient_list = df[ (df['Status'] != 'Done') & (df['Status'] != 'Bad') ]['Name'].tolist()
108
+ socketio.emit('initial_stats', {'total': len(patient_list)})
109
+ results = bot_instance.process_patient_list(patient_list)
110
+ is_terminated = bot_instance.termination_event.is_set()
111
+ except Exception as e:
112
+ print(f"Fatal error in automation thread: {e}"); is_crash = True
113
+ socketio.emit('error', {'message': f'A fatal error occurred: {e}'})
114
+ finally:
115
+ socketio.emit('micro_status_update', {'message': 'Generating final reports...'})
116
+ generate_and_send_reports(session_id, results, is_crash_report=is_crash, is_terminated=is_terminated)
117
+ if bot_instance: bot_instance.shutdown(); bot_instance = None
118
+ if session_id in session_data: del session_data[session_id]
119
+
120
+ def generate_and_send_reports(session_id, results, is_crash_report=False, is_terminated=False):
121
+ if not results:
122
+ socketio.emit('process_complete', {'message': 'No patients were processed.'}); return
123
+ data = session_data.get(session_id, {}); original_df = pd.read_csv(io.StringIO(data.get('csv_content')))
124
+ if 'Status' not in original_df.columns: original_df.insert(1, 'Status', '')
125
+ result_df = pd.DataFrame(results).set_index('Name'); original_df.set_index('Name', inplace=True)
126
+ original_df.update(result_df); full_df = original_df.reset_index()
127
+ bad_df = full_df[full_df['Status'] == 'Bad'][['Name', '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
+ drive_service.upload_file(full_report_name, full_report_content)
132
+ attachments = {full_report_name: full_report_content, bad_report_name: bad_df.to_csv(index=False)}
133
+ status_text = "terminated by user" if is_terminated else "crashed due to an error" if is_crash_report else "completed successfully"
134
+ subject = f"Automation Report [{status_text.upper()}]: {custom_name}"
135
+ body = f"""<html><body><h2>Hillside's Quantum Automation Report</h2><p><b>The process was {status_text}.</b></p><p><b>Total Patients in File:</b> {len(full_df)}</p><p><b>Processed in this run:</b> {len(results)}</p><p><b>Successful ('Done'):</b> {len([r for r in results if r['Status'] == 'Done'])}</p><p><b>Bad State ('Bad'):</b> {len([r for r in results if r['Status'] == 'Bad'])}</p><p>The full report and a list of 'Bad' status patients from this run are attached.</p></body></html>"""
136
+ email_service.send_report(data.get('emails'), subject, body, attachments)
137
+ socketio.emit('process_complete', {'message': f'Process {status_text}. Report sent.'})
138
+
139
+ @app.route('/')
140
+ def status_page():
141
+ APP_STATUS_HTML = """
142
+ <!DOCTYPE html><html lang="en"><head><title>API Status</title><style>
143
+ 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:#f0f2f5;}
144
+ .status-box{text-align:center;padding:40px 60px;background:white;border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,0.1);}
145
+ h1{font-size:24px;color:#333;margin-bottom:10px;} .indicator{font-size:18px;font-weight:600;padding:8px 16px;border-radius:20px;}
146
+ .active{color:#28a745;background-color:#e9f7ea;}
147
+ </style></head><body><div class="status-box"><h1>Hillside's Automation API</h1><div class="indicator active">● Active</div></div></body></html>
148
+ """
149
+ return Response(APP_STATUS_HTML)
150
+
151
+ @socketio.on('connect')
152
+ def handle_connect():
153
+ print('Frontend connected.'); emit('email_list', {'emails': get_email_list()})
154
+
155
+ @socketio.on('initialize_session')
156
+ def handle_init(data):
157
+ session_id = 'user_session'
158
+ session_data[session_id] = {'csv_content': data['content'], 'emails': data['emails'], 'filename': data['filename']}
159
+ global bot_instance
160
+ if bot_instance: bot_instance.shutdown()
161
+ bot_instance = QuantumBot(socketio, app)
162
+ if bot_instance.initialize_driver(): emit('bot_initialized')
163
+ else: emit('error', {'message': 'Failed to initialize the automation bot.'})
164
+
165
+ @socketio.on('start_login')
166
+ def handle_login(credentials):
167
+ if not bot_instance: return emit('error', {'message': 'Bot not initialized.'})
168
+ if bot_instance.login(credentials['username'], credentials['password']): emit('otp_required')
169
+ else: emit('error', {'message': 'Login failed. Please check credentials.'})
170
+
171
+ @socketio.on('submit_otp')
172
+ def handle_otp(data):
173
+ if not bot_instance: return emit('error', {'message': 'Bot not initialized.'})
174
+ if bot_instance.submit_otp(data['otp']):
175
+ emit('login_successful')
176
+ session_id = 'user_session'
177
+ socketio.start_background_task(target=run_automation_process, session_id=session_id)
178
+ else: emit('error', {'message': 'OTP was incorrect or timed out.'})
179
+
180
+ @socketio.on('terminate_process')
181
+ def handle_terminate():
182
+ if bot_instance: print("Termination signal received."); bot_instance.stop()
183
+
184
+ if __name__ == '__main__':
185
+ print("====================================================================")
186
+ print(" Hillside Automation Backend - PRODUCTION READY")
187
+ print(" Make sure all secrets are set in your Hugging Face Space.")
188
+ print(" Listening on http://127.0.0.1:5000")
189
+ print("====================================================================")
190
+ socketio.run(app, host='0.0.0.0', port=int(os.getenv('PORT', 7860)))
worker.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import time
3
+ import io
4
+ import threading
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
8
+ from webdriver_manager.chrome import ChromeDriverManager
9
+ from selenium.webdriver.common.by import By
10
+ from selenium.webdriver.support.ui import WebDriverWait
11
+ from selenium.webdriver.support import expected_conditions as EC
12
+
13
+ class QuantumBot:
14
+ def __init__(self, socketio, app):
15
+ self.socketio = socketio
16
+ self.app = app
17
+ self.driver = None
18
+ self.DEFAULT_TIMEOUT = 30
19
+ self.termination_event = threading.Event()
20
+
21
+ def initialize_driver(self):
22
+ try:
23
+ self.micro_status("Initializing headless browser...")
24
+ options = ChromeOptions()
25
+ user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36"
26
+ options.add_argument(f'user-agent={user_agent}')
27
+ options.add_argument("--headless")
28
+ options.add_argument("--window-size=1920,1080")
29
+ options.add_argument('--disable-blink-features=AutomationControlled')
30
+ options.add_experimental_option("excludeSwitches", ["enable-automation"])
31
+ options.add_experimental_option('useAutomationExtension', False)
32
+ service = ChromeService(ChromeDriverManager().install())
33
+ self.driver = webdriver.Chrome(service=service, options=options)
34
+ self.driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
35
+ 'source': "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
36
+ })
37
+ return True
38
+ except Exception as e:
39
+ print(f"CRITICAL ERROR in WebDriver Initialization: {e}")
40
+ self.micro_status(f"Error: Could not start browser. {e}")
41
+ return False
42
+
43
+ def micro_status(self, message):
44
+ print(f"[Bot Action] {message}")
45
+ with self.app.app_context():
46
+ self.socketio.emit('micro_status_update', {'message': message})
47
+
48
+ def stop(self):
49
+ self.micro_status("Termination signal received. Finishing current patient...")
50
+ self.termination_event.set()
51
+
52
+ def login(self, username, password):
53
+ try:
54
+ self.micro_status("Navigating to login page...")
55
+ self.driver.get("https://gateway.quantumepay.com/")
56
+ time.sleep(2)
57
+ self.micro_status("Entering credentials...")
58
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "Username"))).send_keys(username)
59
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "Password"))).send_keys(password)
60
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "login"))).click()
61
+ self.micro_status("Waiting for OTP screen...")
62
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "code1")))
63
+ return True
64
+ except Exception as e:
65
+ print(f"[Bot] ERROR during login: {e}")
66
+ return False
67
+
68
+ def submit_otp(self, otp):
69
+ try:
70
+ self.micro_status(f"Submitting OTP...")
71
+ otp_digits = list(otp)
72
+ for i in range(6):
73
+ self.driver.find_element(By.ID, f"code{i+1}").send_keys(otp_digits[i])
74
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "login"))).click()
75
+ self.micro_status("Verifying login success...")
76
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//span[text()='Payments']")))
77
+ return True
78
+ except Exception as e:
79
+ print(f"[Bot] ERROR during OTP submission: {e}")
80
+ return False
81
+
82
+ def process_patient_list(self, patient_list):
83
+ results = []
84
+ for index, patient_name in enumerate(patient_list):
85
+ if self.termination_event.is_set():
86
+ print("[Bot] Termination detected. Stopping process.")
87
+ break
88
+
89
+ with self.app.app_context():
90
+ self.socketio.emit('stats_update', {
91
+ 'processed': len(results),
92
+ 'remaining': len(patient_list) - len(results)
93
+ })
94
+
95
+ self.micro_status(f"Processing '{patient_name}' ({index + 1}/{len(patient_list)})...")
96
+ status = self._process_single_patient(patient_name)
97
+ results.append({'Name': patient_name, 'Status': status})
98
+
99
+ with self.app.app_context():
100
+ self.socketio.emit('log_update', {'name': patient_name, 'status': status})
101
+ return results
102
+
103
+ def _process_single_patient(self, patient_name):
104
+ try:
105
+ self.micro_status(f"Navigating to Void page for '{patient_name}'")
106
+ self.driver.get("https://gateway.quantumepay.com/credit-card/void")
107
+
108
+ search_successful = False
109
+ for attempt in range(15):
110
+ try:
111
+ self.micro_status(f"Searching for patient (Attempt {attempt + 1})...")
112
+ WebDriverWait(self.driver, 2).until(EC.presence_of_element_located((By.XPATH, "//div[contains(@class, 'table-wrapper')]")))
113
+ search_box = WebDriverWait(self.driver, 2).until(EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']")))
114
+ search_box.click(); time.sleep(0.5)
115
+ search_box.clear(); time.sleep(0.5)
116
+ search_box.send_keys(patient_name)
117
+ search_successful = True
118
+ break
119
+ except Exception: time.sleep(1)
120
+ if not search_successful: raise Exception("Failed to search for patient.")
121
+
122
+ time.sleep(3)
123
+ self.micro_status("Opening transaction details...")
124
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, f"//tr[contains(., \"{patient_name}\")]//button[@data-v-b6b33fa0]"))).click()
125
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.LINK_TEXT, "Transaction Detail"))).click()
126
+
127
+ self.micro_status("Adding to Vault...")
128
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Add to Vault']"))).click()
129
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//div[@class='modal-footer']//button/span[normalize-space()='Confirm']"))).click()
130
+
131
+ try:
132
+ self.micro_status("Verifying success and saving...")
133
+ company_input = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.NAME, "company_name")))
134
+ company_input.clear()
135
+ company_input.send_keys(patient_name)
136
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Save Changes']"))).click()
137
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Confirm']]"))).click()
138
+ time.sleep(5)
139
+ return 'Done'
140
+ except TimeoutException:
141
+ self.micro_status(f"'{patient_name}' is in a bad state, cancelling.")
142
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Cancel']]"))).click()
143
+ return 'Bad'
144
+ except Exception as e:
145
+ print(f"An error occurred while processing {patient_name}: {e}")
146
+ return 'Error'
147
+
148
+ def shutdown(self):
149
+ if self.driver:
150
+ self.driver.quit()