mirror of
http://88.130.71.182:3000/BlitTech/contexta_be.git
synced 2026-06-13 08:56:20 +00:00
- 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>
185 lines
6.4 KiB
Python
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])
|