test-automation / app.py
tusharnik97's picture
Update app.py
4843ee3 verified
import os, json, time, random, psycopg2, requests, datetime, sys
from threading import Thread
from flask import Flask, request, send_from_directory
from fpdf import FPDF
from dotenv import load_dotenv
from huggingface_hub import InferenceClient
from concurrent.futures import ThreadPoolExecutor
# Create a high-speed background lane for WhatsApp messages
# This ensures sending a message never slows down your bot
message_pool = ThreadPoolExecutor(max_workers=20)
load_dotenv()
app = Flask(__name__)
# Create a high-performance session to Meta
# This keeps the connection 'hot' so it never times out
meta_session = requests.Session()
meta_session.headers.update({
"Authorization": f"Bearer {os.getenv('WHATSAPP_TOKEN').strip()}",
"Content-Type": "application/json"
})
# --- PREVENT RETRY SPAM ---
processed_msg_ids = set()
if not os.path.exists('receipts'): os.makedirs('receipts')
# Create a persistent session for Meta to speed up connections
http_session = requests.Session()
# ================= CONFIGURATION (META API) =================
WHATSAPP_TOKEN = os.getenv('WHATSAPP_TOKEN', '').strip()
PHONE_NUMBER_ID = os.getenv('PHONE_NUMBER_ID', '').strip()
VERIFY_TOKEN = os.getenv('VERIFY_TOKEN', '').strip()
ORS_API_KEY = os.getenv('ORS_API_KEY')
DATABASE_URL = os.getenv('DATABASE_URL')
MY_NGROK_URL = os.getenv('MY_NGROK_URL') # Your HF Space URL
HF_TOKEN = os.getenv('HF_TOKEN')
hf_client = InferenceClient(token=HF_TOKEN)
def log(msg):
print(f"--- {msg} ---", flush=True)
# ================= DATABASE HELPERS =================
def db_query(query, params=(), fetchone=False, commit=False):
conn = None
try:
conn = psycopg2.connect(DATABASE_URL, connect_timeout=10, options="-c statement_timeout=10000")
c = conn.cursor()
c.execute(query, params)
if commit: conn.commit()
res = None
if query.strip().upper().startswith("SELECT"):
res = c.fetchone() if fetchone else c.fetchall()
return res
except Exception as e:
log(f"DATABASE ERROR: {e}")
return None
finally:
if conn: conn.close()
# ================= META SENDER (v18.0) =================
def send_whatsapp_message(to_phone, message_text=None, media_url=None, template_name=None, components=None):
token = str(WHATSAPP_TOKEN).strip()
phone_id = str(PHONE_NUMBER_ID).strip()
url = f"https://graph.facebook.com/v18.0/{phone_id}/messages"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
clean_phone = ''.join(filter(str.isdigit, str(to_phone)))
if template_name:
payload = {
"messaging_product": "whatsapp",
"to": clean_phone,
"type": "template",
"template": {
"name": template_name,
"language": {"code": "en"},
"components": components
}
}
elif media_url:
payload = {
"messaging_product": "whatsapp",
"to": clean_phone,
"type": "document",
"document": {"link": media_url, "filename": "Invoice.pdf", "caption": message_text}
}
else:
payload = {
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": clean_phone,
"type": "text",
"text": {"body": message_text}
}
try:
# Increase timeout to 15 seconds. Since this is in a Thread,
# it won't block your bot.
print(f"--- [DEBUG] ATTEMPTING SEND TO {clean_phone} ---")
response = requests.post(url, headers=headers, json=payload, timeout=15, verify=False)
# THIS LINE IS CRITICAL: It will tell us if Meta is happy
print(f"--- [META RESPONSE] Status: {response.status_code} | Body: {response.text} ---")
return True
except Exception as e:
print(f"--- [SYSTEM ERROR] Send failed: {e} ---")
return False # Keep bot moving even if Meta is slow
# Keep bot moving even if Meta is slow
# ================= MAP & RECEIPT HELPERS =================
def get_location_suggestions(text):
try:
# We give the map only 2 seconds. verify=False speeds up the cloud handshake.
url = f"https://api.openrouteservice.org/geocode/autocomplete?api_key={ORS_API_KEY}&text={text}&boundary.country=IN&size=5"
res = requests.get(url, timeout=2.0, verify=False).json()
if 'features' in res:
return [f['properties']['label'] for f in res['features']]
return []
except Exception as e:
print(f"--- [MAP LOG] Timeout or Error: {e} ---")
return [] # Return empty if slow so bot doesn't time out
def get_real_distance(start, end):
try:
ctx = ", Maharashtra, India"
u1 = f"https://api.openrouteservice.org/geocode/search?api_key={ORS_API_KEY}&text={start+ctx}&size=1"
c1 = requests.get(u1, timeout=5).json()['features'][0]['geometry']['coordinates']
u2 = f"https://api.openrouteservice.org/geocode/search?api_key={ORS_API_KEY}&text={end+ctx}&size=1"
c2 = requests.get(u2, timeout=5).json()['features'][0]['geometry']['coordinates']
d_u = f"https://api.openrouteservice.org/v2/directions/driving-car?api_key={ORS_API_KEY}&start={c1[0]},{c1[1]}&end={c2[0]},{c2[1]}"
return round(requests.get(d_u, timeout=5).json()['features'][0]['properties']['segments'][0]['distance'] / 1000, 2)
except: return 5.0
def generate_pdf_receipt(phone, pickup, drop, fare, car, pay_method, customer_name):
try:
# Safety: Ensure no None values are passed to PDF
name = str(customer_name) if customer_name else "Guest"
pickup_loc = str(pickup) if pickup else "N/A"
drop_loc = str(drop) if drop else "N/A"
car_cat = str(car) if car else "Standard"
amount = str(fare) if fare else "0"
pdf = FPDF()
pdf.add_page()
pdf.set_font("Arial", 'B', 16)
pdf.cell(200, 10, txt="CabBot Pro Official Receipt", ln=True, align='C')
pdf.set_font("Arial", size=12)
pdf.ln(10)
pdf.cell(200, 10, txt=f"Ride ID: {int(time.time())}", ln=True)
pdf.cell(200, 10, txt=f"Customer Name: {name}", ln=True)
pdf.cell(200, 10, txt=f"Phone: {phone}", ln=True)
pdf.cell(200, 10, txt=f"Pickup: {pickup_loc}", ln=True)
pdf.cell(200, 10, txt=f"Drop: {drop_loc}", ln=True)
pdf.cell(200, 10, txt=f"Car Category: {car_cat}", ln=True)
pdf.set_font("Arial", 'B', 12)
pdf.cell(200, 10, txt=f"Total Amount: INR {amount}", ln=True)
pdf.ln(10)
pdf.set_font("Arial", 'I', 10)
if str(pay_method).lower() == "cash":
pdf.cell(200, 10, txt="Note: Please pay this amount directly to the driver.", ln=True)
else:
pdf.cell(200, 10, txt="Note: This amount has been paid online via CabBot Pay.", ln=True)
f_name = f"receipt_{int(time.time())}.pdf"
pdf.output(f"receipts/{f_name}")
return f_name
except Exception as e:
log(f"PDF GENERATION ERROR: {e}")
raise e
# ================= BACKGROUND WORKFLOW =================
def driver_workflow(phone, ride_id, pickup, drop, fare, car, pay_method, customer_name):
log(f"DRIVER THREAD STARTED FOR #{ride_id}")
try:
# 1. Simulate Driver Search
time.sleep(5)
otp = str(random.randint(1000, 9999))
db_query("UPDATE rides SET otp=%s, status='MATCHED' WHERE id=%s", (otp, ride_id), commit=True)
msg_match = (
f"πŸš• *Driver Found!*\n\n"
f"Mark is arriving in a {car}.\n"
f"πŸ”‘ *Safety OTP:* {otp}\n\n"
f"πŸ“ *Track Live:* {MY_NGROK_URL}/track/{ride_id}"
)
send_whatsapp_message(phone, msg_match)
# (Wait for Driver arrival and trip finish - handled by buttons now)
# Note: If you are still using this thread to send messages,
# apply the same ( ) multi-line string format as above.
except Exception as e:
log(f"Driver Workflow Error: {e}")
def broadcast_to_drivers(ride_id, pickup, drop, fare, car_type, customer_name):
"""Searches for available drivers and messages them in the background"""
try:
# Find all available drivers
drivers = db_query("SELECT name, phone FROM drivers WHERE status='AVAILABLE'")
if drivers:
print(f"--- [THREAD] BROADCASTING TO {len(drivers)} DRIVERS ---")
for d_name, d_phone in drivers:
# 1. Create the variables for the 'driver_broadcast' template
comp_driver = [
{
"type": "body",
"parameters": [
{"type": "text", "text": str(customer_name)}, # {{1}} Customer Name
{"type": "text", "text": str(pickup)}, # {{2}} Pickup
{"type": "text", "text": str(drop)}, # {{3}} Drop
{"type": "text", "text": str(fare)} # {{4}} Fare
]
},
{
"type": "button",
"sub_type": "url",
"index": "0",
"parameters": [{"type": "text", "text": f"{ride_id}?d_phone={d_phone}"}] # {{1}} URL Suffix
}
]
# 2. Call the sender using the template
send_whatsapp_message(d_phone, template_name="driver_broadcast_v2", components=comp_driver)
else:
print("--- [THREAD] No drivers found in database ---")
except Exception as e:
print(f"--- [THREAD ERROR] Broadcast failed: {e} ---")
def confirm_and_broadcast_thread(phone, ride_id, user_name):
"""Background worker to send customer confirmation and alert drivers"""
try:
# 1. Send confirmation to the Customer first
msg = f"πŸš• *Hi {user_name}, Confirmed!* Searching for nearby drivers... Please wait."
send_whatsapp_message(phone, msg)
# 2. Fetch the latest ride details from DB
r = db_query("SELECT pickup, drop_off, fare, car_type FROM rides WHERE id=%s", (ride_id,), fetchone=True)
if r:
# 3. Use your existing broadcast function to alert the drivers
# Indices: r[0]=pickup, r[1]=drop, r[2]=fare, r[3]=car_type
broadcast_to_drivers(ride_id, r[0], r[1], r[2], r[3], user_name)
except Exception as e:
print(f"--- [CRITICAL] Confirm & Broadcast Thread Failed: {e} ---")
# ================= MAIN BOT LOGIC =================
def handle_bot_logic(phone, msg_body, first_name):
try:
intent = "UNKNOWN"
# 1. FETCH RIDE: Isolated by phone
# We added 'driver_phone' to the end of the SELECT list
active_ride = db_query("SELECT id, status, pickup, fare, temp_suggestions, customer_name, driver_phone FROM rides WHERE phone=%s AND status NOT IN ('COMPLETED', 'CANCELLED') ORDER BY id DESC LIMIT 1", (str(phone),), fetchone=True)
reply_text = ""
# 🚨 SAFETY SOS
if "sos" in msg_body or "emergency" in msg_body:
if active_ride:
alert = f"🚨 *SOS* 🚨\nUser: {first_name} ({phone})\nRide: #{active_ride[0]}\nTrack: {MY_NGROK_URL}/track/{active_ride[0]}"
send_whatsapp_message("919819342854", alert) # Admin
reply_text = "🚨 *SOS Activated.* Help is on the way."
else:
reply_text = "SOS received, but no active ride found. Call 100."
send_whatsapp_message(phone, reply_text)
return
if not msg_body.isdigit():
intent = get_ai_intent(msg_body)
# 2. START / NEW / HI LOGIC
if msg_body in ["hi", "new", "start", "book"]:
if active_ride:
db_query("UPDATE rides SET status='CANCELLED' WHERE id=%s", (active_ride[0],), commit=True)
# 1. DO DATABASE WORK FIRST
db_query("INSERT INTO rides (phone, customer_name, status, created_at) VALUES (%s, %s, %s, %s)", (str(phone), first_name, "AWAITING_PICKUP", str(datetime.datetime.now())), commit=True)
# 2. WAIT 1 SECOND FOR DATABASE TO FINISH
time.sleep(1)
# 3. THEN SEND MESSAGE
reply_text = f"πŸš• *Hi {first_name}! Welcome to CabBot Pro* πŸš•\n\nWhere should we pick you up?"
send_whatsapp_message(phone, reply_text)
return
# ... after your Start/Hi logic block ...
# --- 🚨 CANCELLATION LOGIC (PASTE THIS HERE) ---
if "cancel" in msg_body or intent == "CANCELLING":
if active_ride:
ride_id = active_ride[0]
current_status = active_ride[1]
# driver_phone is now at index 6 because we updated the SELECT query
d_phone = active_ride[6]
# 1. Update ride status in Database
db_query("UPDATE rides SET status='CANCELLED' WHERE id=%s AND phone=%s", (ride_id, phone), commit=True)
reply_text = f"❌ *Ride #{ride_id} has been cancelled.* I hope to serve you again soon!"
# 2. Notify the Driver if they were already assigned
if d_phone:
driver_alert = f"⚠️ *RIDE CANCELLED* ⚠️\n\nCustomer {first_name} has cancelled Ride #{ride_id}. You are now AVAILABLE for new bookings."
send_whatsapp_message(d_phone, driver_alert)
# Set driver status back to available in the drivers table
db_query("UPDATE drivers SET status='AVAILABLE' WHERE phone=%s", (d_phone,), commit=True)
else:
reply_text = "You don't have any active rides to cancel."
send_whatsapp_message(phone, reply_text)
return # This ensures the code stops here and doesn't run further
# -----------------------------------------------
# ... the rest of your elif active_ride logic follows below ...
# 3. WORKFLOW PROGRESSION
elif active_ride:
ride_id, state, pickup, stored_km, temp_sugg, db_name, car_cat = active_ride
user_display_name = db_name if db_name else first_name
# Number Selection (1-5)
if msg_body.isdigit() and state in ["SUGGEST_PICKUP", "SUGGEST_DROP"]:
suggs = json.loads(temp_sugg) if temp_sugg else []
idx = int(msg_body) - 1
if 0 <= idx < len(suggs):
selected = suggs[idx]
if state == "SUGGEST_PICKUP":
db_query("UPDATE rides SET pickup=%s, status='AWAITING_DROP' WHERE id=%s AND phone=%s", (selected, ride_id, phone), commit=True)
reply_text = f"βœ… *Pickup:* {selected}\n\nNow, where are you going?"
else:
km = get_real_distance(pickup, selected)
p_m, p_s, p_suv = round(50+km*12), round(70+km*18), round(100+km*25)
db_query("UPDATE rides SET drop_off=%s, fare=%s, status='AWAITING_CAR' WHERE id=%s AND phone=%s", (selected, km, ride_id, phone), commit=True)
reply_text = f"🏁 Route: {km} km\n\n*Choose Car:*\n1. Mini (β‚Ή{p_m})\n2. Sedan (β‚Ή{p_s})\n3. SUV (β‚Ή{p_suv})"
else: reply_text = "Invalid choice. Choose 1-5."
# Text Search Logic
elif state in ["AWAITING_PICKUP", "SUGGEST_PICKUP", "AWAITING_DROP", "SUGGEST_DROP"]:
suggs = get_location_suggestions(msg_body)
if suggs:
n_s = "SUGGEST_PICKUP" if "PICKUP" in state else "SUGGEST_DROP"
db_query("UPDATE rides SET temp_suggestions=%s, status=%s WHERE id=%s AND phone=%s", (json.dumps(suggs), n_s, ride_id, phone), commit=True)
reply_text = "πŸ” *Select number:*\n\n" + "\n".join([f"{i+1}. {s}" for i,s in enumerate(suggs)])
else:
reply_text = "Location not found. Try a different name."
# Car Category Selection
elif state == "AWAITING_CAR":
km = float(stored_km or 5.0)
cars = {"1": ("Mini", round(50+km*12)), "2": ("Sedan", round(70+km*18)), "3": ("SUV", round(100+km*25))}
if msg_body in cars:
name, price = cars[msg_body]
db_query("UPDATE rides SET car_type=%s, fare=%s, status='AWAITING_PAY_METHOD' WHERE id=%s AND phone=%s", (name, price, ride_id, phone), commit=True)
reply_text = f"πŸš– *{name}* (β‚Ή{price})\n\nSelect Payment:\n1. Cash\n2. Online"
else: reply_text = "Choose 1, 2, or 3."
# Payment Method Selection
elif state == "AWAITING_PAY_METHOD":
methods = {"1": "cash", "2": "online"}
if msg_body in methods:
db_query("UPDATE rides SET pay_method=%s, status='AWAITING_CONFIRMATION' WHERE id=%s AND phone=%s", (methods[msg_body], ride_id, phone), commit=True)
reply_text = f"βœ… Method: {methods[msg_body].upper()}\nReply YES to confirm cab."
else: reply_text = "Choose 1 or 2."
# YES Confirmation (The Broadcast Trigger)
elif state == "AWAITING_CONFIRMATION" and "yes" in msg_body:
# 1. Update status in DB instantly (Takes 0.1 seconds)
db_query("UPDATE rides SET status='SEARCHING' WHERE id=%s AND phone=%s", (ride_id, phone), commit=True)
# 2. START THE MASTER BACKGROUND PROCESS
# This handles ALL messages so this function can finish immediately
Thread(target=confirm_and_broadcast_thread, args=(phone, ride_id, user_display_name)).start()
print(f"--- [LOG] Confirmation Thread Started for {phone} ---")
return # Exit immediately to prevent timeout
if reply_text:
time.sleep(1) # Gap to stabilize connection
send_whatsapp_message(phone, reply_text)
except Exception as e:
log(f"Logic Error: {e}") # THIS CLOSES THE TRY BLOCK AND FIXES THE SYNTAX ERROR
# ================= WEBHOOK (META) =================
# Create a small list to remember recently processed message IDs
# Create a small list to remember recently processed message IDs (Duplicate Guard)
# Create a list to keep track of processed message IDs (at the top of your file)
processed_msg_ids = set()
@app.route("/whatsapp_secure_0526_tushar_bot", methods=['GET', 'POST'])
def whatsapp_bot():
if request.method == 'GET':
if request.args.get('hub.verify_token') == VERIFY_TOKEN: return request.args.get('hub.challenge'), 200
return 'Fail', 403
data = request.get_json()
try:
val = data['entry'][0]['changes'][0]['value']
if 'messages' in val:
msg_body = val['messages'][0]['text']['body'].strip().lower()
phone = val['messages'][0]['from']
profile_name = val['contacts'][0]['profile']['name']
first_name = profile_name.split(' ')[0] if profile_name else "Guest"
# --- START LOGIC IN BACKGROUND ---
# This returns "OK" to Meta instantly while the work happens behind the scenes
Thread(target=handle_bot_logic, args=(phone, msg_body, first_name)).start()
except Exception as e:
print(f"Webhook Error: {e}")
# ALWAYS return 200 OK immediately (Under 1 second)
return "OK", 200 # Immediate 200 OK to Meta
# ================= SYSTEM ROUTES =================
@app.route("/")
def home(): return "<h1>πŸš– CabBot Pro is Online</h1>"
# --- HEARTBEAT TO KEEP SERVER AWAKE ---
@app.route("/heartbeat")
def heartbeat():
return "Bot is Awake", 200
# ================= ADMIN UI =================
# ================= ADMIN UI =================
@app.route("/admin")
def admin():
try:
# 1. Fetch rides including 'customer_name' (index 9)
rides = db_query("SELECT id, phone, status, pickup, drop_off, fare, car_type, pay_method, created_at, customer_name FROM rides ORDER BY id DESC")
if rides is None:
rides = []
# 2. Calculate Dashboard Stats
total_rides = len(rides)
completed_count = len([r for r in rides if r[2] == 'COMPLETED'])
active_count = len([r for r in rides if r[2] not in ['COMPLETED', 'CANCELLED']])
total_revenue = sum([r[5] for r in rides if r[2] == 'COMPLETED' and r[5] is not None])
# 3. Generate Table Rows
rows_html = ""
for r in rides:
status = r[2]
badge_class = "bg-secondary"
if status == "COMPLETED": badge_class = "bg-success"
elif status == "CANCELLED": badge_class = "bg-danger"
elif "AWAITING" in status: badge_class = "bg-warning text-dark"
elif status == "MATCHED": badge_class = "bg-primary"
elif status == "ARRIVED": badge_class = "bg-info text-white"
elif status == "BOOKED": badge_class = "bg-dark"
# r[9] is the customer_name, r[1] is the phone
customer_name = r[9] if (len(r) > 9 and r[9]) else "Guest"
rows_html += f'''
<tr>
<td><span class="text-muted small">#{r[0]}</span></td>
<td><span class="fw-bold">{customer_name}</span><br><small class="text-muted">{r[1]}</small></td>
<td><span class="badge {badge_class}">{status}</span></td>
<td>
<div class="small">
<span class="text-success">●</span> <b>From:</b> {r[3] if r[3] else '---'}<br>
<span class="text-danger">●</span> <b>To:</b> {r[4] if r[4] else '---'}
</div>
</td>
<td><span class="badge border border-primary text-primary">{r[6] if r[6] else '---'}</span></td>
<td><span class="badge bg-light text-dark border">{r[7].upper() if r[7] else '---'}</span></td>
<td class="fw-bold text-dark">β‚Ή{int(r[5]) if r[5] else '0'}</td>
<td><small class="text-muted">{r[8][:16] if r[8] else '---'}</small></td>
</tr>
'''
return f'''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CabBot Pro | Cloud Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<style>
body {{ background-color: #f4f7f6; font-family: 'Inter', sans-serif; color: #333; }}
.navbar {{ background: #1a1d20; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }}
.stat-card {{ border: none; border-radius: 16px; box-shadow: 0 4px 20px rgba(0,0,0,0.05); transition: 0.3s ease; background: white; }}
.stat-card:hover {{ transform: translateY(-5px); box-shadow: 0 8px 25px rgba(0,0,0,0.1); }}
.table-container {{ background: white; border-radius: 20px; padding: 30px; margin-top: 30px; box-shadow: 0 10px 30px rgba(0,0,0,0.03); }}
.badge {{ padding: 10px 14px; border-radius: 8px; font-size: 0.75rem; letter-spacing: 0.5px; text-transform: uppercase; }}
.refresh-btn {{ border-radius: 10px; padding: 8px 20px; font-weight: 600; }}
</style>
</head>
<body>
<nav class="navbar navbar-dark py-3">
<div class="container">
<a class="navbar-brand d-flex align-items-center fw-bold fs-4" href="#">
<span class="me-2">πŸš–</span> CabBot Pro <span class="badge bg-warning text-dark ms-3" style="font-size: 10px;">Cloud v2.0</span>
</a>
<button onclick="location.reload()" class="btn btn-outline-light refresh-btn">πŸ”„ Refresh Dashboard</button>
</div>
</nav>
<div class="container mt-5">
<div class="row g-4 text-center">
<div class="col-6 col-md-3">
<div class="stat-card p-4">
<p class="text-muted mb-1 small fw-bold">TOTAL RIDES</p>
<h2 class="fw-bold m-0">{total_rides}</h2>
</div>
</div>
<div class="col-6 col-md-3">
<div class="stat-card p-4">
<p class="text-muted mb-1 small fw-bold">COMPLETED</p>
<h2 class="fw-bold text-success m-0">{completed_count}</h2>
</div>
</div>
<div class="col-6 col-md-3">
<div class="stat-card p-4">
<p class="text-muted mb-1 small fw-bold">ACTIVE</p>
<h2 class="fw-bold text-primary m-0">{active_count}</h2>
</div>
</div>
<div class="col-6 col-md-3">
<div class="stat-card p-4">
<p class="text-muted mb-1 small fw-bold">REVENUE</p>
<h2 class="fw-bold text-dark m-0">β‚Ή{int(total_revenue)}</h2>
</div>
</div>
</div>
<div class="table-container mb-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="fw-bold m-0">Live Operational Log</h4>
<span class="text-muted small">Connected to Supabase PostgreSQL</span>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle border-0">
<thead>
<tr class="text-muted small">
<th>ID</th><th>CUSTOMER</th><th>STATUS</th><th>ROUTE</th><th>CAR</th><th>PAYMENT</th><th>FARE</th><th>CREATED</th>
</tr>
</thead>
<tbody>
{rows_html if rows_html else '<tr><td colspan="8" class="text-center py-5 text-muted">No rides found</td></tr>'}
</tbody>
</table>
</div>
</div>
</div>
<footer class="text-center py-4 text-muted small">Designed for Tushar's Automation Systems &copy; 2026</footer>
<link href="https://cdn.jsdelivr.net/npm/@n8n/chat/dist/style.css" rel="stylesheet" />
<script type="module">
import {{ createChat }} from 'https://cdn.jsdelivr.net/npm/@n8n/chat/dist/chat.bundle.es.js';
createChat({{
webhookUrl: 'https://tushn.app.n8n.cloud/webhook/63396bb7-608a-469e-9cbc-4a9da7a8e134/chat'
}});
</script>
</body>
</html>
'''
except Exception as e:
return f'<div style="text-align:center;padding:50px;"><h2>Dashboard Error</h2><p>{str(e)}</p></div>'
# ================= LIVE TRACKING MAP ROUTE =================
@app.route("/track/<int:ride_id>")
def track_driver(ride_id):
# 1. Fetch details including 'status'
r = db_query("SELECT pickup, car_type, driver_phone, status FROM rides WHERE id=%s", (ride_id,), fetchone=True)
if not r:
return "<h1 style='text-align:center; margin-top:50px;'>Ride not found.</h1>", 404
current_status = r[3]
# 2. Check if the trip is already over
if current_status in ['COMPLETED', 'CANCELLED']:
# Define the message based on the status
heading = "Trip Finished βœ…" if current_status == 'COMPLETED' else "Trip Cancelled ❌"
sub_text = "This tracking link is no longer active for security reasons." if current_status == 'COMPLETED' else "This ride was cancelled by the user or the driver."
color = "#2ecc71" if current_status == 'COMPLETED' else "#e74c3c"
return f'''
<!DOCTYPE html><html><head><meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {{ background:#f4f7f6; font-family:sans-serif; text-align:center; padding:50px 20px; }}
.card {{ background:white; border-radius:20px; padding:40px; box-shadow:0 10px 30px rgba(0,0,0,0.1); max-width:400px; margin:auto; }}
.icon {{ font-size:60px; margin-bottom:20px; }}
h2 {{ color:#2c3e50; margin-bottom:10px; }}
p {{ color:#7f8c8d; line-height:1.5; }}
.btn {{ display:inline-block; margin-top:30px; padding:12px 25px; background:{color}; color:white; text-decoration:none; border-radius:10px; font-weight:bold; }}
</style></head>
<body>
<div class="card">
<div class="icon">{"🏁" if current_status == 'COMPLETED' else "🚫"}</div>
<h2>{heading}</h2>
<p>{sub_text}</p>
<p style="font-size:13px;">Ride ID: #{ride_id}</p>
<a href="/" class="btn">Close Window</a>
</div>
</body></html>
'''
# 3. If the trip is ACTIVE, show the map as before
html = '''
<!DOCTYPE html><html><head><title>Track My Ride</title><meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
body { margin: 0; font-family: sans-serif; background: #f8f9fa; }
#map { height: 60vh; width: 100%; z-index: 1; }
.bottom-panel {
position: fixed; bottom: 0; width: 100%; background: white;
border-radius: 25px 25px 0 0; padding: 20px; z-index: 1000;
box-shadow: 0 -10px 30px rgba(0,0,0,0.1); box-sizing: border-box;
}
.status-row { display: flex; align-items: center; margin-bottom: 10px; }
.dot { height: 12px; width: 12px; background: #2ecc71; border-radius: 50%; margin-right: 10px; animation: blink 1.5s infinite; }
@keyframes blink { 50% { opacity: 0.3; } }
.vh-info { color: #6c757d; font-size: 14px; margin-bottom: 15px; }
.pickup-box { background: #f8f9fa; padding: 12px; border-radius: 10px; margin-bottom: 15px; font-size: 14px; border-left: 4px solid #2ecc71; }
.btn {
display: block; width: 100%; padding: 15px; border-radius: 12px;
text-decoration: none; text-align: center; font-weight: bold;
margin-top: 10px; border: none; font-size: 15px; color: white;
}
.btn-dark { background: #212529; }
.btn-safety { background: #25D366; }
</style>
</head>
<body>
<div id="map"></div>
<div class="bottom-panel">
<div class="status-row">
<div class="dot"></div>
<h4 style="margin:0; font-weight:700;">Driver is arriving...</h4>
</div>
<div class="vh-info">Vehicle: White {{CAR}} (MH-12-AB-1234)</div>
<div class="pickup-box">
<b>Pickup:</b><br>{{PICKUP}}
</div>
<a href="whatsapp://send?text=Track my ride live: {{URL}}/track/{{ID}}" class="btn btn-safety">
Share Trip with Family πŸ›‘οΈ
</a>
<button onclick="cancelRide()" id="canB" class="btn" style="background:#e74c3c; color:white;">CANCEL RIDE ❌</button>
<a href="tel:{{D_PHONE}}" class="btn btn-dark">πŸ“ž Call Driver</a>
</div>
<script>
var map = L.map('map', {zoomControl: false}).setView([18.5204, 73.8567], 15);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
var carIcon = L.icon({ iconUrl: 'https://cdn-icons-png.flaticon.com/512/744/744465.png', iconSize: [45, 45] });
var userIcon = L.icon({ iconUrl: 'https://cdn-icons-png.flaticon.com/512/1673/1673188.png', iconSize: [40, 40] });
L.marker([18.5204, 73.8567], {icon: userIcon}).addTo(map);
var carMarker = L.marker([0, 0], {icon: carIcon}).addTo(map);
function updateGPS() {
fetch('/api/get-driver-location/{{ID}}')
.then(res => res.json())
.then(data => {
if (data.lat && data.lng) {
carMarker.setLatLng([data.lat, data.lng]);
map.panTo([data.lat, data.lng]);
}
});
}
function cancelRide() {
if(confirm("Are you sure you want to cancel this ride?")) {
const btn = document.getElementById("canB");
btn.innerText = "CANCELLING...";
btn.disabled = true;
fetch('/api/cancel-ride/{{ID}}', {method:'POST'})
.then(res => {
if(res.ok) {
alert("Ride Cancelled successfully.");
window.location.href = "/";
} else {
alert("Cannot cancel. Ride might be completed.");
location.reload();
}
});
}
}
setInterval(updateGPS, 4000);
</script>
</body></html>
'''
driver_phone = str(r[2]).replace('whatsapp:', '').replace('+', '') if r[2] else ""
return html.replace("{{CAR}}", str(r[1])).replace("{{PICKUP}}", str(r[0])).replace("{{URL}}", MY_NGROK_URL).replace("{{ID}}", str(ride_id)).replace("{{D_PHONE}}", driver_phone)
# ================= PAYMENT ROUTES (ADD HERE) =================
@app.route("/pay-now")
def pay_now():
# We now get the data from the DB using the ID instead of the URL
ride_id = request.args.get('id')
# Fetch phone and fare from DB using the ID
r = db_query("SELECT phone, fare FROM rides WHERE id=%s", (ride_id,), fetchone=True)
if not r:
return "<h1>Ride not found.</h1>", 404
phone = r[0]
fare_val = r[1]
html = '''
<div style="text-align:center; padding-top:100px; font-family:sans-serif;">
<div style="border:1px solid #ddd; display:inline-block; padding:30px; border-radius:15px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); max-width:350px;">
<h2 style="color:#2ecc71; margin-bottom:5px;">CabBot Pay</h2>
<hr style="border:0; border-top:1px solid #eee; margin:20px 0;">
<p><b>Ride ID:</b> #{{ID}}</p>
<p style="color:gray;">Total Amount: <b>β‚Ή{{FARE}}</b></p>
<button onclick="location.href='/confirm-payment?phone={{PHONE}}&id={{ID}}'"
style="padding:15px 30px; background:#2ecc71; color:white; border:none; border-radius:8px; font-size:16px; cursor:pointer; width:100%; font-weight:bold;">
COMPLETE PAYMENT (DEMO)
</button>
</div>
</div>'''
return html.replace("{{ID}}", str(ride_id)).replace("{{FARE}}", str(fare_val)).replace("{{PHONE}}", str(phone).replace('+', '%2B'))
@app.route("/confirm-payment")
def confirm_payment():
try:
raw_phone = request.args.get('phone', '')
# Clean phone to match DB format (9198...)
clean_p = ''.join(filter(str.isdigit, str(raw_phone)))
ride_id = request.args.get('id')
log(f"Confirming payment for Ride #{ride_id}")
# 1. Fetch details - Ensure index matches the PDF function call
# Index: 0=pickup, 1=drop, 2=fare, 3=car, 4=name
r = db_query("SELECT pickup, drop_off, fare, car_type, customer_name FROM rides WHERE id=%s AND phone=%s", (ride_id, clean_p), fetchone=True)
if not r:
log(f"Ride #{ride_id} not found in DB for {clean_p}")
return "<h1>Error: Ride record not found. Please check WhatsApp.</h1>"
# 2. Update status to COMPLETED
db_query("UPDATE rides SET status='COMPLETED' WHERE id=%s", (ride_id,), commit=True)
# 3. Generate Receipt (Passing all 7 required arguments)
# Note: We pass r[4] as the customer name
f_name = generate_pdf_receipt(clean_p, r[0], r[1], r[2], r[3], "online", r[4])
# 4. Send Success WhatsApp
msg = f"βœ… *Payment Successful!*\n\nThank you {r[4] if r[4] else ''} for riding with CabBot. Your official receipt is attached below:"
send_whatsapp_message(clean_p, msg, f"{MY_NGROK_URL}/receipts/{f_name}")
return "<h1>Payment Successful! You can close this tab and check WhatsApp.</h1>"
except Exception as e:
log(f"CRITICAL PAYMENT ERROR: {e}")
return f"<h1>Internal Error: {e}</h1>"
@app.route("/receipts/<path:path>")
def send_receipt(path): return send_from_directory('receipts', path)
# ================= REAL DRIVER LOGIC ROUTES =================
@app.route("/accept/<int:ride_id>")
def accept_ride(ride_id):
# This route must match: https://.../accept/{{1}}?d_phone={{2}}
d_phone = request.args.get('d_phone')
print(f"--- [LOG] ACCEPT ATTEMPT: Ride #{ride_id} by Driver {d_phone} ---")
# 1. Fetch the ride to check if it's still 'SEARCHING'
ride = db_query("SELECT status, phone, customer_name, pickup, fare, car_type, pay_method FROM rides WHERE id=%s", (ride_id,), fetchone=True)
if not ride:
return "<h1>Error: Ride not found in database.</h1>", 404
if ride[0] != 'SEARCHING':
return "<h1>This ride has already been accepted by another driver.</h1>"
# 2. Assign driver and set status to MATCHED
db_query("UPDATE rides SET status='MATCHED', driver_phone=%s WHERE id=%s", (d_phone, ride_id), commit=True)
# 3. Get driver name for the customer
d_info = db_query("SELECT name, car_details FROM drivers WHERE phone=%s", (d_phone,), fetchone=True)
driver_name = d_info[0] if d_info else "A driver"
# 4. Generate OTP
otp = str(random.randint(1000, 9999))
db_query("UPDATE rides SET otp=%s WHERE id=%s", (otp, ride_id), commit=True)
# 5. Notify the Customer using a Template (Hassle-free button)
comp_cust = [
{"type": "body", "parameters": [
{"type": "text", "text": str(driver_name)},
{"type": "text", "text": str(d_info[1] if d_info else "Cab")},
{"type": "text", "text": str(otp)}
]},
{"type": "button", "sub_type": "url", "index": "0", "parameters": [{"type": "text", "text": str(ride_id)}]}
]
send_whatsapp_message(ride[1], template_name="trip_assignment_details", components=comp_cust)
return f'''
<div style="text-align:center; padding-top:100px; font-family:sans-serif;">
<h2 style="color:#2ecc71;">Ride Accepted!</h2>
<p>Customer: {ride[2]}</p>
<hr>
<button onclick="location.href='/driver-panel/{ride_id}'" style="padding:20px; background:black; color:white; border-radius:10px; cursor:pointer;">
OPEN DRIVER CONTROL PANEL
</button>
</div>
'''
@app.route("/driver-panel/<int:ride_id>")
def driver_panel(ride_id):
# 1. Fetch latest data including status
r = db_query("SELECT customer_name, pickup, drop_off, status FROM rides WHERE id=%s", (ride_id,), fetchone=True)
if not r: return "Ride not found", 404
# Define curr_status from the database result (index 3)
curr_status = str(r[3])
# 2. Logic to disable buttons based on status
arr_disabled = "disabled" if curr_status in ['ARRIVED', 'STARTED', 'ON_TRIP', 'COMPLETED', 'CANCELLED'] else ""
# OTP box only shows when driver has 'ARRIVED'
show_otp = "block" if curr_status == 'ARRIVED' else "none"
# Start button only enabled after OTP verified ('STARTED' state)
start_disabled = "disabled" if curr_status != 'STARTED' else ""
# Finish button only enabled during the trip
fin_disabled = "disabled" if curr_status != 'ON_TRIP' else ""
gps_text = "GPS SHARING ACTIVE" if curr_status != 'SEARCHING' else "1. START LIVE GPS"
html = '''
<!DOCTYPE html><html><head><meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body{background:#1a1d20; color:white; font-family:sans-serif; text-align:center; padding:20px;}
.card{background:#2c3034; border-radius:15px; padding:20px; margin-top:20px; text-align:left; border:1px solid #444;}
.btn{width:100%; padding:18px; font-weight:bold; border-radius:12px; color:white; font-size:16px; cursor:pointer; margin-top:15px; border:none;}
.gps{background:#2ecc71;} .arrived{background:#3498db;} .start{background:#9b59b6;} .finish{background:#e67e22;}
.btn:disabled{background:#444 !important; color:#888 !important; cursor:not-allowed; opacity:0.6;}
input{width:100%; padding:15px; border-radius:10px; border:1px solid #555; background:#1a1d20; color:white; font-size:22px; text-align:center; margin-top:10px;}
</style></head>
<body>
<h2 style="font-weight:800;">πŸš– Driver Console</h2>
<div class="card">
<p style="margin:5px 0;"><b>Customer:</b> {{N}}</p>
<p style="margin:5px 0;"><b>Status:</b> <span style="color:#f1c40f;">{{ST}}</span></p>
<hr style="border-top:1px solid #444;">
<p style="font-size:13px; color:#bbb;"><b>From:</b> {{P}}</p>
<p style="font-size:13px; color:#bbb;"><b>To:</b> {{D}}</p>
</div>
<button id="gpsBtn" class="btn gps" onclick="startGps()">''' + gps_text + '''</button>
<button id="arrB" class="btn arrived" onclick="arrived()" ''' + arr_disabled + '''>2. I HAVE ARRIVED πŸ“</button>
<div id="otpSection" style="display:''' + show_otp + '''; margin-top:20px; background:rgba(52, 152, 219, 0.1); padding:15px; border-radius:10px; border: 1px dashed #3498db;">
<p style="margin:0; font-size:14px;">Enter Customer OTP:</p>
<input type="text" id="otpIn" placeholder="0000" maxlength="4">
<button class="btn arrived" style="margin-top:10px;" onclick="verifyOtp()">VERIFY OTP</button>
</div>
<button id="staB" class="btn start" onclick="startRide()" ''' + start_disabled + '''>3. START RIDE ▢️</button>
<button id="finB" class="btn finish" onclick="finishTrip()" ''' + fin_disabled + '''>4. TRIP FINISHED 🏁</button>
<script>
function arrived() { fetch('/api/driver-arrived/{{ID}}', {method:'POST'}).then(res => { if(res.ok) location.reload(); }); }
function verifyOtp() {
const otp = document.getElementById("otpIn").value;
fetch('/api/verify-otp/{{ID}}', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({otp: otp}) })
.then(res => { if(res.ok) { alert("OTP Verified!"); location.reload(); } else alert("Wrong OTP"); });
}
function startRide() { fetch('/api/start-ride/{{ID}}', {method:'POST'}).then(res => { if(res.ok) location.reload(); }); }
function finishTrip() { if(confirm("Finish trip?")) { fetch('/api/trip-finished/{{ID}}', {method:'POST'}).then(() => { alert("Done!"); location.reload(); }); } }
function startGps() { navigator.geolocation.watchPosition((pos) => { fetch('/api/update-location/{{ID}}', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({lat: pos.coords.latitude, lng: pos.coords.longitude}) }); }, null, {enableHighAccuracy:true}); document.getElementById("gpsBtn").innerText="GPS ACTIVE"; }
</script></body></html>'''
return html.replace("{{N}}", str(r[0])).replace("{{P}}", str(r[1])).replace("{{D}}", str(r[2])).replace("{{ID}}", str(ride_id)).replace("{{ST}}", curr_status)
@app.route("/api/update-location/<int:ride_id>", methods=['POST'])
def update_location(ride_id):
try:
data = request.get_json()
lat = data.get('lat')
lng = data.get('lng')
# Use %s for Supabase/Postgres
db_query("UPDATE rides SET driver_lat=%s, driver_lng=%s WHERE id=%s", (lat, lng, ride_id), commit=True)
return "OK", 200
except Exception as e:
print(f"Update GPS Error: {e}")
return "Error", 500
@app.route("/api/get-driver-location/<int:ride_id>")
def get_driver_location(ride_id):
# Pull real coordinates from Supabase
r = db_query("SELECT driver_lat, driver_lng FROM rides WHERE id=%s", (ride_id,), fetchone=True)
if r and r[0] and r[1]:
return json.dumps({"lat": r[0], "lng": r[1]})
return json.dumps({"lat": None, "lng": None})
@app.route("/api/driver-arrived/<int:ride_id>", methods=['POST'])
def driver_arrived_api(ride_id):
# 1. Update Database IMMEDIATELY (Takes 0.01 seconds)
r = db_query("SELECT status, phone, customer_name FROM rides WHERE id=%s", (ride_id,), fetchone=True)
if r and r[0] == 'MATCHED':
db_query("UPDATE rides SET status='ARRIVED' WHERE id=%s", (ride_id,), commit=True)
# 2. Send WhatsApp in a Background Thread
# The driver's screen will NOT wait for this to finish
name = r[2] if r[2] else "there"
msg = f"πŸš• *Hi {name}! Your driver is outside!* Please board the vehicle. πŸ“"
Thread(target=send_whatsapp_message, args=(r[1], msg)).start()
return "OK", 200
return "Already notified", 200
@app.route("/api/trip-finished/<int:ride_id>", methods=['POST'])
def api_trip_finished(ride_id):
# This also returns '200 OK' to the driver instantly
# All the heavy work (PDF, DB, WhatsApp) happens in the background
Thread(target=process_trip_completion, args=(ride_id,)).start()
return "OK", 200
return "Trip must be in 'ON_TRIP' state to finish", 400
# ================= BACKGROUND HELPERS =================
def process_trip_completion(ride_id):
try:
r = db_query("SELECT phone, pickup, drop_off, fare, car_type, pay_method, customer_name FROM rides WHERE id=%s", (ride_id,), fetchone=True)
if not r: return
phone, pickup, drop, fare, car, pay_method, name = r
if str(pay_method).lower() == "online":
db_query("UPDATE rides SET status='AWAITING_PAYMENT' WHERE id=%s", (ride_id,), commit=True)
comp_pay = [
{
"type": "body",
"parameters": [{"type": "text", "text": str(fare)}] # {{1}} Fare
},
{
"type": "button",
"sub_type": "url",
"index": "0",
"parameters": [{"type": "text", "text": str(ride_id)}]
}
]
send_whatsapp_message(phone, template_name="payment_request", components=comp_pay)
else:
db_query("UPDATE rides SET status='COMPLETED' WHERE id=%s", (ride_id,), commit=True)
f_name = generate_pdf_receipt(phone, pickup, drop, fare, car, "cash", name)
msg = (
f"🏁 *Trip Finished!*\n\n"
f"πŸ’° *Total Fare: β‚Ή{fare}*\n"
f"Please pay this amount in *CASH* directly to the driver.\n\n"
f"Thank you {name}! Your official receipt is attached below:"
)
send_whatsapp_message(phone, msg, f"{MY_NGROK_URL}/receipts/{f_name}")
except Exception as e:
print(f"Error in background trip completion: {e}")
@app.route("/api/verify-otp/<int:ride_id>", methods=['POST'])
def verify_otp_api(ride_id):
data = request.get_json()
driver_otp = data.get('otp')
# Fetch real OTP from Supabase
r = db_query("SELECT otp FROM rides WHERE id=%s", (ride_id,), fetchone=True)
if r and str(r[0]) == str(driver_otp):
# Update status to 'STARTED' - this unlocks the Start button
db_query("UPDATE rides SET status='STARTED' WHERE id=%s", (ride_id,), commit=True)
return "OK", 200
return "Invalid OTP", 401
@app.route("/api/start-ride/<int:ride_id>", methods=['POST'])
def start_ride_api(ride_id):
r = db_query("SELECT phone, customer_name FROM rides WHERE id=%s", (ride_id,), fetchone=True)
if r:
# Move to ON_TRIP status
db_query("UPDATE rides SET status='ON_TRIP' WHERE id=%s", (ride_id,), commit=True)
msg = f"πŸš• *Trip Started!*\n\nHave a safe journey, {r[1]}. I will notify you when we reach the destination."
Thread(target=send_whatsapp_message, args=(r[0], msg)).start()
return "OK", 200
return "Error", 400
@app.route("/api/cancel-ride/<int:ride_id>", methods=['POST'])
def api_cancel_ride(ride_id):
# 1. Check if ride can be cancelled (Not already finished)
r = db_query("SELECT phone, status, driver_phone FROM rides WHERE id=%s", (ride_id,), fetchone=True)
if r and r[1] not in ['COMPLETED', 'CANCELLED']:
# 2. Update status
db_query("UPDATE rides SET status='CANCELLED' WHERE id=%s", (ride_id,), commit=True)
# 3. Notify Customer on WhatsApp
send_whatsapp_message(r[0], "❌ *Ride Cancelled.* Your request via the live map has been processed.")
# 4. Notify Driver if matched
if r[2]:
send_whatsapp_message(r[2], f"⚠️ *Ride #{ride_id} Cancelled* by customer. You are now AVAILABLE.")
db_query("UPDATE drivers SET status='AVAILABLE' WHERE phone=%s", (r[2],), commit=True)
return "OK", 200
return "Cannot cancel ride", 400
if __name__ == "__main__":
app.run(host='0.0.0.0', port=7860)