Files
Consultoría AS ecc2ca73ea feat: Add Analytics, Odoo Integration, A/B Testing, and Content features
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>
2026-01-28 03:10:42 +00:00

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
}