Phase 1 - Analytics y Reportes: - PostMetrics and AnalyticsReport models for tracking engagement - Analytics service with dashboard stats, top posts, optimal times - 8 API endpoints at /api/analytics/* - Interactive dashboard with Chart.js charts - Celery tasks for metrics fetch (15min) and weekly reports Phase 2 - Integración Odoo: - Lead and OdooSyncLog models for CRM integration - Odoo fields added to Product and Service models - XML-RPC service for bidirectional sync - Lead management API at /api/leads/* - Leads dashboard template - Celery tasks for product/service sync and lead export Phase 3 - A/B Testing y Recycling: - ABTest, ABTestVariant, RecycledPost models - Statistical winner analysis using chi-square test - Content recycling with engagement-based scoring - APIs at /api/ab-tests/* and /api/recycling/* - Automated test evaluation and content recycling tasks Phase 4 - Thread Series y Templates: - ThreadSeries and ThreadPost models for multi-post threads - AI-powered thread generation - Enhanced ImageTemplate with HTML template support - APIs at /api/threads/* and /api/templates/* - Thread scheduling with reply chain support Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
318 lines
8.4 KiB
Python
318 lines
8.4 KiB
Python
"""
|
|
API Routes for Leads Management.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from typing import Optional, List
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy.orm import Session
|
|
from pydantic import BaseModel
|
|
|
|
from app.core.database import get_db
|
|
from app.models.lead import Lead
|
|
from app.models.interaction import Interaction
|
|
from app.services.odoo_service import odoo_service
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
class LeadCreate(BaseModel):
|
|
"""Schema for creating a lead."""
|
|
name: Optional[str] = None
|
|
email: Optional[str] = None
|
|
phone: Optional[str] = None
|
|
company: Optional[str] = None
|
|
platform: str
|
|
username: Optional[str] = None
|
|
profile_url: Optional[str] = None
|
|
interest: Optional[str] = None
|
|
notes: Optional[str] = None
|
|
priority: str = "medium"
|
|
products_interested: Optional[List[int]] = None
|
|
services_interested: Optional[List[int]] = None
|
|
tags: Optional[List[str]] = None
|
|
|
|
|
|
class LeadUpdate(BaseModel):
|
|
"""Schema for updating a lead."""
|
|
name: Optional[str] = None
|
|
email: Optional[str] = None
|
|
phone: Optional[str] = None
|
|
company: Optional[str] = None
|
|
interest: Optional[str] = None
|
|
notes: Optional[str] = None
|
|
status: Optional[str] = None
|
|
priority: Optional[str] = None
|
|
assigned_to: Optional[str] = None
|
|
products_interested: Optional[List[int]] = None
|
|
services_interested: Optional[List[int]] = None
|
|
tags: Optional[List[str]] = None
|
|
|
|
|
|
@router.get("/")
|
|
async def list_leads(
|
|
status: Optional[str] = None,
|
|
priority: Optional[str] = None,
|
|
platform: Optional[str] = None,
|
|
synced: Optional[bool] = None,
|
|
limit: int = Query(50, ge=1, le=200),
|
|
offset: int = Query(0, ge=0),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
List all leads with optional filters.
|
|
|
|
- **status**: Filter by status (new, contacted, qualified, proposal, won, lost)
|
|
- **priority**: Filter by priority (low, medium, high, urgent)
|
|
- **platform**: Filter by source platform
|
|
- **synced**: Filter by Odoo sync status
|
|
"""
|
|
query = db.query(Lead)
|
|
|
|
if status:
|
|
query = query.filter(Lead.status == status)
|
|
if priority:
|
|
query = query.filter(Lead.priority == priority)
|
|
if platform:
|
|
query = query.filter(Lead.platform == platform)
|
|
if synced is not None:
|
|
query = query.filter(Lead.synced_to_odoo == synced)
|
|
|
|
total = query.count()
|
|
leads = query.order_by(Lead.created_at.desc()).offset(offset).limit(limit).all()
|
|
|
|
return {
|
|
"leads": [lead.to_dict() for lead in leads],
|
|
"total": total,
|
|
"limit": limit,
|
|
"offset": offset
|
|
}
|
|
|
|
|
|
@router.get("/{lead_id}")
|
|
async def get_lead(
|
|
lead_id: int,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Get a specific lead by ID.
|
|
"""
|
|
lead = db.query(Lead).filter(Lead.id == lead_id).first()
|
|
if not lead:
|
|
raise HTTPException(status_code=404, detail="Lead not found")
|
|
|
|
return lead.to_dict()
|
|
|
|
|
|
@router.post("/")
|
|
async def create_lead(
|
|
lead_data: LeadCreate,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Create a new lead manually.
|
|
"""
|
|
lead = Lead(
|
|
name=lead_data.name,
|
|
email=lead_data.email,
|
|
phone=lead_data.phone,
|
|
company=lead_data.company,
|
|
platform=lead_data.platform,
|
|
username=lead_data.username,
|
|
profile_url=lead_data.profile_url,
|
|
interest=lead_data.interest,
|
|
notes=lead_data.notes,
|
|
priority=lead_data.priority,
|
|
products_interested=lead_data.products_interested,
|
|
services_interested=lead_data.services_interested,
|
|
tags=lead_data.tags,
|
|
status="new"
|
|
)
|
|
|
|
db.add(lead)
|
|
db.commit()
|
|
db.refresh(lead)
|
|
|
|
return {
|
|
"message": "Lead created successfully",
|
|
"lead": lead.to_dict()
|
|
}
|
|
|
|
|
|
@router.post("/from-interaction/{interaction_id}")
|
|
async def create_lead_from_interaction(
|
|
interaction_id: int,
|
|
interest: Optional[str] = None,
|
|
priority: str = "medium",
|
|
notes: Optional[str] = None,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Create a lead from an existing interaction.
|
|
|
|
- **interaction_id**: ID of the interaction to convert
|
|
- **interest**: What the lead is interested in
|
|
- **priority**: Lead priority (low, medium, high, urgent)
|
|
- **notes**: Additional notes
|
|
"""
|
|
# Get interaction
|
|
interaction = db.query(Interaction).filter(Interaction.id == interaction_id).first()
|
|
if not interaction:
|
|
raise HTTPException(status_code=404, detail="Interaction not found")
|
|
|
|
# Check if lead already exists for this interaction
|
|
existing = db.query(Lead).filter(Lead.interaction_id == interaction_id).first()
|
|
if existing:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Lead already exists for this interaction"
|
|
)
|
|
|
|
# Create lead
|
|
lead = Lead(
|
|
interaction_id=interaction_id,
|
|
platform=interaction.platform,
|
|
username=interaction.author_username,
|
|
profile_url=interaction.author_profile_url,
|
|
source_content=interaction.content,
|
|
interest=interest or f"Interest shown in post {interaction.post_id}",
|
|
notes=notes,
|
|
priority=priority,
|
|
status="new"
|
|
)
|
|
|
|
# Mark interaction as potential lead
|
|
interaction.is_potential_lead = True
|
|
|
|
db.add(lead)
|
|
db.commit()
|
|
db.refresh(lead)
|
|
|
|
return {
|
|
"message": "Lead created from interaction",
|
|
"lead": lead.to_dict()
|
|
}
|
|
|
|
|
|
@router.put("/{lead_id}")
|
|
async def update_lead(
|
|
lead_id: int,
|
|
lead_data: LeadUpdate,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Update an existing lead.
|
|
"""
|
|
lead = db.query(Lead).filter(Lead.id == lead_id).first()
|
|
if not lead:
|
|
raise HTTPException(status_code=404, detail="Lead not found")
|
|
|
|
# Update only provided fields
|
|
update_data = lead_data.dict(exclude_unset=True)
|
|
for field, value in update_data.items():
|
|
if value is not None:
|
|
setattr(lead, field, value)
|
|
|
|
lead.updated_at = datetime.utcnow()
|
|
|
|
# If status changed to contacted, update last_contacted_at
|
|
if lead_data.status == "contacted":
|
|
lead.last_contacted_at = datetime.utcnow()
|
|
|
|
db.commit()
|
|
db.refresh(lead)
|
|
|
|
return {
|
|
"message": "Lead updated successfully",
|
|
"lead": lead.to_dict()
|
|
}
|
|
|
|
|
|
@router.delete("/{lead_id}")
|
|
async def delete_lead(
|
|
lead_id: int,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Delete a lead.
|
|
"""
|
|
lead = db.query(Lead).filter(Lead.id == lead_id).first()
|
|
if not lead:
|
|
raise HTTPException(status_code=404, detail="Lead not found")
|
|
|
|
db.delete(lead)
|
|
db.commit()
|
|
|
|
return {"message": "Lead deleted successfully"}
|
|
|
|
|
|
@router.post("/{lead_id}/sync-odoo")
|
|
async def sync_lead_to_odoo(
|
|
lead_id: int,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Sync a specific lead to Odoo CRM.
|
|
"""
|
|
lead = db.query(Lead).filter(Lead.id == lead_id).first()
|
|
if not lead:
|
|
raise HTTPException(status_code=404, detail="Lead not found")
|
|
|
|
if lead.synced_to_odoo:
|
|
return {
|
|
"message": "Lead already synced to Odoo",
|
|
"odoo_lead_id": lead.odoo_lead_id
|
|
}
|
|
|
|
result = await odoo_service.create_lead(lead)
|
|
|
|
if not result.get("success"):
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=result.get("error", "Sync failed")
|
|
)
|
|
|
|
lead.odoo_lead_id = result["odoo_lead_id"]
|
|
lead.synced_to_odoo = True
|
|
lead.odoo_synced_at = datetime.utcnow()
|
|
db.commit()
|
|
|
|
return {
|
|
"message": "Lead synced to Odoo successfully",
|
|
"odoo_lead_id": result["odoo_lead_id"]
|
|
}
|
|
|
|
|
|
@router.get("/stats/summary")
|
|
async def get_leads_summary(
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Get leads summary statistics.
|
|
"""
|
|
from sqlalchemy import func
|
|
|
|
total = db.query(Lead).count()
|
|
by_status = db.query(
|
|
Lead.status, func.count(Lead.id)
|
|
).group_by(Lead.status).all()
|
|
|
|
by_platform = db.query(
|
|
Lead.platform, func.count(Lead.id)
|
|
).group_by(Lead.platform).all()
|
|
|
|
by_priority = db.query(
|
|
Lead.priority, func.count(Lead.id)
|
|
).group_by(Lead.priority).all()
|
|
|
|
unsynced = db.query(Lead).filter(Lead.synced_to_odoo == False).count()
|
|
|
|
return {
|
|
"total": total,
|
|
"by_status": {status: count for status, count in by_status},
|
|
"by_platform": {platform: count for platform, count in by_platform},
|
|
"by_priority": {priority: count for priority, count in by_priority},
|
|
"unsynced_to_odoo": unsynced
|
|
}
|