Files
contexta_be/app/routers/leads.py
belviskhoremk 92d4c2fc5e feat: add appointments, campaigns, admin, storage, tests and various updates
- Add new routers: admin, appointments, campaigns
- Add storage service and logging config
- Add migrations directory and test suite with pytest config
- Add supabase_migration_features.sql
- Update models, dependencies, config, and existing routers
- Remove whatsapp_service (deleted)
- Update pyproject.toml and uv.lock dependencies

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 09:11:58 +00:00

185 lines
6.4 KiB
Python

from fastapi import APIRouter, HTTPException, Depends, Query
from fastapi.responses import StreamingResponse
from app.database import get_supabase
from app.dependencies import get_current_user
from app.models import LeadCreate, LeadResponse, LeadUpdate
from typing import List, Optional
import uuid
import csv
import io
import logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/leads", tags=["Leads"])
def _get_user_company_id(user_id: str, supabase) -> str:
result = supabase.table("companies").select("id").eq("owner_id", user_id).execute()
if not result.data:
raise HTTPException(status_code=404, detail="Company not found")
return result.data[0]["id"]
def _check_leads_access(user_id: str, supabase):
sub = supabase.table("subscriptions").select("plan").eq("user_id", user_id).eq("status", "active").execute()
plan = sub.data[0]["plan"] if sub.data else "free"
if plan not in ("starter", "business", "agency", "enterprise"):
raise HTTPException(status_code=402, detail="Lead capture requires Starter plan or higher")
return plan
@router.get("", response_model=List[LeadResponse])
async def list_leads(
chatbot_id: Optional[str] = Query(None),
page: int = Query(1, ge=1),
limit: int = Query(50, ge=1, le=200),
user=Depends(get_current_user),
):
"""List leads for the user's chatbots."""
supabase = get_supabase()
_check_leads_access(user.id, supabase)
company_id = _get_user_company_id(user.id, supabase)
# Get owned chatbot IDs
chatbots_q = supabase.table("chatbots").select("id").eq("company_id", company_id)
if chatbot_id:
chatbots_q = chatbots_q.eq("id", chatbot_id)
chatbots = chatbots_q.execute()
chatbot_ids = [c["id"] for c in (chatbots.data or [])]
if not chatbot_ids:
return []
offset = (page - 1) * limit
result = supabase.table("leads").select("*") \
.in_("chatbot_id", chatbot_ids) \
.order("created_at", desc=True) \
.range(offset, offset + limit - 1) \
.execute()
return [LeadResponse(**lead) for lead in (result.data or [])]
@router.patch("/{lead_id}", response_model=LeadResponse)
async def update_lead(
lead_id: str,
data: LeadUpdate,
user=Depends(get_current_user),
):
"""Update lead status or notes."""
supabase = get_supabase()
_check_leads_access(user.id, supabase)
company_id = _get_user_company_id(user.id, supabase)
lead = supabase.table("leads").select("*, chatbots(company_id)") \
.eq("id", lead_id).execute()
if not lead.data:
raise HTTPException(status_code=404, detail="Lead not found")
if lead.data[0].get("chatbots", {}).get("company_id") != company_id:
raise HTTPException(status_code=403, detail="Not authorized")
update_fields: dict = {}
if data.status is not None:
valid_statuses = ("new", "contacted", "qualified", "closed", "lost")
if data.status not in valid_statuses:
raise HTTPException(status_code=400, detail=f"Status must be one of {valid_statuses}")
update_fields["status"] = data.status
if data.notes is not None:
update_fields["notes"] = data.notes
if not update_fields:
return LeadResponse(**lead.data[0])
result = supabase.table("leads").update(update_fields).eq("id", lead_id).execute()
return LeadResponse(**result.data[0])
@router.get("/export")
async def export_leads_csv(
chatbot_id: Optional[str] = Query(None),
user=Depends(get_current_user),
):
"""Export leads as CSV file."""
supabase = get_supabase()
_check_leads_access(user.id, supabase)
company_id = _get_user_company_id(user.id, supabase)
chatbots_q = supabase.table("chatbots").select("id").eq("company_id", company_id)
if chatbot_id:
chatbots_q = chatbots_q.eq("id", chatbot_id)
chatbots = chatbots_q.execute()
chatbot_ids = [c["id"] for c in (chatbots.data or [])]
if not chatbot_ids:
leads_data = []
else:
result = supabase.table("leads").select("*") \
.in_("chatbot_id", chatbot_ids) \
.order("created_at", desc=True) \
.execute()
leads_data = result.data or []
# Build CSV
output = io.StringIO()
writer = csv.DictWriter(output, fieldnames=["id", "chatbot_id", "email", "name", "phone", "company", "created_at"])
writer.writeheader()
for lead in leads_data:
writer.writerow({
"id": lead.get("id", ""),
"chatbot_id": lead.get("chatbot_id", ""),
"email": lead.get("email", ""),
"name": lead.get("name", ""),
"phone": lead.get("phone", ""),
"company": lead.get("company", ""),
"created_at": lead.get("created_at", ""),
})
csv_bytes = output.getvalue().encode("utf-8")
return StreamingResponse(
iter([csv_bytes]),
media_type="text/csv",
headers={"Content-Disposition": "attachment; filename=leads.csv"},
)
# Public endpoint — no auth required
leads_public_router = APIRouter(tags=["Leads"])
@leads_public_router.post("/chatbots/{chatbot_id}/leads", response_model=LeadResponse, status_code=201)
async def submit_lead(chatbot_id: str, data: LeadCreate):
"""Submit a lead for a chatbot (public endpoint, no auth required)."""
supabase = get_supabase()
# Verify chatbot exists
chatbot = supabase.table("chatbots").select("id, lead_capture_enabled").eq("id", chatbot_id).execute()
if not chatbot.data:
raise HTTPException(status_code=404, detail="Chatbot not found")
if not chatbot.data[0].get("lead_capture_enabled", False):
raise HTTPException(status_code=400, detail="Lead capture not enabled for this chatbot")
# Deduplicate by email+chatbot_id
if data.email:
existing = supabase.table("leads").select("*") \
.eq("chatbot_id", chatbot_id) \
.eq("email", data.email) \
.execute()
if existing.data:
return LeadResponse(**existing.data[0])
lead_data = {
"id": str(uuid.uuid4()),
"chatbot_id": chatbot_id,
"conversation_id": data.conversation_id,
"email": data.email,
"name": data.name,
"phone": data.phone,
"company": data.company,
}
result = supabase.table("leads").insert(lead_data).execute()
if not result.data:
raise HTTPException(status_code=500, detail="Failed to save lead")
return LeadResponse(**result.data[0])