Implementación inicial del sistema de automatización de redes sociales

- Estructura completa del proyecto con FastAPI
- Modelos de base de datos (productos, servicios, posts, calendario, interacciones)
- Publishers para X, Threads, Instagram, Facebook
- Generador de contenido con DeepSeek API
- Worker de Celery con tareas programadas
- Dashboard básico con templates HTML
- Docker Compose para despliegue
- Documentación completa

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-28 01:11:44 +00:00
commit 049d2133f9
53 changed files with 5876 additions and 0 deletions

1
app/api/__init__.py Normal file
View File

@@ -0,0 +1 @@
# API Routes

View File

@@ -0,0 +1 @@
# API Route modules

201
app/api/routes/calendar.py Normal file
View File

@@ -0,0 +1,201 @@
"""
API Routes para gestión del Calendario de Contenido.
"""
from datetime import datetime, time
from typing import List, Optional
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.content_calendar import ContentCalendar
router = APIRouter()
# ===========================================
# SCHEMAS
# ===========================================
class CalendarEntryCreate(BaseModel):
day_of_week: int # 0=Lunes, 6=Domingo
time: str # Formato "HH:MM"
content_type: str
platforms: List[str]
requires_approval: bool = False
category_filter: Optional[str] = None
priority: int = 0
description: Optional[str] = None
class Config:
from_attributes = True
class CalendarEntryUpdate(BaseModel):
day_of_week: Optional[int] = None
time: Optional[str] = None
content_type: Optional[str] = None
platforms: Optional[List[str]] = None
is_active: Optional[bool] = None
requires_approval: Optional[bool] = None
category_filter: Optional[str] = None
priority: Optional[int] = None
description: Optional[str] = None
class Config:
from_attributes = True
# ===========================================
# ENDPOINTS
# ===========================================
@router.get("/")
async def list_calendar_entries(
day_of_week: Optional[int] = Query(None),
content_type: Optional[str] = Query(None),
platform: Optional[str] = Query(None),
is_active: Optional[bool] = Query(True),
db: Session = Depends(get_db)
):
"""Listar entradas del calendario."""
query = db.query(ContentCalendar)
if day_of_week is not None:
query = query.filter(ContentCalendar.day_of_week == day_of_week)
if content_type:
query = query.filter(ContentCalendar.content_type == content_type)
if is_active is not None:
query = query.filter(ContentCalendar.is_active == is_active)
if platform:
query = query.filter(ContentCalendar.platforms.contains([platform]))
entries = query.order_by(
ContentCalendar.day_of_week,
ContentCalendar.time
).all()
return [e.to_dict() for e in entries]
@router.get("/week")
async def get_week_schedule(db: Session = Depends(get_db)):
"""Obtener el calendario semanal completo."""
entries = db.query(ContentCalendar).filter(
ContentCalendar.is_active == True
).order_by(
ContentCalendar.day_of_week,
ContentCalendar.time
).all()
# Organizar por día
week = {i: [] for i in range(7)}
for entry in entries:
week[entry.day_of_week].append(entry.to_dict())
days = ["Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado", "Domingo"]
return {
days[i]: week[i] for i in range(7)
}
@router.get("/today")
async def get_today_schedule(db: Session = Depends(get_db)):
"""Obtener el calendario de hoy."""
today = datetime.now().weekday()
entries = db.query(ContentCalendar).filter(
ContentCalendar.day_of_week == today,
ContentCalendar.is_active == True
).order_by(ContentCalendar.time).all()
return [e.to_dict() for e in entries]
@router.get("/{entry_id}")
async def get_calendar_entry(entry_id: int, db: Session = Depends(get_db)):
"""Obtener una entrada del calendario por ID."""
entry = db.query(ContentCalendar).filter(ContentCalendar.id == entry_id).first()
if not entry:
raise HTTPException(status_code=404, detail="Entrada no encontrada")
return entry.to_dict()
@router.post("/")
async def create_calendar_entry(entry_data: CalendarEntryCreate, db: Session = Depends(get_db)):
"""Crear una nueva entrada en el calendario."""
# Parsear hora
hour, minute = map(int, entry_data.time.split(":"))
entry_time = time(hour=hour, minute=minute)
entry = ContentCalendar(
day_of_week=entry_data.day_of_week,
time=entry_time,
content_type=entry_data.content_type,
platforms=entry_data.platforms,
requires_approval=entry_data.requires_approval,
category_filter=entry_data.category_filter,
priority=entry_data.priority,
description=entry_data.description
)
db.add(entry)
db.commit()
db.refresh(entry)
return entry.to_dict()
@router.put("/{entry_id}")
async def update_calendar_entry(
entry_id: int,
entry_data: CalendarEntryUpdate,
db: Session = Depends(get_db)
):
"""Actualizar una entrada del calendario."""
entry = db.query(ContentCalendar).filter(ContentCalendar.id == entry_id).first()
if not entry:
raise HTTPException(status_code=404, detail="Entrada no encontrada")
update_data = entry_data.dict(exclude_unset=True)
# Parsear hora si se proporciona
if "time" in update_data and update_data["time"]:
hour, minute = map(int, update_data["time"].split(":"))
update_data["time"] = time(hour=hour, minute=minute)
for field, value in update_data.items():
setattr(entry, field, value)
entry.updated_at = datetime.utcnow()
db.commit()
db.refresh(entry)
return entry.to_dict()
@router.delete("/{entry_id}")
async def delete_calendar_entry(entry_id: int, db: Session = Depends(get_db)):
"""Eliminar una entrada del calendario."""
entry = db.query(ContentCalendar).filter(ContentCalendar.id == entry_id).first()
if not entry:
raise HTTPException(status_code=404, detail="Entrada no encontrada")
db.delete(entry)
db.commit()
return {"message": "Entrada eliminada", "entry_id": entry_id}
@router.post("/{entry_id}/toggle")
async def toggle_calendar_entry(entry_id: int, db: Session = Depends(get_db)):
"""Activar/desactivar una entrada del calendario."""
entry = db.query(ContentCalendar).filter(ContentCalendar.id == entry_id).first()
if not entry:
raise HTTPException(status_code=404, detail="Entrada no encontrada")
entry.is_active = not entry.is_active
db.commit()
return {
"message": f"Entrada {'activada' if entry.is_active else 'desactivada'}",
"is_active": entry.is_active
}

118
app/api/routes/dashboard.py Normal file
View File

@@ -0,0 +1,118 @@
"""
API Routes para el Dashboard.
"""
from datetime import datetime, timedelta
from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from sqlalchemy import func
from app.core.database import get_db
from app.models.post import Post
from app.models.interaction import Interaction
router = APIRouter()
templates = Jinja2Templates(directory="dashboard/templates")
@router.get("/", response_class=HTMLResponse)
async def dashboard_home(request: Request, db: Session = Depends(get_db)):
"""Página principal del dashboard."""
# Estadísticas
now = datetime.utcnow()
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
week_start = today_start - timedelta(days=now.weekday())
stats = {
"posts_today": db.query(Post).filter(
Post.published_at >= today_start
).count(),
"posts_week": db.query(Post).filter(
Post.published_at >= week_start
).count(),
"pending_approval": db.query(Post).filter(
Post.status == "pending_approval"
).count(),
"scheduled": db.query(Post).filter(
Post.status == "scheduled"
).count(),
"interactions_pending": db.query(Interaction).filter(
Interaction.responded == False,
Interaction.is_archived == False
).count()
}
# Posts pendientes
pending_posts = db.query(Post).filter(
Post.status == "pending_approval"
).order_by(Post.scheduled_at.asc()).limit(5).all()
# Próximas publicaciones
scheduled_posts = db.query(Post).filter(
Post.status == "scheduled",
Post.scheduled_at >= now
).order_by(Post.scheduled_at.asc()).limit(5).all()
# Interacciones recientes
recent_interactions = db.query(Interaction).filter(
Interaction.responded == False
).order_by(Interaction.interaction_at.desc()).limit(5).all()
return templates.TemplateResponse("index.html", {
"request": request,
"stats": stats,
"pending_posts": [p.to_dict() for p in pending_posts],
"scheduled_posts": [p.to_dict() for p in scheduled_posts],
"recent_interactions": [i.to_dict() for i in recent_interactions]
})
@router.get("/posts", response_class=HTMLResponse)
async def dashboard_posts(request: Request, db: Session = Depends(get_db)):
"""Página de gestión de posts."""
posts = db.query(Post).order_by(Post.created_at.desc()).limit(50).all()
return templates.TemplateResponse("posts.html", {
"request": request,
"posts": [p.to_dict() for p in posts]
})
@router.get("/calendar", response_class=HTMLResponse)
async def dashboard_calendar(request: Request):
"""Página de calendario."""
return templates.TemplateResponse("calendar.html", {
"request": request
})
@router.get("/interactions", response_class=HTMLResponse)
async def dashboard_interactions(request: Request, db: Session = Depends(get_db)):
"""Página de interacciones."""
interactions = db.query(Interaction).filter(
Interaction.is_archived == False
).order_by(Interaction.interaction_at.desc()).limit(50).all()
return templates.TemplateResponse("interactions.html", {
"request": request,
"interactions": [i.to_dict() for i in interactions]
})
@router.get("/products", response_class=HTMLResponse)
async def dashboard_products(request: Request):
"""Página de productos."""
return templates.TemplateResponse("products.html", {
"request": request
})
@router.get("/services", response_class=HTMLResponse)
async def dashboard_services(request: Request):
"""Página de servicios."""
return templates.TemplateResponse("services.html", {
"request": request
})

View File

@@ -0,0 +1,226 @@
"""
API Routes para gestión de Interacciones.
"""
from datetime import datetime
from typing import List, Optional
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.interaction import Interaction
router = APIRouter()
# ===========================================
# SCHEMAS
# ===========================================
class InteractionResponse(BaseModel):
content: str
class Config:
from_attributes = True
# ===========================================
# ENDPOINTS
# ===========================================
@router.get("/")
async def list_interactions(
platform: Optional[str] = Query(None),
interaction_type: Optional[str] = Query(None),
responded: Optional[bool] = Query(None),
is_lead: Optional[bool] = Query(None),
is_read: Optional[bool] = Query(None),
limit: int = Query(50, le=100),
offset: int = Query(0),
db: Session = Depends(get_db)
):
"""Listar interacciones con filtros."""
query = db.query(Interaction)
if platform:
query = query.filter(Interaction.platform == platform)
if interaction_type:
query = query.filter(Interaction.interaction_type == interaction_type)
if responded is not None:
query = query.filter(Interaction.responded == responded)
if is_lead is not None:
query = query.filter(Interaction.is_lead == is_lead)
if is_read is not None:
query = query.filter(Interaction.is_read == is_read)
interactions = query.order_by(
Interaction.interaction_at.desc()
).offset(offset).limit(limit).all()
return [i.to_dict() for i in interactions]
@router.get("/pending")
async def list_pending_interactions(db: Session = Depends(get_db)):
"""Listar interacciones sin responder."""
interactions = db.query(Interaction).filter(
Interaction.responded == False,
Interaction.is_archived == False
).order_by(
Interaction.priority.desc(),
Interaction.interaction_at.desc()
).all()
return [i.to_dict() for i in interactions]
@router.get("/leads")
async def list_leads(db: Session = Depends(get_db)):
"""Listar interacciones marcadas como leads."""
interactions = db.query(Interaction).filter(
Interaction.is_lead == True
).order_by(Interaction.interaction_at.desc()).all()
return [i.to_dict() for i in interactions]
@router.get("/stats")
async def get_interaction_stats(db: Session = Depends(get_db)):
"""Obtener estadísticas de interacciones."""
total = db.query(Interaction).count()
pending = db.query(Interaction).filter(
Interaction.responded == False,
Interaction.is_archived == False
).count()
leads = db.query(Interaction).filter(Interaction.is_lead == True).count()
# Por plataforma
by_platform = {}
for platform in ["x", "threads", "instagram", "facebook"]:
by_platform[platform] = db.query(Interaction).filter(
Interaction.platform == platform
).count()
return {
"total": total,
"pending": pending,
"leads": leads,
"by_platform": by_platform
}
@router.get("/{interaction_id}")
async def get_interaction(interaction_id: int, db: Session = Depends(get_db)):
"""Obtener una interacción por ID."""
interaction = db.query(Interaction).filter(Interaction.id == interaction_id).first()
if not interaction:
raise HTTPException(status_code=404, detail="Interacción no encontrada")
return interaction.to_dict()
@router.post("/{interaction_id}/respond")
async def respond_to_interaction(
interaction_id: int,
response_data: InteractionResponse,
db: Session = Depends(get_db)
):
"""Responder a una interacción."""
interaction = db.query(Interaction).filter(Interaction.id == interaction_id).first()
if not interaction:
raise HTTPException(status_code=404, detail="Interacción no encontrada")
# TODO: Enviar respuesta a la plataforma correspondiente
# from app.publishers import get_publisher
# publisher = get_publisher(interaction.platform)
# result = await publisher.reply(interaction.external_id, response_data.content)
interaction.responded = True
interaction.response_content = response_data.content
interaction.responded_at = datetime.utcnow()
db.commit()
return {
"message": "Respuesta guardada",
"interaction_id": interaction_id,
"note": "La publicación a la plataforma está en desarrollo"
}
@router.post("/{interaction_id}/suggest-response")
async def suggest_response(interaction_id: int, db: Session = Depends(get_db)):
"""Generar sugerencia de respuesta con IA."""
interaction = db.query(Interaction).filter(Interaction.id == interaction_id).first()
if not interaction:
raise HTTPException(status_code=404, detail="Interacción no encontrada")
# TODO: Implementar generación con DeepSeek
# from app.services.content_generator import generate_response_suggestion
# suggestions = await generate_response_suggestion(interaction)
# Respuestas placeholder
suggestions = [
f"¡Gracias por tu comentario! Nos da gusto que te interese nuestro contenido.",
f"¡Hola! Gracias por escribirnos. ¿En qué podemos ayudarte?",
f"¡Excelente pregunta! Te respondemos por DM para darte más detalles."
]
return {
"suggestions": suggestions,
"note": "La generación con IA está en desarrollo"
}
@router.post("/{interaction_id}/mark-as-lead")
async def mark_as_lead(interaction_id: int, db: Session = Depends(get_db)):
"""Marcar interacción como lead potencial."""
interaction = db.query(Interaction).filter(Interaction.id == interaction_id).first()
if not interaction:
raise HTTPException(status_code=404, detail="Interacción no encontrada")
interaction.is_lead = not interaction.is_lead
db.commit()
return {
"message": f"{'Marcado' if interaction.is_lead else 'Desmarcado'} como lead",
"is_lead": interaction.is_lead
}
@router.post("/{interaction_id}/mark-as-read")
async def mark_as_read(interaction_id: int, db: Session = Depends(get_db)):
"""Marcar interacción como leída."""
interaction = db.query(Interaction).filter(Interaction.id == interaction_id).first()
if not interaction:
raise HTTPException(status_code=404, detail="Interacción no encontrada")
interaction.is_read = True
db.commit()
return {"message": "Marcado como leído", "interaction_id": interaction_id}
@router.post("/{interaction_id}/archive")
async def archive_interaction(interaction_id: int, db: Session = Depends(get_db)):
"""Archivar una interacción."""
interaction = db.query(Interaction).filter(Interaction.id == interaction_id).first()
if not interaction:
raise HTTPException(status_code=404, detail="Interacción no encontrada")
interaction.is_archived = True
db.commit()
return {"message": "Interacción archivada", "interaction_id": interaction_id}
@router.delete("/{interaction_id}")
async def delete_interaction(interaction_id: int, db: Session = Depends(get_db)):
"""Eliminar una interacción."""
interaction = db.query(Interaction).filter(Interaction.id == interaction_id).first()
if not interaction:
raise HTTPException(status_code=404, detail="Interacción no encontrada")
db.delete(interaction)
db.commit()
return {"message": "Interacción eliminada", "interaction_id": interaction_id}

216
app/api/routes/posts.py Normal file
View File

@@ -0,0 +1,216 @@
"""
API Routes para gestión de Posts.
"""
from datetime import datetime
from typing import List, Optional
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.post import Post
router = APIRouter()
# ===========================================
# SCHEMAS
# ===========================================
class PostCreate(BaseModel):
content: str
content_type: str
platforms: List[str]
scheduled_at: Optional[datetime] = None
image_url: Optional[str] = None
approval_required: bool = False
hashtags: Optional[List[str]] = None
class Config:
from_attributes = True
class PostUpdate(BaseModel):
content: Optional[str] = None
content_x: Optional[str] = None
content_threads: Optional[str] = None
content_instagram: Optional[str] = None
content_facebook: Optional[str] = None
platforms: Optional[List[str]] = None
scheduled_at: Optional[datetime] = None
status: Optional[str] = None
image_url: Optional[str] = None
hashtags: Optional[List[str]] = None
class Config:
from_attributes = True
class PostResponse(BaseModel):
id: int
content: str
content_type: str
platforms: List[str]
status: str
scheduled_at: Optional[datetime]
published_at: Optional[datetime]
image_url: Optional[str]
approval_required: bool
created_at: datetime
class Config:
from_attributes = True
# ===========================================
# ENDPOINTS
# ===========================================
@router.get("/", response_model=List[PostResponse])
async def list_posts(
status: Optional[str] = Query(None, description="Filtrar por estado"),
content_type: Optional[str] = Query(None, description="Filtrar por tipo"),
platform: Optional[str] = Query(None, description="Filtrar por plataforma"),
limit: int = Query(50, le=100),
offset: int = Query(0),
db: Session = Depends(get_db)
):
"""Listar posts con filtros opcionales."""
query = db.query(Post)
if status:
query = query.filter(Post.status == status)
if content_type:
query = query.filter(Post.content_type == content_type)
if platform:
query = query.filter(Post.platforms.contains([platform]))
posts = query.order_by(Post.created_at.desc()).offset(offset).limit(limit).all()
return posts
@router.get("/pending")
async def list_pending_posts(db: Session = Depends(get_db)):
"""Listar posts pendientes de aprobación."""
posts = db.query(Post).filter(
Post.status == "pending_approval"
).order_by(Post.scheduled_at.asc()).all()
return [post.to_dict() for post in posts]
@router.get("/scheduled")
async def list_scheduled_posts(db: Session = Depends(get_db)):
"""Listar posts programados."""
posts = db.query(Post).filter(
Post.status == "scheduled",
Post.scheduled_at >= datetime.utcnow()
).order_by(Post.scheduled_at.asc()).all()
return [post.to_dict() for post in posts]
@router.get("/{post_id}")
async def get_post(post_id: int, db: Session = Depends(get_db)):
"""Obtener un post por ID."""
post = db.query(Post).filter(Post.id == post_id).first()
if not post:
raise HTTPException(status_code=404, detail="Post no encontrado")
return post.to_dict()
@router.post("/", response_model=PostResponse)
async def create_post(post_data: PostCreate, db: Session = Depends(get_db)):
"""Crear un nuevo post."""
post = Post(
content=post_data.content,
content_type=post_data.content_type,
platforms=post_data.platforms,
scheduled_at=post_data.scheduled_at,
image_url=post_data.image_url,
approval_required=post_data.approval_required,
hashtags=post_data.hashtags,
status="pending_approval" if post_data.approval_required else "scheduled"
)
db.add(post)
db.commit()
db.refresh(post)
return post
@router.put("/{post_id}")
async def update_post(post_id: int, post_data: PostUpdate, db: Session = Depends(get_db)):
"""Actualizar un post."""
post = db.query(Post).filter(Post.id == post_id).first()
if not post:
raise HTTPException(status_code=404, detail="Post no encontrado")
update_data = post_data.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(post, field, value)
post.updated_at = datetime.utcnow()
db.commit()
db.refresh(post)
return post.to_dict()
@router.post("/{post_id}/approve")
async def approve_post(post_id: int, db: Session = Depends(get_db)):
"""Aprobar un post pendiente."""
post = db.query(Post).filter(Post.id == post_id).first()
if not post:
raise HTTPException(status_code=404, detail="Post no encontrado")
if post.status != "pending_approval":
raise HTTPException(status_code=400, detail="El post no está pendiente de aprobación")
post.status = "scheduled"
post.approved_at = datetime.utcnow()
db.commit()
return {"message": "Post aprobado", "post_id": post_id}
@router.post("/{post_id}/reject")
async def reject_post(post_id: int, db: Session = Depends(get_db)):
"""Rechazar un post pendiente."""
post = db.query(Post).filter(Post.id == post_id).first()
if not post:
raise HTTPException(status_code=404, detail="Post no encontrado")
post.status = "cancelled"
db.commit()
return {"message": "Post rechazado", "post_id": post_id}
@router.post("/{post_id}/regenerate")
async def regenerate_post(post_id: int, db: Session = Depends(get_db)):
"""Regenerar contenido de un post con IA."""
post = db.query(Post).filter(Post.id == post_id).first()
if not post:
raise HTTPException(status_code=404, detail="Post no encontrado")
# TODO: Implementar regeneración con DeepSeek
# from app.services.content_generator import regenerate_content
# new_content = await regenerate_content(post)
return {"message": "Regeneración en desarrollo", "post_id": post_id}
@router.delete("/{post_id}")
async def delete_post(post_id: int, db: Session = Depends(get_db)):
"""Eliminar un post."""
post = db.query(Post).filter(Post.id == post_id).first()
if not post:
raise HTTPException(status_code=404, detail="Post no encontrado")
db.delete(post)
db.commit()
return {"message": "Post eliminado", "post_id": post_id}

180
app/api/routes/products.py Normal file
View File

@@ -0,0 +1,180 @@
"""
API Routes para gestión de Productos.
"""
from datetime import datetime
from typing import List, Optional
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.product import Product
router = APIRouter()
# ===========================================
# SCHEMAS
# ===========================================
class ProductCreate(BaseModel):
name: str
description: Optional[str] = None
short_description: Optional[str] = None
category: str
subcategory: Optional[str] = None
brand: Optional[str] = None
model: Optional[str] = None
price: float
price_usd: Optional[float] = None
stock: int = 0
specs: Optional[dict] = None
images: Optional[List[str]] = None
main_image: Optional[str] = None
tags: Optional[List[str]] = None
highlights: Optional[List[str]] = None
is_featured: bool = False
class Config:
from_attributes = True
class ProductUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
short_description: Optional[str] = None
category: Optional[str] = None
subcategory: Optional[str] = None
brand: Optional[str] = None
model: Optional[str] = None
price: Optional[float] = None
price_usd: Optional[float] = None
stock: Optional[int] = None
is_available: Optional[bool] = None
specs: Optional[dict] = None
images: Optional[List[str]] = None
main_image: Optional[str] = None
tags: Optional[List[str]] = None
highlights: Optional[List[str]] = None
is_featured: Optional[bool] = None
class Config:
from_attributes = True
# ===========================================
# ENDPOINTS
# ===========================================
@router.get("/")
async def list_products(
category: Optional[str] = Query(None),
brand: Optional[str] = Query(None),
is_available: Optional[bool] = Query(None),
is_featured: Optional[bool] = Query(None),
min_price: Optional[float] = Query(None),
max_price: Optional[float] = Query(None),
limit: int = Query(50, le=100),
offset: int = Query(0),
db: Session = Depends(get_db)
):
"""Listar productos con filtros."""
query = db.query(Product)
if category:
query = query.filter(Product.category == category)
if brand:
query = query.filter(Product.brand == brand)
if is_available is not None:
query = query.filter(Product.is_available == is_available)
if is_featured is not None:
query = query.filter(Product.is_featured == is_featured)
if min_price is not None:
query = query.filter(Product.price >= min_price)
if max_price is not None:
query = query.filter(Product.price <= max_price)
products = query.order_by(Product.created_at.desc()).offset(offset).limit(limit).all()
return [p.to_dict() for p in products]
@router.get("/categories")
async def list_categories(db: Session = Depends(get_db)):
"""Listar categorías únicas de productos."""
categories = db.query(Product.category).distinct().all()
return [c[0] for c in categories if c[0]]
@router.get("/featured")
async def list_featured_products(db: Session = Depends(get_db)):
"""Listar productos destacados."""
products = db.query(Product).filter(
Product.is_featured == True,
Product.is_available == True
).all()
return [p.to_dict() for p in products]
@router.get("/{product_id}")
async def get_product(product_id: int, db: Session = Depends(get_db)):
"""Obtener un producto por ID."""
product = db.query(Product).filter(Product.id == product_id).first()
if not product:
raise HTTPException(status_code=404, detail="Producto no encontrado")
return product.to_dict()
@router.post("/")
async def create_product(product_data: ProductCreate, db: Session = Depends(get_db)):
"""Crear un nuevo producto."""
product = Product(**product_data.dict())
db.add(product)
db.commit()
db.refresh(product)
return product.to_dict()
@router.put("/{product_id}")
async def update_product(product_id: int, product_data: ProductUpdate, db: Session = Depends(get_db)):
"""Actualizar un producto."""
product = db.query(Product).filter(Product.id == product_id).first()
if not product:
raise HTTPException(status_code=404, detail="Producto no encontrado")
update_data = product_data.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(product, field, value)
product.updated_at = datetime.utcnow()
db.commit()
db.refresh(product)
return product.to_dict()
@router.delete("/{product_id}")
async def delete_product(product_id: int, db: Session = Depends(get_db)):
"""Eliminar un producto."""
product = db.query(Product).filter(Product.id == product_id).first()
if not product:
raise HTTPException(status_code=404, detail="Producto no encontrado")
db.delete(product)
db.commit()
return {"message": "Producto eliminado", "product_id": product_id}
@router.post("/{product_id}/toggle-featured")
async def toggle_featured(product_id: int, db: Session = Depends(get_db)):
"""Alternar estado de producto destacado."""
product = db.query(Product).filter(Product.id == product_id).first()
if not product:
raise HTTPException(status_code=404, detail="Producto no encontrado")
product.is_featured = not product.is_featured
db.commit()
return {
"message": f"Producto {'destacado' if product.is_featured else 'no destacado'}",
"is_featured": product.is_featured
}

174
app/api/routes/services.py Normal file
View File

@@ -0,0 +1,174 @@
"""
API Routes para gestión de Servicios.
"""
from datetime import datetime
from typing import List, Optional
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.service import Service
router = APIRouter()
# ===========================================
# SCHEMAS
# ===========================================
class ServiceCreate(BaseModel):
name: str
description: str
short_description: Optional[str] = None
category: str
target_sectors: Optional[List[str]] = None
benefits: Optional[List[str]] = None
features: Optional[List[str]] = None
case_studies: Optional[List[dict]] = None
icon: Optional[str] = None
images: Optional[List[str]] = None
main_image: Optional[str] = None
price_range: Optional[str] = None
has_free_demo: bool = False
tags: Optional[List[str]] = None
call_to_action: Optional[str] = None
is_featured: bool = False
class Config:
from_attributes = True
class ServiceUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
short_description: Optional[str] = None
category: Optional[str] = None
target_sectors: Optional[List[str]] = None
benefits: Optional[List[str]] = None
features: Optional[List[str]] = None
case_studies: Optional[List[dict]] = None
icon: Optional[str] = None
images: Optional[List[str]] = None
main_image: Optional[str] = None
price_range: Optional[str] = None
has_free_demo: Optional[bool] = None
tags: Optional[List[str]] = None
call_to_action: Optional[str] = None
is_active: Optional[bool] = None
is_featured: Optional[bool] = None
class Config:
from_attributes = True
# ===========================================
# ENDPOINTS
# ===========================================
@router.get("/")
async def list_services(
category: Optional[str] = Query(None),
target_sector: Optional[str] = Query(None),
is_active: Optional[bool] = Query(True),
is_featured: Optional[bool] = Query(None),
limit: int = Query(50, le=100),
offset: int = Query(0),
db: Session = Depends(get_db)
):
"""Listar servicios con filtros."""
query = db.query(Service)
if category:
query = query.filter(Service.category == category)
if is_active is not None:
query = query.filter(Service.is_active == is_active)
if is_featured is not None:
query = query.filter(Service.is_featured == is_featured)
if target_sector:
query = query.filter(Service.target_sectors.contains([target_sector]))
services = query.order_by(Service.created_at.desc()).offset(offset).limit(limit).all()
return [s.to_dict() for s in services]
@router.get("/categories")
async def list_categories(db: Session = Depends(get_db)):
"""Listar categorías únicas de servicios."""
categories = db.query(Service.category).distinct().all()
return [c[0] for c in categories if c[0]]
@router.get("/featured")
async def list_featured_services(db: Session = Depends(get_db)):
"""Listar servicios destacados."""
services = db.query(Service).filter(
Service.is_featured == True,
Service.is_active == True
).all()
return [s.to_dict() for s in services]
@router.get("/{service_id}")
async def get_service(service_id: int, db: Session = Depends(get_db)):
"""Obtener un servicio por ID."""
service = db.query(Service).filter(Service.id == service_id).first()
if not service:
raise HTTPException(status_code=404, detail="Servicio no encontrado")
return service.to_dict()
@router.post("/")
async def create_service(service_data: ServiceCreate, db: Session = Depends(get_db)):
"""Crear un nuevo servicio."""
service = Service(**service_data.dict())
db.add(service)
db.commit()
db.refresh(service)
return service.to_dict()
@router.put("/{service_id}")
async def update_service(service_id: int, service_data: ServiceUpdate, db: Session = Depends(get_db)):
"""Actualizar un servicio."""
service = db.query(Service).filter(Service.id == service_id).first()
if not service:
raise HTTPException(status_code=404, detail="Servicio no encontrado")
update_data = service_data.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(service, field, value)
service.updated_at = datetime.utcnow()
db.commit()
db.refresh(service)
return service.to_dict()
@router.delete("/{service_id}")
async def delete_service(service_id: int, db: Session = Depends(get_db)):
"""Eliminar un servicio."""
service = db.query(Service).filter(Service.id == service_id).first()
if not service:
raise HTTPException(status_code=404, detail="Servicio no encontrado")
db.delete(service)
db.commit()
return {"message": "Servicio eliminado", "service_id": service_id}
@router.post("/{service_id}/toggle-featured")
async def toggle_featured(service_id: int, db: Session = Depends(get_db)):
"""Alternar estado de servicio destacado."""
service = db.query(Service).filter(Service.id == service_id).first()
if not service:
raise HTTPException(status_code=404, detail="Servicio no encontrado")
service.is_featured = not service.is_featured
db.commit()
return {
"message": f"Servicio {'destacado' if service.is_featured else 'no destacado'}",
"is_featured": service.is_featured
}