feat: Add Analytics, Odoo Integration, A/B Testing, and Content features

Phase 1 - Analytics y Reportes:
- PostMetrics and AnalyticsReport models for tracking engagement
- Analytics service with dashboard stats, top posts, optimal times
- 8 API endpoints at /api/analytics/*
- Interactive dashboard with Chart.js charts
- Celery tasks for metrics fetch (15min) and weekly reports

Phase 2 - Integración Odoo:
- Lead and OdooSyncLog models for CRM integration
- Odoo fields added to Product and Service models
- XML-RPC service for bidirectional sync
- Lead management API at /api/leads/*
- Leads dashboard template
- Celery tasks for product/service sync and lead export

Phase 3 - A/B Testing y Recycling:
- ABTest, ABTestVariant, RecycledPost models
- Statistical winner analysis using chi-square test
- Content recycling with engagement-based scoring
- APIs at /api/ab-tests/* and /api/recycling/*
- Automated test evaluation and content recycling tasks

Phase 4 - Thread Series y Templates:
- ThreadSeries and ThreadPost models for multi-post threads
- AI-powered thread generation
- Enhanced ImageTemplate with HTML template support
- APIs at /api/threads/* and /api/templates/*
- Thread scheduling with reply chain support

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-28 03:10:42 +00:00
parent 03b5f9f2e2
commit ecc2ca73ea
31 changed files with 6067 additions and 6 deletions

View File

@@ -0,0 +1,116 @@
"""
Analytics Report Model - Aggregated analytics snapshots.
"""
from datetime import datetime, date
from sqlalchemy import Column, Integer, Float, String, DateTime, Date, JSON, Text
from app.core.database import Base
class AnalyticsReport(Base):
"""
Stores aggregated analytics reports (daily, weekly, monthly).
"""
__tablename__ = "analytics_reports"
id = Column(Integer, primary_key=True, index=True)
# Report type and period
report_type = Column(String(20), nullable=False, index=True) # daily, weekly, monthly
period_start = Column(Date, nullable=False, index=True)
period_end = Column(Date, nullable=False)
platform = Column(String(50), nullable=True) # null = all platforms
# Aggregated metrics
total_posts = Column(Integer, default=0)
total_impressions = Column(Integer, default=0)
total_reach = Column(Integer, default=0)
total_engagements = Column(Integer, default=0)
total_likes = Column(Integer, default=0)
total_comments = Column(Integer, default=0)
total_shares = Column(Integer, default=0)
# Calculated averages
avg_engagement_rate = Column(Float, default=0.0)
avg_impressions_per_post = Column(Float, default=0.0)
avg_engagements_per_post = Column(Float, default=0.0)
# Comparison with previous period
posts_change_pct = Column(Float, nullable=True)
engagement_change_pct = Column(Float, nullable=True)
impressions_change_pct = Column(Float, nullable=True)
# Top performing data (JSON)
top_posts = Column(JSON, nullable=True)
# [{"post_id": 1, "content": "...", "engagement_rate": 5.2, "platform": "x"}]
best_times = Column(JSON, nullable=True)
# [{"day": 1, "hour": 12, "avg_engagement": 4.5}]
content_performance = Column(JSON, nullable=True)
# {"tip": {"posts": 10, "avg_engagement": 3.2}, "product": {...}}
platform_breakdown = Column(JSON, nullable=True)
# {"x": {"posts": 20, "engagement": 150}, "threads": {...}}
# Report content for Telegram
summary_text = Column(Text, nullable=True)
# Metadata
generated_at = Column(DateTime, default=datetime.utcnow)
sent_to_telegram = Column(DateTime, nullable=True)
def to_dict(self):
return {
"id": self.id,
"report_type": self.report_type,
"period_start": self.period_start.isoformat() if self.period_start else None,
"period_end": self.period_end.isoformat() if self.period_end else None,
"platform": self.platform,
"total_posts": self.total_posts,
"total_impressions": self.total_impressions,
"total_reach": self.total_reach,
"total_engagements": self.total_engagements,
"avg_engagement_rate": round(self.avg_engagement_rate, 2),
"avg_impressions_per_post": round(self.avg_impressions_per_post, 1),
"posts_change_pct": round(self.posts_change_pct, 1) if self.posts_change_pct else None,
"engagement_change_pct": round(self.engagement_change_pct, 1) if self.engagement_change_pct else None,
"top_posts": self.top_posts,
"best_times": self.best_times,
"content_performance": self.content_performance,
"platform_breakdown": self.platform_breakdown,
"generated_at": self.generated_at.isoformat() if self.generated_at else None
}
def generate_telegram_summary(self) -> str:
"""Generate formatted summary for Telegram."""
lines = [
f"📊 *Reporte {self.report_type.title()}*",
f"📅 {self.period_start} - {self.period_end}",
"",
f"📝 Posts publicados: *{self.total_posts}*",
f"👁 Impresiones: *{self.total_impressions:,}*",
f"💬 Interacciones: *{self.total_engagements:,}*",
f"📈 Engagement rate: *{self.avg_engagement_rate:.2f}%*",
]
if self.engagement_change_pct is not None:
emoji = "📈" if self.engagement_change_pct > 0 else "📉"
lines.append(f"{emoji} vs anterior: *{self.engagement_change_pct:+.1f}%*")
if self.platform_breakdown:
lines.append("")
lines.append("*Por plataforma:*")
for platform, data in self.platform_breakdown.items():
lines.append(f"{platform}: {data.get('posts', 0)} posts, {data.get('engagements', 0)} interacciones")
if self.top_posts and len(self.top_posts) > 0:
lines.append("")
lines.append("*Top 3 posts:*")
for i, post in enumerate(self.top_posts[:3], 1):
content = post.get('content', '')[:50] + "..." if len(post.get('content', '')) > 50 else post.get('content', '')
lines.append(f" {i}. {content} ({post.get('engagement_rate', 0):.1f}%)")
self.summary_text = "\n".join(lines)
return self.summary_text