docs: Add comprehensive documentation for all new features
- FEATURES_OVERVIEW.md: Complete summary of all system features - ANALYTICS.md: Analytics and reporting system documentation - ODOO_INTEGRATION.md: Odoo ERP integration guide - AB_TESTING.md: A/B testing system documentation - CONTENT_RECYCLING.md: Content recycling system docs - THREAD_SERIES.md: Thread series and scheduled posts - IMAGE_TEMPLATES.md: Visual template system documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
297
docs/AB_TESTING.md
Normal file
297
docs/AB_TESTING.md
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
# A/B Testing
|
||||||
|
|
||||||
|
Sistema de pruebas A/B para optimizar contenido y maximizar engagement.
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
El A/B Testing permite comparar diferentes versiones de contenido para determinar cuál tiene mejor rendimiento. El sistema:
|
||||||
|
|
||||||
|
1. Crea múltiples variantes de contenido
|
||||||
|
2. Publica cada variante
|
||||||
|
3. Recopila métricas de cada una
|
||||||
|
4. Determina estadísticamente el ganador
|
||||||
|
|
||||||
|
## Modelos de Datos
|
||||||
|
|
||||||
|
### ABTest
|
||||||
|
|
||||||
|
```python
|
||||||
|
ABTest:
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
test_type: str # content, timing, hashtags, image
|
||||||
|
|
||||||
|
platform: str # x, threads, instagram, facebook
|
||||||
|
status: str # draft, running, completed, cancelled
|
||||||
|
|
||||||
|
started_at: datetime
|
||||||
|
ended_at: datetime
|
||||||
|
duration_hours: int # Duración del test (default 24)
|
||||||
|
|
||||||
|
winning_variant_id: int (FK)
|
||||||
|
confidence_level: float # Nivel de confianza estadística
|
||||||
|
|
||||||
|
min_sample_size: int # Mín impresiones por variante (default 100)
|
||||||
|
success_metric: str # engagement_rate, likes, comments, shares
|
||||||
|
```
|
||||||
|
|
||||||
|
### ABTestVariant
|
||||||
|
|
||||||
|
```python
|
||||||
|
ABTestVariant:
|
||||||
|
id: int
|
||||||
|
test_id: int (FK -> ab_tests)
|
||||||
|
name: str # A, B, C, D
|
||||||
|
|
||||||
|
content: str
|
||||||
|
hashtags: JSON
|
||||||
|
image_url: str
|
||||||
|
|
||||||
|
post_id: int (FK -> posts)
|
||||||
|
|
||||||
|
# Métricas
|
||||||
|
impressions: int
|
||||||
|
reach: int
|
||||||
|
likes: int
|
||||||
|
comments: int
|
||||||
|
shares: int
|
||||||
|
clicks: int
|
||||||
|
engagement_rate: float
|
||||||
|
|
||||||
|
is_winner: bool
|
||||||
|
published_at: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### GET /api/ab-tests/
|
||||||
|
Lista todos los tests A/B.
|
||||||
|
|
||||||
|
**Parámetros:**
|
||||||
|
- `status`: draft, running, completed, cancelled
|
||||||
|
- `platform`: x, threads, etc.
|
||||||
|
- `limit` (int, default=20)
|
||||||
|
|
||||||
|
**Respuesta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Test de copies",
|
||||||
|
"platform": "x",
|
||||||
|
"status": "running",
|
||||||
|
"variants": [...]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /api/ab-tests/{test_id}
|
||||||
|
Obtiene un test específico con sus variantes.
|
||||||
|
|
||||||
|
### POST /api/ab-tests/
|
||||||
|
Crea un nuevo test A/B.
|
||||||
|
|
||||||
|
**Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Test de copies para tips",
|
||||||
|
"platform": "x",
|
||||||
|
"variants": [
|
||||||
|
{
|
||||||
|
"name": "A",
|
||||||
|
"content": "💡 Tip: Automatiza tus procesos con IA...",
|
||||||
|
"hashtags": ["#IA", "#Automatización"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "B",
|
||||||
|
"content": "¿Sabías que la IA puede ahorrarte 10 horas semanales?...",
|
||||||
|
"hashtags": ["#Productividad", "#Tech"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"test_type": "content",
|
||||||
|
"duration_hours": 24,
|
||||||
|
"min_sample_size": 100,
|
||||||
|
"success_metric": "engagement_rate"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validaciones:**
|
||||||
|
- Mínimo 2 variantes, máximo 4
|
||||||
|
- Cada variante debe tener contenido
|
||||||
|
|
||||||
|
### POST /api/ab-tests/{test_id}/start
|
||||||
|
Inicia un test A/B.
|
||||||
|
|
||||||
|
**Proceso:**
|
||||||
|
1. Crea posts para cada variante
|
||||||
|
2. Programa publicación (escalonada cada 5 min)
|
||||||
|
3. Cambia status a "running"
|
||||||
|
4. Registra start_time
|
||||||
|
|
||||||
|
**Respuesta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "A/B test started successfully",
|
||||||
|
"success": true,
|
||||||
|
"test_id": 1,
|
||||||
|
"post_ids": [101, 102],
|
||||||
|
"variants_count": 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/ab-tests/{test_id}/evaluate
|
||||||
|
Evalúa resultados y determina ganador.
|
||||||
|
|
||||||
|
**Proceso:**
|
||||||
|
1. Actualiza métricas de cada variante
|
||||||
|
2. Verifica tamaño de muestra mínimo
|
||||||
|
3. Ejecuta test estadístico (chi-square)
|
||||||
|
4. Determina ganador y confianza
|
||||||
|
5. Si duration_hours pasó, marca como completed
|
||||||
|
|
||||||
|
**Respuesta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"winner": {
|
||||||
|
"variant_id": 1,
|
||||||
|
"name": "A",
|
||||||
|
"engagement_rate": 4.5,
|
||||||
|
"impressions": 1500
|
||||||
|
},
|
||||||
|
"runner_up": {
|
||||||
|
"variant_id": 2,
|
||||||
|
"name": "B",
|
||||||
|
"engagement_rate": 3.2,
|
||||||
|
"impressions": 1400
|
||||||
|
},
|
||||||
|
"confidence_level": 95.5,
|
||||||
|
"p_value": 0.045,
|
||||||
|
"test_status": "completed"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Si datos insuficientes:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"status": "insufficient_data",
|
||||||
|
"min_impressions": 50,
|
||||||
|
"required": 100
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /api/ab-tests/{test_id}/results
|
||||||
|
Obtiene resultados actuales (sin determinar ganador).
|
||||||
|
|
||||||
|
### POST /api/ab-tests/{test_id}/cancel
|
||||||
|
Cancela un test en progreso.
|
||||||
|
|
||||||
|
## Tareas Programadas
|
||||||
|
|
||||||
|
### evaluate_ab_tests
|
||||||
|
- **Frecuencia:** Cada hora
|
||||||
|
- **Función:**
|
||||||
|
- Busca tests en estado "running"
|
||||||
|
- Si pasó duration_hours, evalúa y completa
|
||||||
|
- Si no, solo actualiza métricas
|
||||||
|
|
||||||
|
## Tipos de Test
|
||||||
|
|
||||||
|
| Tipo | Descripción |
|
||||||
|
|------|-------------|
|
||||||
|
| `content` | Prueba diferentes textos |
|
||||||
|
| `timing` | Prueba diferentes horarios |
|
||||||
|
| `hashtags` | Prueba diferentes conjuntos de hashtags |
|
||||||
|
| `image` | Prueba diferentes imágenes |
|
||||||
|
|
||||||
|
## Métricas de Éxito
|
||||||
|
|
||||||
|
| Métrica | Descripción |
|
||||||
|
|---------|-------------|
|
||||||
|
| `engagement_rate` | (likes + comments + shares) / impressions |
|
||||||
|
| `likes` | Total de likes |
|
||||||
|
| `comments` | Total de comentarios |
|
||||||
|
| `shares` | Total de compartidos |
|
||||||
|
| `clicks` | Total de clics (si aplica) |
|
||||||
|
|
||||||
|
## Análisis Estadístico
|
||||||
|
|
||||||
|
El sistema usa el **test chi-cuadrado** para determinar significancia estadística:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Tabla de contingencia
|
||||||
|
[
|
||||||
|
[engagements_A, non_engagements_A],
|
||||||
|
[engagements_B, non_engagements_B]
|
||||||
|
]
|
||||||
|
|
||||||
|
chi2, p_value, dof, expected = scipy.stats.chi2_contingency(tabla)
|
||||||
|
confidence = (1 - p_value) * 100
|
||||||
|
```
|
||||||
|
|
||||||
|
### Interpretación
|
||||||
|
|
||||||
|
| p-value | Confianza | Interpretación |
|
||||||
|
|---------|-----------|----------------|
|
||||||
|
| < 0.01 | > 99% | Muy significativo |
|
||||||
|
| < 0.05 | > 95% | Significativo |
|
||||||
|
| < 0.10 | > 90% | Marginalmente significativo |
|
||||||
|
| ≥ 0.10 | < 90% | No significativo |
|
||||||
|
|
||||||
|
## Flujo de Trabajo
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Crear Test (status: draft)
|
||||||
|
↓
|
||||||
|
2. Iniciar Test (status: running)
|
||||||
|
- Crea posts para variantes
|
||||||
|
- Programa publicación
|
||||||
|
↓
|
||||||
|
3. Esperar duración
|
||||||
|
- Sistema recopila métricas cada 15 min
|
||||||
|
- Evalúa cada hora
|
||||||
|
↓
|
||||||
|
4. Evaluar Resultados (status: completed)
|
||||||
|
- Determina ganador estadístico
|
||||||
|
- Calcula nivel de confianza
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ejemplo de Uso
|
||||||
|
|
||||||
|
```python
|
||||||
|
from app.services.ab_testing_service import ab_testing_service
|
||||||
|
|
||||||
|
# Crear test
|
||||||
|
test = await ab_testing_service.create_test(
|
||||||
|
name="Test de engagement",
|
||||||
|
platform="x",
|
||||||
|
variants=[
|
||||||
|
{"name": "A", "content": "Versión corta y directa"},
|
||||||
|
{"name": "B", "content": "Versión más elaborada con detalles"}
|
||||||
|
],
|
||||||
|
duration_hours=48,
|
||||||
|
min_sample_size=200
|
||||||
|
)
|
||||||
|
|
||||||
|
# Iniciar
|
||||||
|
result = await ab_testing_service.start_test(test.id)
|
||||||
|
|
||||||
|
# Evaluar (después de publicado)
|
||||||
|
results = await ab_testing_service.evaluate_test(test.id)
|
||||||
|
if results.get("winner"):
|
||||||
|
print(f"Ganador: Variante {results['winner']['name']}")
|
||||||
|
print(f"Confianza: {results['confidence_level']:.1f}%")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mejores Prácticas
|
||||||
|
|
||||||
|
1. **Tamaño de muestra:** Espera al menos 100 impresiones por variante
|
||||||
|
2. **Una variable:** Prueba solo un elemento a la vez
|
||||||
|
3. **Duración:** Mínimo 24 horas para datos representativos
|
||||||
|
4. **Horarios:** Publica variantes en horarios similares
|
||||||
|
5. **Confianza:** Busca > 95% antes de tomar decisiones
|
||||||
264
docs/ANALYTICS.md
Normal file
264
docs/ANALYTICS.md
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
# Analytics y Reportes
|
||||||
|
|
||||||
|
Sistema completo de métricas y análisis de rendimiento para optimizar tu estrategia de contenido.
|
||||||
|
|
||||||
|
## Modelos de Datos
|
||||||
|
|
||||||
|
### PostMetrics
|
||||||
|
Registra métricas de cada post a lo largo del tiempo.
|
||||||
|
|
||||||
|
```python
|
||||||
|
PostMetrics:
|
||||||
|
id: int
|
||||||
|
post_id: int (FK -> posts)
|
||||||
|
platform: str # x, threads, instagram, facebook
|
||||||
|
likes: int
|
||||||
|
comments: int
|
||||||
|
shares: int
|
||||||
|
impressions: int
|
||||||
|
reach: int
|
||||||
|
saves: int
|
||||||
|
clicks: int
|
||||||
|
replies: int
|
||||||
|
quotes: int
|
||||||
|
engagement_rate: float
|
||||||
|
recorded_at: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
### AnalyticsReport
|
||||||
|
Reportes agregados (diarios, semanales, mensuales).
|
||||||
|
|
||||||
|
```python
|
||||||
|
AnalyticsReport:
|
||||||
|
id: int
|
||||||
|
report_type: str # daily, weekly, monthly
|
||||||
|
period_start: date
|
||||||
|
period_end: date
|
||||||
|
platform: str (opcional)
|
||||||
|
total_posts: int
|
||||||
|
total_impressions: int
|
||||||
|
total_engagements: int
|
||||||
|
avg_engagement_rate: float
|
||||||
|
top_posts: JSON # Lista de mejores posts
|
||||||
|
best_times: JSON # Mejores horarios
|
||||||
|
content_performance: JSON # Por tipo de contenido
|
||||||
|
platform_breakdown: JSON # Por plataforma
|
||||||
|
summary_text: str # Texto para Telegram
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### GET /api/analytics/dashboard
|
||||||
|
Obtiene datos del dashboard principal.
|
||||||
|
|
||||||
|
**Parámetros:**
|
||||||
|
- `days` (int, default=30): Período a analizar
|
||||||
|
- `platform` (str, opcional): Filtrar por plataforma
|
||||||
|
|
||||||
|
**Respuesta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"period_days": 30,
|
||||||
|
"total_posts": 45,
|
||||||
|
"total_impressions": 125000,
|
||||||
|
"total_engagements": 3500,
|
||||||
|
"total_likes": 2800,
|
||||||
|
"total_comments": 450,
|
||||||
|
"total_shares": 250,
|
||||||
|
"avg_engagement_rate": 2.8,
|
||||||
|
"platform_breakdown": {
|
||||||
|
"x": {"posts": 20, "engagements": 1500},
|
||||||
|
"threads": {"posts": 25, "engagements": 2000}
|
||||||
|
},
|
||||||
|
"content_breakdown": {
|
||||||
|
"tip_tech": {"posts": 15, "engagements": 1200},
|
||||||
|
"producto": {"posts": 10, "engagements": 800}
|
||||||
|
},
|
||||||
|
"pending_interactions": 12
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /api/analytics/top-posts
|
||||||
|
Obtiene los posts con mejor rendimiento.
|
||||||
|
|
||||||
|
**Parámetros:**
|
||||||
|
- `days` (int, default=30): Período a analizar
|
||||||
|
- `limit` (int, default=10): Máximo de posts
|
||||||
|
- `platform` (str, opcional): Filtrar por plataforma
|
||||||
|
|
||||||
|
**Respuesta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"posts": [
|
||||||
|
{
|
||||||
|
"id": 123,
|
||||||
|
"content": "Tip: Usa IA para...",
|
||||||
|
"content_type": "tip_tech",
|
||||||
|
"platforms": ["x", "threads"],
|
||||||
|
"published_at": "2025-01-15T10:00:00",
|
||||||
|
"likes": 150,
|
||||||
|
"comments": 25,
|
||||||
|
"shares": 30,
|
||||||
|
"impressions": 5000,
|
||||||
|
"engagement_rate": 4.1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"count": 10
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /api/analytics/optimal-times
|
||||||
|
Calcula los mejores horarios para publicar basado en datos históricos.
|
||||||
|
|
||||||
|
**Parámetros:**
|
||||||
|
- `platform` (str, opcional): Filtrar por plataforma
|
||||||
|
- `days` (int, default=90): Días de datos a analizar
|
||||||
|
|
||||||
|
**Respuesta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"optimal_times": [
|
||||||
|
{
|
||||||
|
"day": 1,
|
||||||
|
"day_name": "Mar",
|
||||||
|
"hour": 12,
|
||||||
|
"hour_formatted": "12:00",
|
||||||
|
"avg_engagement_rate": 4.5,
|
||||||
|
"sample_size": 15
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"analysis_period_days": 90,
|
||||||
|
"platform": "x"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /api/analytics/engagement-trend
|
||||||
|
Obtiene tendencia de engagement para gráficos.
|
||||||
|
|
||||||
|
**Parámetros:**
|
||||||
|
- `days` (int, default=30): Período
|
||||||
|
- `platform` (str, opcional): Plataforma
|
||||||
|
|
||||||
|
**Respuesta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"trend": [
|
||||||
|
{
|
||||||
|
"date": "2025-01-01",
|
||||||
|
"posts": 3,
|
||||||
|
"impressions": 5000,
|
||||||
|
"engagements": 200
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"period_days": 30,
|
||||||
|
"platform": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /api/analytics/reports
|
||||||
|
Obtiene reportes históricos.
|
||||||
|
|
||||||
|
**Parámetros:**
|
||||||
|
- `report_type` (str): daily, weekly, monthly
|
||||||
|
- `limit` (int, default=10): Máximo de reportes
|
||||||
|
|
||||||
|
### POST /api/analytics/reports/generate
|
||||||
|
Genera un nuevo reporte.
|
||||||
|
|
||||||
|
**Parámetros:**
|
||||||
|
- `report_type` (str): weekly (otros tipos próximamente)
|
||||||
|
|
||||||
|
### POST /api/analytics/reports/send-telegram
|
||||||
|
Genera y envía reporte por Telegram.
|
||||||
|
|
||||||
|
### GET /api/analytics/posts/{post_id}/metrics
|
||||||
|
Obtiene métricas detalladas de un post específico.
|
||||||
|
|
||||||
|
**Respuesta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"post_id": 123,
|
||||||
|
"current_metrics": {"likes": 150, "comments": 25},
|
||||||
|
"published_at": "2025-01-15T10:00:00",
|
||||||
|
"platforms": ["x", "threads"],
|
||||||
|
"metrics_history": [
|
||||||
|
{
|
||||||
|
"recorded_at": "2025-01-15T12:00:00",
|
||||||
|
"likes": 50,
|
||||||
|
"comments": 10,
|
||||||
|
"engagement_rate": 2.5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tareas Programadas
|
||||||
|
|
||||||
|
### fetch_post_metrics
|
||||||
|
- **Frecuencia:** Cada 15 minutos
|
||||||
|
- **Función:** Obtiene métricas actualizadas de las APIs de cada plataforma
|
||||||
|
- **Posts afectados:** Publicados en los últimos 7 días
|
||||||
|
|
||||||
|
### generate_weekly_analytics_report
|
||||||
|
- **Frecuencia:** Domingos a las 9:00 AM
|
||||||
|
- **Función:** Genera reporte semanal completo
|
||||||
|
- **Acciones:**
|
||||||
|
- Calcula métricas agregadas
|
||||||
|
- Compara con semana anterior
|
||||||
|
- Genera texto para Telegram
|
||||||
|
- Envía notificación (si está configurado)
|
||||||
|
|
||||||
|
### recalculate_optimal_times
|
||||||
|
- **Frecuencia:** Lunes a las 2:00 AM
|
||||||
|
- **Función:** Recalcula mejores horarios basado en últimos 90 días
|
||||||
|
|
||||||
|
## Dashboard
|
||||||
|
|
||||||
|
Accede al dashboard de analytics en `/dashboard/analytics`.
|
||||||
|
|
||||||
|
### Características:
|
||||||
|
- **Cards de métricas:** Posts, impresiones, engagements, tasa
|
||||||
|
- **Gráfico de tendencia:** Chart.js con impresiones y engagements
|
||||||
|
- **Desglose por plataforma:** Comparativa visual
|
||||||
|
- **Top posts:** Lista de mejores performers
|
||||||
|
- **Mejores horarios:** Mapa de calor visual
|
||||||
|
- **Rendimiento por contenido:** Por tipo de publicación
|
||||||
|
|
||||||
|
## Configuración
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
ANALYTICS_FETCH_INTERVAL=15 # minutos
|
||||||
|
TELEGRAM_REPORT_ENABLED=true
|
||||||
|
TELEGRAM_REPORT_DAY=6 # 0=Lunes, 6=Domingo
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ejemplo de Uso
|
||||||
|
|
||||||
|
```python
|
||||||
|
from app.services.analytics_service import analytics_service
|
||||||
|
|
||||||
|
# Obtener stats del dashboard
|
||||||
|
stats = await analytics_service.get_dashboard_stats(days=30, platform="x")
|
||||||
|
|
||||||
|
# Obtener top posts
|
||||||
|
top = await analytics_service.get_top_posts(days=7, limit=5)
|
||||||
|
|
||||||
|
# Generar reporte semanal
|
||||||
|
report = await analytics_service.generate_weekly_report()
|
||||||
|
print(report.summary_text) # Texto formateado para Telegram
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cálculos
|
||||||
|
|
||||||
|
### Engagement Rate
|
||||||
|
```
|
||||||
|
engagement_rate = (likes + comments + shares) / impressions * 100
|
||||||
|
```
|
||||||
|
|
||||||
|
### Score de Horario Óptimo
|
||||||
|
```
|
||||||
|
Para cada combinación (día, hora):
|
||||||
|
promedio_engagement = sum(engagement_rates) / count(posts)
|
||||||
|
ordenar por promedio_engagement descendente
|
||||||
|
```
|
||||||
267
docs/CONTENT_RECYCLING.md
Normal file
267
docs/CONTENT_RECYCLING.md
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
# Reciclaje de Contenido
|
||||||
|
|
||||||
|
Sistema para republica contenido exitoso y maximizar su alcance.
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
El reciclaje de contenido permite:
|
||||||
|
- Identificar posts con alto engagement
|
||||||
|
- Republicarlos con modificaciones opcionales
|
||||||
|
- Trackear rendimiento de versiones recicladas
|
||||||
|
- Automatizar el proceso de reciclaje
|
||||||
|
|
||||||
|
## Modelo de Datos
|
||||||
|
|
||||||
|
### RecycledPost
|
||||||
|
|
||||||
|
```python
|
||||||
|
RecycledPost:
|
||||||
|
id: int
|
||||||
|
original_post_id: int (FK -> posts)
|
||||||
|
new_post_id: int (FK -> posts)
|
||||||
|
|
||||||
|
recycle_number: int # 1, 2, 3... (veces reciclado)
|
||||||
|
|
||||||
|
modifications: JSON
|
||||||
|
# {"content_changed": true, "hashtags_updated": true, "image_changed": false}
|
||||||
|
|
||||||
|
modification_notes: str
|
||||||
|
|
||||||
|
original_engagement_rate: float
|
||||||
|
new_engagement_rate: float
|
||||||
|
|
||||||
|
status: str # pending, published, cancelled
|
||||||
|
reason: str # high_performer, evergreen, seasonal, manual
|
||||||
|
|
||||||
|
recycled_at: datetime
|
||||||
|
scheduled_for: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
### Campos en Post
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Campos añadidos al modelo Post
|
||||||
|
is_recyclable: bool # Si puede ser reciclado (default True)
|
||||||
|
recycled_from_id: int (FK -> posts) # Post original si es reciclado
|
||||||
|
recycle_count: int # Veces que este post ha sido reciclado
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuración
|
||||||
|
|
||||||
|
```python
|
||||||
|
# En recycling_service.py
|
||||||
|
MIN_DAYS_SINCE_PUBLISH = 30 # No reciclar posts recientes
|
||||||
|
MIN_ENGAGEMENT_RATE = 2.0 # Mínimo 2% engagement
|
||||||
|
MAX_RECYCLE_COUNT = 3 # Máximo 3 veces por post
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### GET /api/recycling/candidates
|
||||||
|
Obtiene posts candidatos para reciclar.
|
||||||
|
|
||||||
|
**Parámetros:**
|
||||||
|
- `platform` (str): Filtrar por plataforma
|
||||||
|
- `content_type` (str): Filtrar por tipo de contenido
|
||||||
|
- `min_engagement_rate` (float, default=2.0): Engagement mínimo
|
||||||
|
- `min_days` (int, default=30): Días desde publicación
|
||||||
|
- `limit` (int, default=20): Máximo candidatos
|
||||||
|
|
||||||
|
**Respuesta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"candidates": [
|
||||||
|
{
|
||||||
|
"id": 123,
|
||||||
|
"content": "Tip: Usa IA para automatizar...",
|
||||||
|
"full_content": "...",
|
||||||
|
"content_type": "tip_tech",
|
||||||
|
"platforms": ["x"],
|
||||||
|
"published_at": "2024-12-01T10:00:00",
|
||||||
|
"days_since_publish": 58,
|
||||||
|
"engagement_rate": 4.5,
|
||||||
|
"recycle_count": 0,
|
||||||
|
"score": 3.8,
|
||||||
|
"metrics": {"likes": 150, "comments": 30}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"count": 10,
|
||||||
|
"filters": {
|
||||||
|
"min_engagement_rate": 2.0,
|
||||||
|
"min_days": 30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/recycling/{post_id}
|
||||||
|
Recicla un post específico.
|
||||||
|
|
||||||
|
**Body (opcional):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"content": "Versión actualizada del contenido...",
|
||||||
|
"hashtags": ["#nuevo", "#hashtag"],
|
||||||
|
"image_url": "https://...",
|
||||||
|
"scheduled_for": "2025-02-01T10:00:00",
|
||||||
|
"platforms": ["x", "threads"],
|
||||||
|
"reason": "high_performer"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Respuesta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Post recycled successfully",
|
||||||
|
"success": true,
|
||||||
|
"new_post_id": 456,
|
||||||
|
"recycle_record_id": 1,
|
||||||
|
"scheduled_for": "2025-02-01T10:00:00",
|
||||||
|
"platforms": ["x", "threads"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/recycling/auto
|
||||||
|
Reciclaje automático de mejores posts.
|
||||||
|
|
||||||
|
**Parámetros:**
|
||||||
|
- `platform` (str, opcional): Plataforma específica
|
||||||
|
- `count` (int, default=1, max=5): Cantidad a reciclar
|
||||||
|
- `min_engagement_rate` (float, default=2.0)
|
||||||
|
|
||||||
|
**Respuesta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"recycled": 2,
|
||||||
|
"posts": [
|
||||||
|
{
|
||||||
|
"original_id": 123,
|
||||||
|
"new_post_id": 456,
|
||||||
|
"engagement_rate": 4.5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /api/recycling/history
|
||||||
|
Obtiene historial de reciclaje.
|
||||||
|
|
||||||
|
**Parámetros:**
|
||||||
|
- `original_post_id` (int, opcional): Filtrar por post original
|
||||||
|
- `limit` (int, default=50)
|
||||||
|
|
||||||
|
### POST /api/recycling/{post_id}/disable
|
||||||
|
Marca un post como no reciclable.
|
||||||
|
|
||||||
|
## Sistema de Puntuación
|
||||||
|
|
||||||
|
Los candidatos se puntúan con la fórmula:
|
||||||
|
|
||||||
|
```python
|
||||||
|
score = engagement_rate * recency_factor * recycled_penalty
|
||||||
|
|
||||||
|
donde:
|
||||||
|
recency_factor = min(days_since_publish / 90, 1.0)
|
||||||
|
recycled_penalty = 1 - (recycle_count * 0.2)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ejemplo
|
||||||
|
|
||||||
|
| Post | Engagement | Días | Reciclajes | Score |
|
||||||
|
|------|------------|------|------------|-------|
|
||||||
|
| A | 4.0% | 60 | 0 | 4.0 × 0.67 × 1.0 = 2.68 |
|
||||||
|
| B | 5.0% | 90 | 1 | 5.0 × 1.0 × 0.8 = 4.00 |
|
||||||
|
| C | 3.0% | 45 | 0 | 3.0 × 0.5 × 1.0 = 1.50 |
|
||||||
|
|
||||||
|
Post B tiene el score más alto.
|
||||||
|
|
||||||
|
## Tareas Programadas
|
||||||
|
|
||||||
|
### auto_recycle_content
|
||||||
|
- **Frecuencia:** Diario a las 2:00 AM
|
||||||
|
- **Función:**
|
||||||
|
- Busca 1 post por plataforma con engagement > 3%
|
||||||
|
- Programa reciclaje automático
|
||||||
|
- **Plataformas:** x, threads
|
||||||
|
|
||||||
|
## Flujo de Reciclaje
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Identificar Candidatos
|
||||||
|
- Posts con > 30 días de antigüedad
|
||||||
|
- Engagement rate > 2%
|
||||||
|
- recycle_count < 3
|
||||||
|
- is_recyclable = true
|
||||||
|
↓
|
||||||
|
2. Seleccionar por Score
|
||||||
|
- Ordenar por score descendente
|
||||||
|
↓
|
||||||
|
3. Crear Post Reciclado
|
||||||
|
- Clonar contenido (o modificar)
|
||||||
|
- Establecer recycled_from_id
|
||||||
|
- Programar publicación
|
||||||
|
↓
|
||||||
|
4. Registrar en RecycledPost
|
||||||
|
- Guardar relación
|
||||||
|
- Incrementar recycle_count del original
|
||||||
|
↓
|
||||||
|
5. Publicar y Trackear
|
||||||
|
- Comparar new_engagement_rate vs original
|
||||||
|
```
|
||||||
|
|
||||||
|
## Razones de Reciclaje
|
||||||
|
|
||||||
|
| Razón | Descripción |
|
||||||
|
|-------|-------------|
|
||||||
|
| `high_performer` | Post con engagement alto |
|
||||||
|
| `evergreen` | Contenido atemporal relevante |
|
||||||
|
| `seasonal` | Contenido de temporada que regresa |
|
||||||
|
| `manual` | Reciclado manualmente |
|
||||||
|
|
||||||
|
## Ejemplo de Uso
|
||||||
|
|
||||||
|
```python
|
||||||
|
from app.services.recycling_service import recycling_service
|
||||||
|
|
||||||
|
# Buscar candidatos
|
||||||
|
candidates = await recycling_service.find_recyclable_posts(
|
||||||
|
platform="x",
|
||||||
|
min_engagement_rate=3.0,
|
||||||
|
limit=10
|
||||||
|
)
|
||||||
|
|
||||||
|
for candidate in candidates:
|
||||||
|
print(f"Post {candidate['id']}: {candidate['engagement_rate']}% - Score {candidate['score']}")
|
||||||
|
|
||||||
|
# Reciclar manualmente
|
||||||
|
result = await recycling_service.recycle_post(
|
||||||
|
post_id=123,
|
||||||
|
modifications={"content": "Versión actualizada..."},
|
||||||
|
reason="evergreen"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reciclaje automático
|
||||||
|
result = await recycling_service.auto_recycle(
|
||||||
|
platform="x",
|
||||||
|
count=2
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mejores Prácticas
|
||||||
|
|
||||||
|
1. **Esperar suficiente tiempo:** Mínimo 30 días entre reciclajes
|
||||||
|
2. **Modificar ligeramente:** Actualiza hashtags o intro
|
||||||
|
3. **No abusar:** Máximo 3 reciclajes por post
|
||||||
|
4. **Monitorear:** Compara rendimiento nuevo vs original
|
||||||
|
5. **Contenido evergreen:** Prioriza contenido atemporal
|
||||||
|
|
||||||
|
## Tipos de Contenido Ideales para Reciclar
|
||||||
|
|
||||||
|
| Tipo | Ideal | Razón |
|
||||||
|
|------|-------|-------|
|
||||||
|
| Tips técnicos | ✅ | Siempre relevantes |
|
||||||
|
| Tutoriales | ✅ | Valor educativo permanente |
|
||||||
|
| Productos | ⚠️ | Si stock disponible |
|
||||||
|
| Promociones | ❌ | Fechas específicas |
|
||||||
|
| Efemérides | ❌ | Fechas específicas |
|
||||||
|
| Noticias | ❌ | Se vuelven obsoletas |
|
||||||
218
docs/FEATURES_OVERVIEW.md
Normal file
218
docs/FEATURES_OVERVIEW.md
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
# Features Overview
|
||||||
|
|
||||||
|
Sistema de automatización de redes sociales para Consultoría AS.
|
||||||
|
|
||||||
|
## Tabla de Contenidos
|
||||||
|
|
||||||
|
1. [Analytics y Reportes](#analytics-y-reportes)
|
||||||
|
2. [Integración Odoo](#integración-odoo)
|
||||||
|
3. [A/B Testing](#ab-testing)
|
||||||
|
4. [Reciclaje de Contenido](#reciclaje-de-contenido)
|
||||||
|
5. [Thread Series](#thread-series)
|
||||||
|
6. [Image Templates](#image-templates)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Analytics y Reportes
|
||||||
|
|
||||||
|
Sistema completo de métricas y análisis de rendimiento.
|
||||||
|
|
||||||
|
**Documentación completa:** [ANALYTICS.md](./ANALYTICS.md)
|
||||||
|
|
||||||
|
### Características principales:
|
||||||
|
- Dashboard interactivo con métricas en tiempo real
|
||||||
|
- Tracking de engagement por post y plataforma
|
||||||
|
- Análisis de mejores horarios para publicar
|
||||||
|
- Reportes semanales automáticos vía Telegram
|
||||||
|
- Histórico de métricas por post
|
||||||
|
|
||||||
|
### Endpoints API:
|
||||||
|
| Método | Endpoint | Descripción |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| GET | `/api/analytics/dashboard` | Datos del dashboard |
|
||||||
|
| GET | `/api/analytics/top-posts` | Posts con mejor rendimiento |
|
||||||
|
| GET | `/api/analytics/optimal-times` | Mejores horarios |
|
||||||
|
| GET | `/api/analytics/engagement-trend` | Tendencia de engagement |
|
||||||
|
| GET | `/api/analytics/reports` | Reportes históricos |
|
||||||
|
| POST | `/api/analytics/reports/generate` | Generar reporte |
|
||||||
|
| POST | `/api/analytics/reports/send-telegram` | Enviar reporte a Telegram |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integración Odoo
|
||||||
|
|
||||||
|
Sincronización bidireccional con Odoo ERP.
|
||||||
|
|
||||||
|
**Documentación completa:** [ODOO_INTEGRATION.md](./ODOO_INTEGRATION.md)
|
||||||
|
|
||||||
|
### Características principales:
|
||||||
|
- Sincronización de productos desde Odoo
|
||||||
|
- Sincronización de servicios desde Odoo
|
||||||
|
- Exportación de leads al CRM de Odoo
|
||||||
|
- Gestión de leads desde interacciones sociales
|
||||||
|
- Logs de sincronización
|
||||||
|
|
||||||
|
### Endpoints API:
|
||||||
|
| Método | Endpoint | Descripción |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| GET | `/api/odoo/status` | Estado de conexión |
|
||||||
|
| POST | `/api/odoo/sync/products` | Sincronizar productos |
|
||||||
|
| POST | `/api/odoo/sync/services` | Sincronizar servicios |
|
||||||
|
| POST | `/api/odoo/sync/leads` | Exportar leads |
|
||||||
|
| GET | `/api/odoo/sync/logs` | Historial de sync |
|
||||||
|
| GET | `/api/leads/` | Listar leads |
|
||||||
|
| POST | `/api/leads/` | Crear lead |
|
||||||
|
| POST | `/api/leads/from-interaction/{id}` | Lead desde interacción |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## A/B Testing
|
||||||
|
|
||||||
|
Sistema de pruebas A/B para optimizar contenido.
|
||||||
|
|
||||||
|
**Documentación completa:** [AB_TESTING.md](./AB_TESTING.md)
|
||||||
|
|
||||||
|
### Características principales:
|
||||||
|
- Crear tests con 2-4 variantes
|
||||||
|
- Publicación automática de variantes
|
||||||
|
- Seguimiento de métricas por variante
|
||||||
|
- Análisis estadístico (chi-square)
|
||||||
|
- Determinación automática de ganador
|
||||||
|
|
||||||
|
### Endpoints API:
|
||||||
|
| Método | Endpoint | Descripción |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| GET | `/api/ab-tests/` | Listar tests |
|
||||||
|
| POST | `/api/ab-tests/` | Crear test |
|
||||||
|
| POST | `/api/ab-tests/{id}/start` | Iniciar test |
|
||||||
|
| POST | `/api/ab-tests/{id}/evaluate` | Evaluar resultados |
|
||||||
|
| GET | `/api/ab-tests/{id}/results` | Ver resultados |
|
||||||
|
| POST | `/api/ab-tests/{id}/cancel` | Cancelar test |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reciclaje de Contenido
|
||||||
|
|
||||||
|
Republica contenido exitoso automáticamente.
|
||||||
|
|
||||||
|
**Documentación completa:** [CONTENT_RECYCLING.md](./CONTENT_RECYCLING.md)
|
||||||
|
|
||||||
|
### Características principales:
|
||||||
|
- Identificación de posts reciclables por engagement
|
||||||
|
- Sistema de puntuación para candidatos
|
||||||
|
- Modificaciones opcionales al reciclar
|
||||||
|
- Límite de reciclajes por post
|
||||||
|
- Reciclaje automático programado
|
||||||
|
|
||||||
|
### Endpoints API:
|
||||||
|
| Método | Endpoint | Descripción |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| GET | `/api/recycling/candidates` | Posts candidatos |
|
||||||
|
| POST | `/api/recycling/{post_id}` | Reciclar post |
|
||||||
|
| POST | `/api/recycling/auto` | Reciclaje automático |
|
||||||
|
| GET | `/api/recycling/history` | Historial |
|
||||||
|
| POST | `/api/recycling/{post_id}/disable` | Deshabilitar reciclaje |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Thread Series
|
||||||
|
|
||||||
|
Publica hilos de múltiples posts programados.
|
||||||
|
|
||||||
|
**Documentación completa:** [THREAD_SERIES.md](./THREAD_SERIES.md)
|
||||||
|
|
||||||
|
### Características principales:
|
||||||
|
- Crear series de posts conectados
|
||||||
|
- Generación con IA
|
||||||
|
- Programación con intervalos configurables
|
||||||
|
- Soporte para reply chains
|
||||||
|
- Seguimiento de progreso
|
||||||
|
|
||||||
|
### Endpoints API:
|
||||||
|
| Método | Endpoint | Descripción |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| GET | `/api/threads/` | Listar series |
|
||||||
|
| POST | `/api/threads/` | Crear serie manual |
|
||||||
|
| POST | `/api/threads/generate` | Generar con IA |
|
||||||
|
| POST | `/api/threads/{id}/schedule` | Programar serie |
|
||||||
|
| POST | `/api/threads/{id}/publish-next` | Publicar siguiente |
|
||||||
|
| POST | `/api/threads/{id}/cancel` | Cancelar serie |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Image Templates
|
||||||
|
|
||||||
|
Sistema de plantillas para generar imágenes.
|
||||||
|
|
||||||
|
**Documentación completa:** [IMAGE_TEMPLATES.md](./IMAGE_TEMPLATES.md)
|
||||||
|
|
||||||
|
### Características principales:
|
||||||
|
- Plantillas HTML/CSS inline
|
||||||
|
- Variables dinámicas
|
||||||
|
- Múltiples tamaños de salida
|
||||||
|
- Categorías y tipos de plantillas
|
||||||
|
- Preview antes de generar
|
||||||
|
|
||||||
|
### Endpoints API:
|
||||||
|
| Método | Endpoint | Descripción |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| GET | `/api/templates/` | Listar plantillas |
|
||||||
|
| POST | `/api/templates/` | Crear plantilla |
|
||||||
|
| PUT | `/api/templates/{id}` | Actualizar |
|
||||||
|
| DELETE | `/api/templates/{id}` | Eliminar |
|
||||||
|
| POST | `/api/templates/preview` | Previsualizar |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tareas Programadas (Celery Beat)
|
||||||
|
|
||||||
|
| Tarea | Frecuencia | Descripción |
|
||||||
|
|-------|------------|-------------|
|
||||||
|
| `check_scheduled_posts` | Cada minuto | Publica posts programados |
|
||||||
|
| `check_thread_schedules` | Cada minuto | Publica posts de hilos |
|
||||||
|
| `fetch_post_metrics` | Cada 15 min | Actualiza métricas |
|
||||||
|
| `sync_interactions` | Cada 15 min | Sincroniza interacciones |
|
||||||
|
| `export_leads_to_odoo` | Cada hora | Exporta leads |
|
||||||
|
| `evaluate_ab_tests` | Cada hora | Evalúa tests A/B |
|
||||||
|
| `generate_daily_content` | 6:00 AM | Genera contenido |
|
||||||
|
| `sync_products_from_odoo` | 6:00 AM | Sincroniza productos |
|
||||||
|
| `sync_services_from_odoo` | 6:05 AM | Sincroniza servicios |
|
||||||
|
| `auto_recycle_content` | 2:00 AM | Recicla contenido |
|
||||||
|
| `recalculate_optimal_times` | Lunes 2:00 AM | Recalcula horarios |
|
||||||
|
| `generate_weekly_report` | Domingo 9:00 AM | Genera reporte |
|
||||||
|
| `send_daily_summary` | 9:00 PM | Resumen diario |
|
||||||
|
| `cleanup_old_data` | Domingo 3:00 AM | Limpieza de datos |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuración
|
||||||
|
|
||||||
|
Variables de entorno requeridas en `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Analytics
|
||||||
|
ANALYTICS_FETCH_INTERVAL=15
|
||||||
|
TELEGRAM_REPORT_ENABLED=true
|
||||||
|
TELEGRAM_REPORT_DAY=6
|
||||||
|
|
||||||
|
# Odoo
|
||||||
|
ODOO_URL=https://tuempresa.odoo.com
|
||||||
|
ODOO_DB=nombre_bd
|
||||||
|
ODOO_USERNAME=usuario
|
||||||
|
ODOO_PASSWORD=api_key
|
||||||
|
ODOO_SYNC_ENABLED=false
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dashboard URLs
|
||||||
|
|
||||||
|
| URL | Descripción |
|
||||||
|
|-----|-------------|
|
||||||
|
| `/dashboard` | Panel principal |
|
||||||
|
| `/dashboard/analytics` | Analytics |
|
||||||
|
| `/dashboard/leads` | Gestión de leads |
|
||||||
|
| `/dashboard/posts` | Gestión de posts |
|
||||||
|
| `/dashboard/calendar` | Calendario |
|
||||||
|
| `/dashboard/interactions` | Interacciones |
|
||||||
|
| `/api/docs` | Documentación Swagger |
|
||||||
350
docs/IMAGE_TEMPLATES.md
Normal file
350
docs/IMAGE_TEMPLATES.md
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
# Image Templates
|
||||||
|
|
||||||
|
Sistema de plantillas para generar imágenes dinámicas.
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Las plantillas de imagen permiten:
|
||||||
|
- Definir diseños reutilizables en HTML/CSS
|
||||||
|
- Insertar variables dinámicas
|
||||||
|
- Generar imágenes para diferentes plataformas
|
||||||
|
- Mantener consistencia visual
|
||||||
|
|
||||||
|
## Modelo de Datos
|
||||||
|
|
||||||
|
### ImageTemplate
|
||||||
|
|
||||||
|
```python
|
||||||
|
ImageTemplate:
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
|
||||||
|
category: str # tip, producto, servicio, promocion, etc.
|
||||||
|
template_type: str # tip_card, product_card, quote, promo, announcement
|
||||||
|
|
||||||
|
# Plantilla
|
||||||
|
template_file: str # Ruta a archivo (opcional)
|
||||||
|
html_template: str # HTML inline
|
||||||
|
|
||||||
|
preview_url: str # URL de imagen preview
|
||||||
|
|
||||||
|
# Variables
|
||||||
|
variables: [str] # ["title", "content", "accent_color"]
|
||||||
|
|
||||||
|
# Configuración de diseño
|
||||||
|
design_config: JSON
|
||||||
|
# {
|
||||||
|
# "width": 1080,
|
||||||
|
# "height": 1080,
|
||||||
|
# "background_color": "#1a1a2e",
|
||||||
|
# "accent_color": "#d4a574",
|
||||||
|
# "font_family": "Inter"
|
||||||
|
# }
|
||||||
|
|
||||||
|
# Tamaños de salida
|
||||||
|
output_sizes: JSON
|
||||||
|
# {
|
||||||
|
# "instagram": {"width": 1080, "height": 1080},
|
||||||
|
# "x": {"width": 1200, "height": 675},
|
||||||
|
# "facebook": {"width": 1200, "height": 630}
|
||||||
|
# }
|
||||||
|
|
||||||
|
is_active: bool
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### GET /api/templates/
|
||||||
|
Lista todas las plantillas.
|
||||||
|
|
||||||
|
**Parámetros:**
|
||||||
|
- `category`: tip, producto, servicio, etc.
|
||||||
|
- `template_type`: tip_card, product_card, etc.
|
||||||
|
- `active_only` (bool, default=true)
|
||||||
|
- `limit` (int, default=50)
|
||||||
|
|
||||||
|
**Respuesta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"templates": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Tip Card Oscuro",
|
||||||
|
"description": "Tarjeta para tips con fondo oscuro",
|
||||||
|
"category": "tip",
|
||||||
|
"template_type": "tip_card",
|
||||||
|
"variables": ["title", "content", "emoji"],
|
||||||
|
"preview_url": "/uploads/previews/tip_card_dark.png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"count": 10
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /api/templates/{template_id}
|
||||||
|
Obtiene una plantilla con detalles completos.
|
||||||
|
|
||||||
|
Incluye `full_html_template` con el HTML completo.
|
||||||
|
|
||||||
|
### POST /api/templates/
|
||||||
|
Crea una nueva plantilla.
|
||||||
|
|
||||||
|
**Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Quote Card",
|
||||||
|
"description": "Tarjeta para frases motivacionales",
|
||||||
|
"category": "frase",
|
||||||
|
"template_type": "quote",
|
||||||
|
"html_template": "<div class='card'>...</div>",
|
||||||
|
"variables": ["quote", "author", "background_image"],
|
||||||
|
"design_config": {
|
||||||
|
"width": 1080,
|
||||||
|
"height": 1080,
|
||||||
|
"background_color": "#1a1a2e"
|
||||||
|
},
|
||||||
|
"output_sizes": {
|
||||||
|
"instagram": {"width": 1080, "height": 1080},
|
||||||
|
"x": {"width": 1200, "height": 675}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validación:**
|
||||||
|
- Requiere `html_template` o `template_file`
|
||||||
|
|
||||||
|
### PUT /api/templates/{template_id}
|
||||||
|
Actualiza una plantilla.
|
||||||
|
|
||||||
|
### DELETE /api/templates/{template_id}
|
||||||
|
Elimina una plantilla.
|
||||||
|
|
||||||
|
### POST /api/templates/preview
|
||||||
|
Genera preview de una plantilla con variables.
|
||||||
|
|
||||||
|
**Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"template_id": 1,
|
||||||
|
"variables": {
|
||||||
|
"title": "Tip del día",
|
||||||
|
"content": "Automatiza tus procesos con IA",
|
||||||
|
"emoji": "💡"
|
||||||
|
},
|
||||||
|
"output_size": {"width": 1080, "height": 1080}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
O con HTML directo:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"html_template": "<div>{{title}}</div>",
|
||||||
|
"variables": {
|
||||||
|
"title": "Mi título"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Respuesta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"rendered_html": "<div>Mi título</div>",
|
||||||
|
"output_size": {"width": 1080, "height": 1080},
|
||||||
|
"variables_used": ["title"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /api/templates/categories/list
|
||||||
|
Lista categorías disponibles.
|
||||||
|
|
||||||
|
### GET /api/templates/types/list
|
||||||
|
Lista tipos de plantilla disponibles.
|
||||||
|
|
||||||
|
## Estructura HTML
|
||||||
|
|
||||||
|
### Variables
|
||||||
|
Las variables se definen con doble llave: `{{variable}}`
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="card">
|
||||||
|
<span class="emoji">{{emoji}}</span>
|
||||||
|
<h1>{{title}}</h1>
|
||||||
|
<p>{{content}}</p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ejemplo Completo
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
width: 1080px;
|
||||||
|
height: 1080px;
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
color: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
width: 900px;
|
||||||
|
padding: 60px;
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
border-radius: 24px;
|
||||||
|
border: 1px solid rgba(212, 165, 116, 0.3);
|
||||||
|
}
|
||||||
|
.emoji {
|
||||||
|
font-size: 64px;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #d4a574;
|
||||||
|
font-size: 36px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
margin-top: 40px;
|
||||||
|
color: #888;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<span class="emoji">{{emoji}}</span>
|
||||||
|
<h1>{{title}}</h1>
|
||||||
|
<p>{{content}}</p>
|
||||||
|
<div class="footer">@{{username}} • Consultoría AS</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Categorías
|
||||||
|
|
||||||
|
| Categoría | Uso |
|
||||||
|
|-----------|-----|
|
||||||
|
| `tip` | Tips tecnológicos |
|
||||||
|
| `producto` | Fichas de producto |
|
||||||
|
| `servicio` | Promoción de servicios |
|
||||||
|
| `promocion` | Ofertas y descuentos |
|
||||||
|
| `frase` | Frases motivacionales |
|
||||||
|
| `dato` | Datos curiosos |
|
||||||
|
| `anuncio` | Anuncios generales |
|
||||||
|
|
||||||
|
## Tipos de Plantilla
|
||||||
|
|
||||||
|
| Tipo | Descripción |
|
||||||
|
|------|-------------|
|
||||||
|
| `tip_card` | Tarjeta de tip con emoji |
|
||||||
|
| `product_card` | Ficha de producto con imagen |
|
||||||
|
| `quote` | Frase con autor |
|
||||||
|
| `promo` | Promoción con precio |
|
||||||
|
| `announcement` | Anuncio destacado |
|
||||||
|
| `stats` | Estadísticas/números |
|
||||||
|
| `comparison` | Antes/después o A vs B |
|
||||||
|
|
||||||
|
## Tamaños por Plataforma
|
||||||
|
|
||||||
|
| Plataforma | Ancho | Alto | Ratio |
|
||||||
|
|------------|-------|------|-------|
|
||||||
|
| Instagram Post | 1080 | 1080 | 1:1 |
|
||||||
|
| Instagram Story | 1080 | 1920 | 9:16 |
|
||||||
|
| X (Twitter) | 1200 | 675 | 16:9 |
|
||||||
|
| Facebook | 1200 | 630 | 1.91:1 |
|
||||||
|
| LinkedIn | 1200 | 627 | 1.91:1 |
|
||||||
|
| Threads | 1080 | 1080 | 1:1 |
|
||||||
|
|
||||||
|
## Ejemplo de Uso
|
||||||
|
|
||||||
|
```python
|
||||||
|
from app.models.image_template import ImageTemplate
|
||||||
|
from app.core.database import SessionLocal
|
||||||
|
|
||||||
|
db = SessionLocal()
|
||||||
|
|
||||||
|
# Crear plantilla
|
||||||
|
template = ImageTemplate(
|
||||||
|
name="Tip Card Dark",
|
||||||
|
category="tip",
|
||||||
|
template_type="tip_card",
|
||||||
|
html_template="""
|
||||||
|
<div style="...">
|
||||||
|
<h1>{{title}}</h1>
|
||||||
|
<p>{{content}}</p>
|
||||||
|
</div>
|
||||||
|
""",
|
||||||
|
variables=["title", "content"],
|
||||||
|
design_config={
|
||||||
|
"width": 1080,
|
||||||
|
"height": 1080,
|
||||||
|
"background_color": "#1a1a2e"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
db.add(template)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Renderizar
|
||||||
|
html = template.html_template
|
||||||
|
html = html.replace("{{title}}", "Mi Título")
|
||||||
|
html = html.replace("{{content}}", "Mi contenido")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generación de Imágenes
|
||||||
|
|
||||||
|
Para convertir HTML a imagen se recomienda:
|
||||||
|
|
||||||
|
### Playwright (recomendado)
|
||||||
|
```python
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
|
||||||
|
async def html_to_image(html: str, width: int, height: int) -> bytes:
|
||||||
|
async with async_playwright() as p:
|
||||||
|
browser = await p.chromium.launch()
|
||||||
|
page = await browser.new_page(viewport={"width": width, "height": height})
|
||||||
|
await page.set_content(html)
|
||||||
|
screenshot = await page.screenshot(type="png")
|
||||||
|
await browser.close()
|
||||||
|
return screenshot
|
||||||
|
```
|
||||||
|
|
||||||
|
### WeasyPrint (alternativa)
|
||||||
|
```python
|
||||||
|
from weasyprint import HTML
|
||||||
|
|
||||||
|
def html_to_pdf(html: str) -> bytes:
|
||||||
|
return HTML(string=html).write_pdf()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mejores Prácticas
|
||||||
|
|
||||||
|
1. **Fuentes web:** Usa Google Fonts o incluye fuentes inline
|
||||||
|
2. **Colores consistentes:** Define palette en design_config
|
||||||
|
3. **Responsive:** Usa unidades relativas cuando sea posible
|
||||||
|
4. **Contraste:** Asegura legibilidad texto/fondo
|
||||||
|
5. **Logo:** Incluye branding en todas las plantillas
|
||||||
|
6. **Variables descriptivas:** `{{product_name}}` mejor que `{{name}}`
|
||||||
|
7. **Fallbacks:** Define valores por defecto para variables opcionales
|
||||||
|
|
||||||
|
## Integración con Posts
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Al crear un post con imagen generada
|
||||||
|
post = Post(
|
||||||
|
content="Tip del día...",
|
||||||
|
image_template_id=template.id, # Referencia a la plantilla usada
|
||||||
|
image_url="/uploads/generated/tip_123.png"
|
||||||
|
)
|
||||||
|
```
|
||||||
369
docs/ODOO_INTEGRATION.md
Normal file
369
docs/ODOO_INTEGRATION.md
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
# Integración con Odoo
|
||||||
|
|
||||||
|
Sincronización bidireccional con Odoo ERP para productos, servicios y leads.
|
||||||
|
|
||||||
|
## Descripción General
|
||||||
|
|
||||||
|
La integración permite:
|
||||||
|
- **Importar** productos y servicios desde Odoo
|
||||||
|
- **Exportar** leads generados en redes sociales al CRM de Odoo
|
||||||
|
- **Consultar** resumen de ventas
|
||||||
|
|
||||||
|
## Configuración
|
||||||
|
|
||||||
|
### Variables de Entorno
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
ODOO_URL=https://tuempresa.odoo.com
|
||||||
|
ODOO_DB=nombre_base_datos
|
||||||
|
ODOO_USERNAME=usuario@empresa.com
|
||||||
|
ODOO_PASSWORD=api_key_o_password
|
||||||
|
ODOO_SYNC_ENABLED=true # false para deshabilitar
|
||||||
|
```
|
||||||
|
|
||||||
|
### Obtener API Key en Odoo
|
||||||
|
|
||||||
|
1. Ir a **Ajustes > Usuarios**
|
||||||
|
2. Seleccionar tu usuario
|
||||||
|
3. Pestaña **Claves de API**
|
||||||
|
4. Crear nueva clave
|
||||||
|
|
||||||
|
## Modelos de Datos
|
||||||
|
|
||||||
|
### Lead
|
||||||
|
Leads generados desde interacciones en redes sociales.
|
||||||
|
|
||||||
|
```python
|
||||||
|
Lead:
|
||||||
|
id: int
|
||||||
|
interaction_id: int (FK, opcional)
|
||||||
|
platform: str # x, threads, instagram, facebook, manual
|
||||||
|
|
||||||
|
# Contacto
|
||||||
|
name: str
|
||||||
|
email: str
|
||||||
|
phone: str
|
||||||
|
company: str
|
||||||
|
|
||||||
|
# Social
|
||||||
|
username: str
|
||||||
|
profile_url: str
|
||||||
|
|
||||||
|
# Interés
|
||||||
|
interest: str
|
||||||
|
source_content: str
|
||||||
|
notes: str
|
||||||
|
products_interested: [int] # IDs de productos
|
||||||
|
services_interested: [int] # IDs de servicios
|
||||||
|
|
||||||
|
# Estado
|
||||||
|
status: str # new, contacted, qualified, proposal, won, lost
|
||||||
|
priority: str # low, medium, high, urgent
|
||||||
|
|
||||||
|
# Odoo
|
||||||
|
odoo_lead_id: int
|
||||||
|
synced_to_odoo: bool
|
||||||
|
odoo_synced_at: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
### OdooSyncLog
|
||||||
|
Registro de operaciones de sincronización.
|
||||||
|
|
||||||
|
```python
|
||||||
|
OdooSyncLog:
|
||||||
|
id: int
|
||||||
|
sync_type: str # products, services, leads, sales
|
||||||
|
direction: str # import, export
|
||||||
|
status: str # started, completed, failed, partial
|
||||||
|
records_processed: int
|
||||||
|
records_created: int
|
||||||
|
records_updated: int
|
||||||
|
records_failed: int
|
||||||
|
error_message: str
|
||||||
|
error_details: JSON
|
||||||
|
started_at: datetime
|
||||||
|
completed_at: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
### Campos Odoo en Product/Service
|
||||||
|
|
||||||
|
```python
|
||||||
|
# En Product
|
||||||
|
odoo_product_id: int (unique)
|
||||||
|
odoo_last_synced: datetime
|
||||||
|
|
||||||
|
# En Service
|
||||||
|
odoo_service_id: int (unique)
|
||||||
|
odoo_last_synced: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Odoo Status
|
||||||
|
|
||||||
|
#### GET /api/odoo/status
|
||||||
|
Verifica conexión con Odoo.
|
||||||
|
|
||||||
|
**Respuesta exitosa:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"connected": true,
|
||||||
|
"version": "17.0",
|
||||||
|
"uid": 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Sin conexión:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"connected": false,
|
||||||
|
"error": "Authentication failed",
|
||||||
|
"configured": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sincronización
|
||||||
|
|
||||||
|
#### POST /api/odoo/sync/products
|
||||||
|
Sincroniza productos desde Odoo.
|
||||||
|
|
||||||
|
**Parámetros:**
|
||||||
|
- `limit` (int, default=100): Máximo de productos
|
||||||
|
|
||||||
|
**Respuesta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Products synced successfully",
|
||||||
|
"success": true,
|
||||||
|
"processed": 50,
|
||||||
|
"created": 10,
|
||||||
|
"updated": 40,
|
||||||
|
"failed": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### POST /api/odoo/sync/services
|
||||||
|
Sincroniza servicios desde Odoo.
|
||||||
|
|
||||||
|
#### POST /api/odoo/sync/leads
|
||||||
|
Exporta leads sin sincronizar a Odoo CRM.
|
||||||
|
|
||||||
|
**Respuesta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Leads exported successfully",
|
||||||
|
"success": true,
|
||||||
|
"processed": 5,
|
||||||
|
"created": 5,
|
||||||
|
"failed": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GET /api/odoo/sync/logs
|
||||||
|
Obtiene historial de sincronizaciones.
|
||||||
|
|
||||||
|
**Parámetros:**
|
||||||
|
- `limit` (int, default=20)
|
||||||
|
|
||||||
|
#### GET /api/odoo/sales
|
||||||
|
Obtiene resumen de ventas desde Odoo.
|
||||||
|
|
||||||
|
**Parámetros:**
|
||||||
|
- `days` (int, default=30)
|
||||||
|
|
||||||
|
**Respuesta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"period_days": 30,
|
||||||
|
"total_orders": 15,
|
||||||
|
"total_revenue": 125000.00,
|
||||||
|
"avg_order_value": 8333.33,
|
||||||
|
"orders": [...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Leads API
|
||||||
|
|
||||||
|
#### GET /api/leads/
|
||||||
|
Lista leads con filtros.
|
||||||
|
|
||||||
|
**Parámetros:**
|
||||||
|
- `status`: new, contacted, qualified, proposal, won, lost
|
||||||
|
- `priority`: low, medium, high, urgent
|
||||||
|
- `platform`: x, threads, instagram, facebook, manual
|
||||||
|
- `synced`: true/false (sincronizado a Odoo)
|
||||||
|
- `limit`, `offset`: Paginación
|
||||||
|
|
||||||
|
#### GET /api/leads/{lead_id}
|
||||||
|
Obtiene un lead específico.
|
||||||
|
|
||||||
|
#### POST /api/leads/
|
||||||
|
Crea lead manualmente.
|
||||||
|
|
||||||
|
**Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Juan Pérez",
|
||||||
|
"email": "juan@empresa.com",
|
||||||
|
"phone": "+52 664 123 4567",
|
||||||
|
"company": "Empresa SA",
|
||||||
|
"platform": "manual",
|
||||||
|
"interest": "Interesado en automatización",
|
||||||
|
"priority": "high"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### POST /api/leads/from-interaction/{interaction_id}
|
||||||
|
Convierte una interacción en lead.
|
||||||
|
|
||||||
|
**Parámetros:**
|
||||||
|
- `interest` (str): Descripción del interés
|
||||||
|
- `priority` (str): Prioridad
|
||||||
|
- `notes` (str): Notas adicionales
|
||||||
|
|
||||||
|
#### PUT /api/leads/{lead_id}
|
||||||
|
Actualiza un lead.
|
||||||
|
|
||||||
|
#### DELETE /api/leads/{lead_id}
|
||||||
|
Elimina un lead.
|
||||||
|
|
||||||
|
#### POST /api/leads/{lead_id}/sync-odoo
|
||||||
|
Sincroniza un lead específico a Odoo.
|
||||||
|
|
||||||
|
**Respuesta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Lead synced to Odoo successfully",
|
||||||
|
"odoo_lead_id": 1234
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GET /api/leads/stats/summary
|
||||||
|
Obtiene estadísticas de leads.
|
||||||
|
|
||||||
|
**Respuesta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"total": 50,
|
||||||
|
"by_status": {
|
||||||
|
"new": 20,
|
||||||
|
"contacted": 15,
|
||||||
|
"qualified": 10,
|
||||||
|
"won": 5
|
||||||
|
},
|
||||||
|
"by_platform": {
|
||||||
|
"x": 25,
|
||||||
|
"threads": 15,
|
||||||
|
"manual": 10
|
||||||
|
},
|
||||||
|
"by_priority": {
|
||||||
|
"high": 10,
|
||||||
|
"medium": 30,
|
||||||
|
"low": 10
|
||||||
|
},
|
||||||
|
"unsynced_to_odoo": 8
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tareas Programadas
|
||||||
|
|
||||||
|
### sync_products_from_odoo
|
||||||
|
- **Frecuencia:** Diario a las 6:00 AM
|
||||||
|
- **Función:** Importa/actualiza productos desde Odoo
|
||||||
|
- **Límite:** 200 productos por ejecución
|
||||||
|
|
||||||
|
### sync_services_from_odoo
|
||||||
|
- **Frecuencia:** Diario a las 6:05 AM
|
||||||
|
- **Función:** Importa/actualiza servicios desde Odoo
|
||||||
|
- **Límite:** 100 servicios por ejecución
|
||||||
|
|
||||||
|
### export_leads_to_odoo
|
||||||
|
- **Frecuencia:** Cada hora (minuto 30)
|
||||||
|
- **Función:** Exporta leads no sincronizados al CRM
|
||||||
|
|
||||||
|
## Dashboard de Leads
|
||||||
|
|
||||||
|
Accede en `/dashboard/leads`.
|
||||||
|
|
||||||
|
### Características:
|
||||||
|
- Cards de estadísticas (total, nuevos, contactados, etc.)
|
||||||
|
- Filtros por estado, prioridad y plataforma
|
||||||
|
- Lista paginada de leads
|
||||||
|
- Modal para crear/editar leads
|
||||||
|
- Botones de sincronización individual y masiva
|
||||||
|
|
||||||
|
### Estados de Lead
|
||||||
|
|
||||||
|
| Estado | Descripción |
|
||||||
|
|--------|-------------|
|
||||||
|
| `new` | Lead nuevo sin contactar |
|
||||||
|
| `contacted` | Se ha hecho contacto inicial |
|
||||||
|
| `qualified` | Lead calificado con potencial |
|
||||||
|
| `proposal` | Se envió propuesta |
|
||||||
|
| `won` | Lead convertido a cliente |
|
||||||
|
| `lost` | Lead perdido |
|
||||||
|
|
||||||
|
### Prioridades
|
||||||
|
|
||||||
|
| Prioridad | Color |
|
||||||
|
|-----------|-------|
|
||||||
|
| `urgent` | Rojo |
|
||||||
|
| `high` | Naranja |
|
||||||
|
| `medium` | Azul |
|
||||||
|
| `low` | Gris |
|
||||||
|
|
||||||
|
## Ejemplo de Uso
|
||||||
|
|
||||||
|
```python
|
||||||
|
from app.services.odoo_service import odoo_service
|
||||||
|
|
||||||
|
# Verificar conexión
|
||||||
|
status = await odoo_service.test_connection()
|
||||||
|
if status["connected"]:
|
||||||
|
print(f"Conectado a Odoo {status['version']}")
|
||||||
|
|
||||||
|
# Sincronizar productos
|
||||||
|
result = await odoo_service.sync_products(limit=50)
|
||||||
|
print(f"Productos: {result['created']} nuevos, {result['updated']} actualizados")
|
||||||
|
|
||||||
|
# Exportar leads
|
||||||
|
result = await odoo_service.export_leads_to_odoo()
|
||||||
|
print(f"Leads exportados: {result['created']}")
|
||||||
|
|
||||||
|
# Obtener ventas
|
||||||
|
sales = await odoo_service.get_sales_summary(days=30)
|
||||||
|
print(f"Ventas: ${sales['total_revenue']:,.2f}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mapeo de Campos
|
||||||
|
|
||||||
|
### Producto Odoo → Local
|
||||||
|
| Odoo | Local |
|
||||||
|
|------|-------|
|
||||||
|
| id | odoo_product_id |
|
||||||
|
| name | name |
|
||||||
|
| description_sale | description |
|
||||||
|
| list_price | price |
|
||||||
|
| categ_id | category |
|
||||||
|
| qty_available | stock |
|
||||||
|
|
||||||
|
### Lead Local → Odoo
|
||||||
|
| Local | Odoo |
|
||||||
|
|-------|------|
|
||||||
|
| interest | name |
|
||||||
|
| name | contact_name |
|
||||||
|
| email | email_from |
|
||||||
|
| phone | phone |
|
||||||
|
| company | partner_name |
|
||||||
|
| (generated) | description |
|
||||||
|
| priority | priority (0-2) |
|
||||||
|
|
||||||
|
## Notas Técnicas
|
||||||
|
|
||||||
|
- La conexión usa XML-RPC (protocolo estándar de Odoo)
|
||||||
|
- Cada sincronización crea un registro en `odoo_sync_logs`
|
||||||
|
- Los productos/servicios se identifican por `odoo_product_id`/`odoo_service_id`
|
||||||
|
- Si un producto local ya existe (mismo ID de Odoo), se actualiza
|
||||||
|
- Los leads se exportan como tipo "lead" (no "opportunity")
|
||||||
334
docs/THREAD_SERIES.md
Normal file
334
docs/THREAD_SERIES.md
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
# Thread Series
|
||||||
|
|
||||||
|
Sistema para crear y publicar hilos de múltiples posts conectados.
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Los Thread Series permiten:
|
||||||
|
- Crear contenido largo dividido en posts conectados
|
||||||
|
- Generar hilos con IA
|
||||||
|
- Programar publicación escalonada
|
||||||
|
- Mantener cadena de respuestas (reply chain)
|
||||||
|
|
||||||
|
## Modelos de Datos
|
||||||
|
|
||||||
|
### ThreadSeries
|
||||||
|
|
||||||
|
```python
|
||||||
|
ThreadSeries:
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
topic: str
|
||||||
|
|
||||||
|
platform: str # x, threads
|
||||||
|
|
||||||
|
# Configuración de programación
|
||||||
|
schedule_type: str # sequential, timed
|
||||||
|
interval_minutes: int # Tiempo entre posts (default 5)
|
||||||
|
start_time: datetime
|
||||||
|
|
||||||
|
# Estado
|
||||||
|
total_posts: int
|
||||||
|
posts_published: int
|
||||||
|
status: str # draft, scheduled, publishing, completed, paused, cancelled
|
||||||
|
|
||||||
|
# Reply chain
|
||||||
|
first_platform_post_id: str # ID del primer post (para replies)
|
||||||
|
|
||||||
|
# IA
|
||||||
|
ai_generated: bool
|
||||||
|
generation_prompt: str
|
||||||
|
|
||||||
|
hashtags: JSON # Hashtags comunes
|
||||||
|
```
|
||||||
|
|
||||||
|
### ThreadPost
|
||||||
|
|
||||||
|
```python
|
||||||
|
ThreadPost:
|
||||||
|
id: int
|
||||||
|
series_id: int (FK -> thread_series)
|
||||||
|
sequence_number: int # 1, 2, 3...
|
||||||
|
|
||||||
|
content: str
|
||||||
|
image_url: str
|
||||||
|
|
||||||
|
post_id: int (FK -> posts)
|
||||||
|
|
||||||
|
# Para reply chain
|
||||||
|
platform_post_id: str
|
||||||
|
reply_to_platform_id: str
|
||||||
|
|
||||||
|
scheduled_at: datetime
|
||||||
|
status: str # pending, scheduled, published, failed
|
||||||
|
error_message: str
|
||||||
|
published_at: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### GET /api/threads/
|
||||||
|
Lista todas las series.
|
||||||
|
|
||||||
|
**Parámetros:**
|
||||||
|
- `status`: draft, scheduled, publishing, completed
|
||||||
|
- `platform`: x, threads
|
||||||
|
- `limit` (int, default=20)
|
||||||
|
|
||||||
|
### GET /api/threads/{series_id}
|
||||||
|
Obtiene una serie con todos sus posts.
|
||||||
|
|
||||||
|
**Respuesta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Hilo sobre IA",
|
||||||
|
"topic": "Inteligencia Artificial",
|
||||||
|
"platform": "x",
|
||||||
|
"status": "scheduled",
|
||||||
|
"total_posts": 5,
|
||||||
|
"posts_published": 0,
|
||||||
|
"start_time": "2025-02-01T10:00:00",
|
||||||
|
"interval_minutes": 5,
|
||||||
|
"posts": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"sequence_number": 1,
|
||||||
|
"content": "🧵 Hilo: Todo sobre IA...",
|
||||||
|
"status": "scheduled",
|
||||||
|
"scheduled_at": "2025-02-01T10:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"sequence_number": 2,
|
||||||
|
"content": "1/ La IA está transformando...",
|
||||||
|
"status": "scheduled",
|
||||||
|
"scheduled_at": "2025-02-01T10:05:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/threads/
|
||||||
|
Crea una serie manualmente.
|
||||||
|
|
||||||
|
**Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Tips de productividad",
|
||||||
|
"platform": "x",
|
||||||
|
"posts": [
|
||||||
|
{"content": "🧵 Hilo: 5 tips para ser más productivo"},
|
||||||
|
{"content": "1/ Usa la técnica Pomodoro..."},
|
||||||
|
{"content": "2/ Elimina distracciones..."},
|
||||||
|
{"content": "3/ Prioriza con Eisenhower..."},
|
||||||
|
{"content": "4/ Automatiza tareas repetitivas..."},
|
||||||
|
{"content": "5/ Revisa tu progreso diario..."}
|
||||||
|
],
|
||||||
|
"description": "Tips de productividad para profesionales",
|
||||||
|
"schedule_type": "sequential",
|
||||||
|
"interval_minutes": 3,
|
||||||
|
"hashtags": ["#Productividad", "#Tips"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validaciones:**
|
||||||
|
- Mínimo 2 posts, máximo 20
|
||||||
|
- Platform requerido
|
||||||
|
|
||||||
|
### POST /api/threads/generate
|
||||||
|
Genera un hilo con IA.
|
||||||
|
|
||||||
|
**Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"topic": "Cómo empezar con inteligencia artificial",
|
||||||
|
"platform": "x",
|
||||||
|
"num_posts": 5,
|
||||||
|
"style": "educational",
|
||||||
|
"name": "Intro a IA" // Opcional: si se incluye, guarda la serie
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Estilos disponibles:**
|
||||||
|
- `educational`: Contenido informativo y didáctico
|
||||||
|
- `storytelling`: Formato narrativo
|
||||||
|
- `tips`: Lista de consejos prácticos
|
||||||
|
|
||||||
|
**Respuesta (sin name):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Thread generated (not saved)",
|
||||||
|
"generated_posts": [
|
||||||
|
{"sequence": 1, "content": "🧵 Te explico qué es la IA..."},
|
||||||
|
{"sequence": 2, "content": "1/ La IA es..."}
|
||||||
|
],
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Respuesta (con name):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Thread generated and saved",
|
||||||
|
"series": {...},
|
||||||
|
"generated_posts": [...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/threads/{series_id}/schedule
|
||||||
|
Programa una serie para publicación.
|
||||||
|
|
||||||
|
**Body (opcional):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"start_time": "2025-02-01T10:00:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Si no se especifica, inicia en 5 minutos.
|
||||||
|
|
||||||
|
**Proceso:**
|
||||||
|
1. Crea posts reales para cada ThreadPost
|
||||||
|
2. Programa cada uno con el intervalo configurado
|
||||||
|
3. Cambia status a "scheduled"
|
||||||
|
|
||||||
|
**Respuesta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Series scheduled successfully",
|
||||||
|
"success": true,
|
||||||
|
"series_id": 1,
|
||||||
|
"start_time": "2025-02-01T10:00:00",
|
||||||
|
"posts_scheduled": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/threads/{series_id}/publish-next
|
||||||
|
Publica manualmente el siguiente post de la serie.
|
||||||
|
|
||||||
|
### POST /api/threads/{series_id}/cancel
|
||||||
|
Cancela una serie y sus posts programados.
|
||||||
|
|
||||||
|
## Tareas Programadas
|
||||||
|
|
||||||
|
### check_thread_schedules
|
||||||
|
- **Frecuencia:** Cada minuto
|
||||||
|
- **Función:**
|
||||||
|
- Busca ThreadPosts programados para ahora
|
||||||
|
- Encola tarea de publicación
|
||||||
|
- Actualiza estado
|
||||||
|
|
||||||
|
### update_thread_post_status
|
||||||
|
- **Trigger:** Después de publicar un post
|
||||||
|
- **Función:**
|
||||||
|
- Actualiza estado del ThreadPost
|
||||||
|
- Guarda platform_post_id (para reply chain)
|
||||||
|
- Actualiza progreso de la serie
|
||||||
|
- Marca serie como completed si todos publicados
|
||||||
|
|
||||||
|
## Tipos de Programación
|
||||||
|
|
||||||
|
| Tipo | Descripción |
|
||||||
|
|------|-------------|
|
||||||
|
| `sequential` | Posts uno tras otro con intervalo fijo |
|
||||||
|
| `timed` | Posts en horarios específicos (próximamente) |
|
||||||
|
|
||||||
|
## Reply Chain
|
||||||
|
|
||||||
|
Para que los posts aparezcan como respuestas:
|
||||||
|
|
||||||
|
1. Se publica el primer post
|
||||||
|
2. Se guarda su `platform_post_id`
|
||||||
|
3. Los siguientes posts se publican como respuesta al anterior
|
||||||
|
4. Cada uno guarda su `reply_to_platform_id`
|
||||||
|
|
||||||
|
```
|
||||||
|
Post 1 (original)
|
||||||
|
└── Post 2 (reply to 1)
|
||||||
|
└── Post 3 (reply to 2)
|
||||||
|
└── Post 4 (reply to 3)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ejemplo de Uso
|
||||||
|
|
||||||
|
```python
|
||||||
|
from app.services.thread_service import thread_service
|
||||||
|
|
||||||
|
# Crear serie manual
|
||||||
|
series = await thread_service.create_series(
|
||||||
|
name="Tips de Python",
|
||||||
|
platform="x",
|
||||||
|
posts_content=[
|
||||||
|
{"content": "🧵 Hilo: 5 tips de Python que debes conocer"},
|
||||||
|
{"content": "1/ List comprehensions..."},
|
||||||
|
{"content": "2/ F-strings..."},
|
||||||
|
{"content": "3/ Context managers..."},
|
||||||
|
{"content": "4/ Generators..."},
|
||||||
|
{"content": "5/ Decorators..."}
|
||||||
|
],
|
||||||
|
interval_minutes=3
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generar con IA
|
||||||
|
result = await thread_service.generate_thread_with_ai(
|
||||||
|
topic="Beneficios de la automatización",
|
||||||
|
platform="threads",
|
||||||
|
num_posts=5,
|
||||||
|
style="storytelling"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Programar
|
||||||
|
await thread_service.schedule_series(
|
||||||
|
series_id=series.id,
|
||||||
|
start_time=datetime(2025, 2, 1, 10, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ver estado
|
||||||
|
series = await thread_service.get_series(series.id)
|
||||||
|
print(f"Publicados: {series['posts_published']}/{series['total_posts']}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flujo Completo
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Crear Serie
|
||||||
|
- Manual: Proporcionar posts
|
||||||
|
- IA: Generar con prompt
|
||||||
|
↓
|
||||||
|
2. Revisar y Editar
|
||||||
|
- Verificar contenido
|
||||||
|
- Ajustar hashtags
|
||||||
|
↓
|
||||||
|
3. Programar
|
||||||
|
- Establecer start_time
|
||||||
|
- Crear posts reales
|
||||||
|
↓
|
||||||
|
4. Publicación Automática
|
||||||
|
- Celery Beat cada minuto
|
||||||
|
- Publica posts programados
|
||||||
|
- Mantiene reply chain
|
||||||
|
↓
|
||||||
|
5. Completado
|
||||||
|
- Todos los posts publicados
|
||||||
|
- Status: completed
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mejores Prácticas
|
||||||
|
|
||||||
|
1. **Primer post atractivo:** El 1/5 o 🧵 debe captar atención
|
||||||
|
2. **Numeración clara:** "1/", "2/" o "1.", "2."
|
||||||
|
3. **Intervalos cortos:** 3-5 minutos mantienen engagement
|
||||||
|
4. **Último post con CTA:** Llamado a la acción al final
|
||||||
|
5. **Hashtags solo al final:** No saturar cada post
|
||||||
|
6. **Máximo 10-15 posts:** Hilos muy largos pierden audiencia
|
||||||
|
|
||||||
|
## Plataformas Soportadas
|
||||||
|
|
||||||
|
| Plataforma | Reply Chain | Notas |
|
||||||
|
|------------|-------------|-------|
|
||||||
|
| X (Twitter) | ✅ | Ideal para threads |
|
||||||
|
| Threads | ✅ | Buen soporte |
|
||||||
|
| Instagram | ❌ | No soporta threads |
|
||||||
|
| Facebook | ❌ | No soporta threads |
|
||||||
Reference in New Issue
Block a user