"""customer_handler.py — Full state machine for every lead. States: FOUND → OUTREACH_SENT → DETAILS_REQUESTED → DEMO_BUILDING → DEMO_SENT → NEGOTIATING → PAYMENT_PENDING → PAYMENT_VERIFIED → DELIVERED / REJECTED """ import asyncio import logging from typing import Optional import ai_agent import database as db import email_manager import website_builder import netlify_deployer import scraper as scraper_mod from config import PAYPAL_EMAIL, BANK_INFO, SENDER_EMAILS log = logging.getLogger(__name__) _admin_notify_fn = None def set_admin_notifier(fn): global _admin_notify_fn _admin_notify_fn = fn async def _notify(text: str): if _admin_notify_fn: try: await _admin_notify_fn(text) except Exception as e: log.error("Admin notify error: %s", e) def _sender(lead) -> Optional[dict]: sid = lead["sender_email_id"] for a in SENDER_EMAILS: if a["id"] == sid: return a return None def _history(lead_id: int) -> str: logs = db.get_email_history(lead_id) if not logs: return "No prior conversation." parts = [] for e in logs[-6:]: who = "Us" if e["direction"] == "outbound" else "Customer" parts.append(f"[{who}]: {e['body'][:100]}...") return "\n".join(parts) async def _reply_to(lead, reply: dict, body: str, subject_prefix: str = "Re: "): subj = subject_prefix + reply["subject"] sender = _sender(lead) used = await email_manager.send_email(reply["from_email"], subj, body, sender=sender) if used: db.log_email(lead["id"], used, "outbound", subj, body) # ── Build and deploy demo site ──────────────────────────────────────────── async def _build_and_deploy(lead_id: int, extra_details: dict = None) -> tuple: """Returns (site_id, live_url, zip_path).""" lead = db.get_lead(lead_id) extra = extra_details or {} db.update_lead(lead_id, state="DEMO_BUILDING") # Scrape extra business details from Google Maps if not provided if not extra: log.info("🔍 Fetching business details for '%s'...", lead["business_name"]) try: extra = await scraper_mod.fetch_business_details_for_lead(lead_id) except Exception as e: log.warning("Could not fetch extra details: %s", e) # DeepSeek R1 generates content content = ai_agent.generate_site_content( business_name = lead["business_name"], category = lead["category"] or "business", address = lead["address"] or "", phone = lead["phone"] or "", email = lead["email"], business_hours = extra.get("hours",""), description = extra.get("description",""), social_links = [], ) # Build HTML site_dir = website_builder.build_site_from_content(lead_id, content) zip_path = website_builder.get_site_zip_path(site_dir) # Deploy to Netlify site_id, live_url = netlify_deployer.deploy_for_lead( lead_id, lead["business_name"], zip_path ) # Store zip path for possible re-deploy after payment db.update_lead(lead_id, state="DEMO_SENT", netlify_site_id=site_id, netlify_url=live_url, notes=zip_path) # reuse notes field for zip path return site_id, live_url, zip_path # ── Main reply handler ──────────────────────────────────────────────────── async def handle_reply(reply: dict): lead_id = reply["lead_id"] body = reply["body"] from_email = reply["from_email"] lead = db.get_lead(lead_id) if not lead: return # Log inbound db.log_email(lead_id, from_email, "inbound", reply["subject"], body, reply.get("message_id","")) intent = ai_agent.classify_reply(body) log.info("🧠 Lead #%d '%s' → %s", lead_id, lead["business_name"], intent) history = _history(lead_id) # ── NOT INTERESTED ──────────────────────────────────────────────────── if intent == "NEGATIVE": resp = ai_agent.compose_reply("NEGATIVE", lead["business_name"], history_summary=history) await _reply_to(lead, reply, resp) db.update_lead(lead_id, state="REJECTED") return # ── POSITIVE — ask for business details before building ─────────────── if intent == "POSITIVE": ask_body = ai_agent.generate_details_request_email(lead["business_name"]) await _reply_to(lead, reply, ask_body) db.update_lead(lead_id, state="DETAILS_REQUESTED") await _notify( f"✨ *Positive Reply!*\n" f"Business: *{lead['business_name']}*\n" f"Email: {from_email}\n" "Asked them for business details (photos, hours, description)." ) return # ── CUSTOMER SENDS DETAILS — build + deploy demo ────────────────────── if intent == "DETAILS" or ( lead["state"] == "DETAILS_REQUESTED" and intent == "NEUTRAL" ): await _notify( f"📋 *Details received from {lead['business_name']}*\n" "Building their demo website now..." ) try: _, live_url, _ = await _build_and_deploy(lead_id) except Exception as e: log.error("Build error for lead #%d: %s", lead_id, e) await _notify(f"⚠️ Build failed for lead #{lead_id}: {e}") return resp = ( f"Hi {lead['business_name']} team! 🎉\n\n" f"Your demo website is ready! Check it out here:\n\n" f"👉 {live_url}\n\n" "Let me know what you think! If you'd like any changes to colours, " "text, layout — just reply and I'll update it right away.\n\n" "If you're happy with it, I'll give you the final price to make it permanently yours!\n\n" "Best,\nWebReach Team" ) await _reply_to(lead, reply, resp) await _notify( f"🚀 *Demo Deployed!*\n" f"Business: *{lead['business_name']}*\n" f"URL: {live_url}" ) return # ── EDIT REQUEST ────────────────────────────────────────────────────── if intent == "EDIT_REQUEST": edits = ai_agent.extract_edit_instructions(body) db.update_lead(lead_id, state="DEMO_SENT", notes=(db.get_lead(lead_id)["notes"] or "") + f" | EDITS: {edits}") # Rebuild with same details but note the edits try: _, live_url, _ = await _build_and_deploy(lead_id) except Exception as e: log.error("Rebuild error: %s", e) return resp = ai_agent.compose_reply( "EDIT_REQUEST", lead["business_name"], netlify_url=live_url, extra=edits, history_summary=history ) await _reply_to(lead, reply, resp) return # ── SATISFIED — quote price ─────────────────────────────────────────── if intent == "SATISFIED": price = ai_agent.calculate_price( lead["business_name"], lead["category"] or "", history ) db.update_lead(lead_id, state="NEGOTIATING", price_quoted=price) resp = ai_agent.compose_reply( "SATISFIED", lead["business_name"], netlify_url=lead["netlify_url"] or "", price=price, history_summary=history ) await _reply_to(lead, reply, resp) return # ── PRICING QUESTION ────────────────────────────────────────────────── if intent == "PRICING": price = ai_agent.calculate_price( lead["business_name"], lead["category"] or "", history ) db.update_lead(lead_id, state="NEGOTIATING", price_quoted=price) resp = ai_agent.compose_reply( "PRICING", lead["business_name"], price=price, history_summary=history ) await _reply_to(lead, reply, resp) return # ── CUSTOMER CLAIMS PAID ────────────────────────────────────────────── if intent == "PAID": db.update_lead(lead_id, state="PAYMENT_PENDING") db.add_payment_request(lead_id, lead["price_quoted"] or 0) # Deploy the real site immediately (but it's not "approved" yet) live_url = lead["netlify_url"] or "" if not live_url: try: _, live_url, _ = await _build_and_deploy(lead_id) except Exception as e: log.error("Deploy error: %s", e) # Ask about domain (explain what it is simply) domain_ask = ( f"Hi {lead['business_name']} team!\n\n" "Thank you — we're verifying your payment now and will confirm shortly. ✅\n\n" "Quick question: do you have a **domain name** for your website?\n\n" "A domain is your website address — like *mybusiness.com* — " "so people can find you easily instead of a long link.\n\n" "If you have one, just reply with it and we'll connect it for you.\n" "If you don't have one yet, no worries — your site will work on our link for now, " "and we can help you get a domain later!\n\n" "Best,\nWebReach Team" ) await _reply_to(lead, reply, domain_ask) # Alert admin on Telegram await _notify( f"💰 *PAYMENT CLAIM*\n" f"Lead #: `{lead_id}`\n" f"Business: *{lead['business_name']}*\n" f"Customer: {from_email}\n" f"Amount: ${lead['price_quoted'] or '?':.0f}\n" f"Site: {live_url}\n\n" f"✅ /verify {lead_id} — payment confirmed\n" f"❌ /reject {lead_id} — not paid, block site" ) return # ── DOMAIN PROVIDED ─────────────────────────────────────────────────── if intent == "OWN_DOMAIN_PROVIDED": domain = ai_agent.extract_domain_from_reply(body) if domain: db.update_lead(lead_id, custom_domain=domain) resp = ai_agent.compose_reply( "OWN_DOMAIN_PROVIDED", lead["business_name"], extra=domain or "domain not clear", history_summary=history ) await _reply_to(lead, reply, resp) return # ── ASKING ABOUT DOMAIN ─────────────────────────────────────────────── if intent == "DOMAIN": resp = ai_agent.compose_reply("DOMAIN", lead["business_name"], history_summary=history) await _reply_to(lead, reply, resp) return # ── ASKING FOR SOURCE CODES ─────────────────────────────────────────── if intent == "CODES": if lead["payment_confirmed"]: # Payment already verified — send codes notice resp = ( f"Hi {lead['business_name']} team,\n\n" "Great news — since your payment is verified, we'll send over your website files shortly!\n\n" "We'll email them to you as a ZIP file within 24 hours.\n\n" "Best,\nWebReach Team" ) await _notify( f"📦 *Code Request — APPROVED*\n" f"Lead #{lead_id}: *{lead['business_name']}*\n" "Payment already verified. Please send them the ZIP." ) else: # Not yet verified — hold resp = ai_agent.compose_reply("CODES", lead["business_name"], history_summary=history) await _notify( f"⚠️ *Code Request — NOT VERIFIED*\n" f"Lead #{lead_id}: *{lead['business_name']}*\n" "Payment NOT confirmed. Holding codes." ) await _reply_to(lead, reply, resp) return # ── OTHER HOST ──────────────────────────────────────────────────────── if intent == "OTHER_HOST": resp = ai_agent.compose_reply("OTHER_HOST", lead["business_name"], history_summary=history) await _reply_to(lead, reply, resp) db.update_lead(lead_id, notes="Wants own hosting") return # ── NEUTRAL / FALLBACK ──────────────────────────────────────────────── resp = ai_agent.compose_reply("NEUTRAL", lead["business_name"], history_summary=history) await _reply_to(lead, reply, resp) # ── Admin: verify payment ───────────────────────────────────────────────── async def verify_payment(lead_id: int): """Admin confirmed payment — unblock/redeploy and deliver.""" lead = db.get_lead(lead_id) if not lead: return db.confirm_payment(lead_id) live_url = lead["netlify_url"] or "" # Attach custom domain if provided if lead["custom_domain"] and lead["netlify_site_id"]: try: live_url = netlify_deployer.set_custom_domain( lead["netlify_site_id"], lead["custom_domain"] ) db.update_lead(lead_id, netlify_url=live_url) except Exception as e: log.warning("Domain attach error: %s", e) # Email customer with live URL sender = _sender(lead) delivery_body = ( f"Hi {lead['business_name']} team! 🎉\n\n" "Your payment has been confirmed — your website is now fully LIVE!\n\n" f"👉 {live_url}\n\n" + (f"Your custom domain *{lead['custom_domain']}* has been connected. " "DNS changes can take up to 24 hours to fully propagate worldwide.\n\n" if lead["custom_domain"] else "") + "Your website will stay live. If you'd like any future changes, just reach out!\n\n" "Thank you for choosing us — we'd love a review when you get a chance! 😊\n\n" "Best,\nWebReach Team" ) used = await email_manager.send_email(lead["email"], "🎉 Your website is LIVE!", delivery_body, sender=sender) if used: db.log_email(lead_id, used, "outbound", "🎉 Your website is LIVE!", delivery_body) db.update_lead(lead_id, state="DELIVERED") log.info("🎉 Delivered to lead #%d", lead_id) await _notify( f"✅ *DELIVERED!*\n" f"Business: *{lead['business_name']}*\n" f"URL: {live_url}" ) # ── Admin: reject payment — block site ─────────────────────────────────── async def reject_payment(lead_id: int): """Admin said not paid — block the Netlify site.""" lead = db.get_lead(lead_id) if not lead: return if lead["netlify_site_id"]: try: netlify_deployer.block_site(lead["netlify_site_id"]) log.info("🔒 Site blocked for lead #%d", lead_id) except Exception as e: log.error("Block site error: %s", e) db.update_lead(lead_id, state="NEGOTIATING", notes="Payment rejected — site blocked") # Tell customer politely sender = _sender(lead) msg = ( f"Hi {lead['business_name']} team,\n\n" "We weren't able to verify your payment yet. " "The demo has been temporarily paused.\n\n" "Once payment is confirmed, we'll restore it immediately. " f"Please send payment to:\n" f"💳 PayPal: {PAYPAL_EMAIL}\n" f"🏦 {BANK_INFO}\n\n" "Reply once you've sent it and we'll reactivate your site right away!\n\n" "Best,\nWebReach Team" ) used = await email_manager.send_email(lead["email"], "Payment Verification Needed", msg, sender=sender) if used: db.log_email(lead_id, used, "outbound", "Payment Verification Needed", msg) await _notify(f"🔒 Site blocked for lead #{lead_id}: *{lead['business_name']}*") # ── Poll all replies ────────────────────────────────────────────────────── async def process_all_replies(): replies = await email_manager.check_all_replies() if replies: log.info("📬 Processing %d replies", len(replies)) for r in replies: await handle_reply(r) else: log.debug("📭 No new replies")