Spaces:
Running
Running
| 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() | |
| 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 ================= | |
| def home(): return "<h1>π CabBot Pro is Online</h1>" | |
| # --- HEARTBEAT TO KEEP SERVER AWAKE --- | |
| def heartbeat(): | |
| return "Bot is Awake", 200 | |
| # ================= ADMIN UI ================= | |
| # ================= ADMIN UI ================= | |
| 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 © 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 ================= | |
| 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) ================= | |
| 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')) | |
| 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>" | |
| def send_receipt(path): return send_from_directory('receipts', path) | |
| # ================= REAL DRIVER LOGIC ROUTES ================= | |
| 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> | |
| ''' | |
| 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) | |
| 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 | |
| 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}) | |
| 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 | |
| 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}") | |
| 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 | |
| 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 | |
| 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) |