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>
This commit is contained in:
317
app/api/routes/leads.py
Normal file
317
app/api/routes/leads.py
Normal file
@@ -0,0 +1,317 @@
|
||||
"""
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user