Files

430 lines
19 KiB
Python
Raw Permalink Normal View History

2025-07-21 22:44:11 +02:00
# ================================================================
# services/notification/app/core/database.py - COMPLETE IMPLEMENTATION
# ================================================================
"""
2025-07-21 22:44:11 +02:00
Database configuration and initialization for notification service
"""
2025-07-21 22:44:11 +02:00
import structlog
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy import text
from shared.database.base import Base, DatabaseManager
from app.core.config import settings
2025-07-21 22:44:11 +02:00
logger = structlog.get_logger()
# Initialize database manager with notification service configuration
database_manager = DatabaseManager(settings.DATABASE_URL)
2025-07-21 22:44:11 +02:00
# Convenience alias for dependency injection
get_db = database_manager.get_db
2025-07-21 22:44:11 +02:00
async def init_db():
"""Initialize database tables and seed data"""
try:
logger.info("Initializing notification service database...")
# Import all models to ensure they're registered with SQLAlchemy
from app.models.notifications import (
Notification, NotificationTemplate, NotificationPreference,
NotificationLog
)
# Import template models (these are separate and optional)
try:
from app.models.templates import EmailTemplate, WhatsAppTemplate
logger.info("Template models imported successfully")
except ImportError:
logger.warning("Template models not found, using basic templates only")
logger.info("Models imported successfully")
# Create all tables
await database_manager.create_tables()
logger.info("Database tables created successfully")
# Seed default templates
await _seed_default_templates()
logger.info("Default templates seeded successfully")
# Test database connection
await _test_database_connection()
logger.info("Database connection test passed")
logger.info("Notification service database initialization completed")
except Exception as e:
logger.error(f"Failed to initialize notification database: {e}")
raise
async def _seed_default_templates():
"""Seed default notification templates"""
try:
async for db in get_db():
# Check if templates already exist
from sqlalchemy import select
from app.models.notifications import NotificationTemplate
result = await db.execute(
select(NotificationTemplate).where(
NotificationTemplate.is_system == True
).limit(1)
)
if result.scalar_one_or_none():
logger.info("Default templates already exist, skipping seeding")
return
# Create default email templates
default_templates = [
{
"template_key": "welcome_email",
"name": "Bienvenida - Email",
"description": "Email de bienvenida para nuevos usuarios",
"category": "transactional",
"type": "email",
"subject_template": "¡Bienvenido a Bakery Forecast, {{user_name}}!",
"body_template": """
¡Hola {{user_name}}!
Bienvenido a Bakery Forecast, la plataforma de pronóstico de demanda para panaderías.
Tu cuenta ha sido creada exitosamente. Ya puedes:
- Subir datos de ventas históricos
- Generar pronósticos de demanda
- Optimizar tu producción diaria
Para comenzar, visita tu dashboard: {{dashboard_url}}
Si tienes alguna pregunta, nuestro equipo está aquí para ayudarte.
¡Éxito en tu panadería!
Saludos,
El equipo de Bakery Forecast
""".strip(),
"html_template": """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bienvenido a Bakery Forecast</title>
</head>
<body style="font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; background-color: #f4f4f4;">
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
<h1 style="color: white; margin: 0; font-size: 28px;">🥖 Bakery Forecast</h1>
<p style="color: #e8e8e8; margin: 10px 0 0 0; font-size: 16px;">Pronósticos inteligentes para tu panadería</p>
</div>
<div style="background: white; padding: 30px; border-radius: 0 0 10px 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
<h2 style="color: #333; margin-top: 0;">¡Hola {{user_name}}!</h2>
<p style="font-size: 16px; margin-bottom: 20px;">
Bienvenido a <strong>Bakery Forecast</strong>, la plataforma de pronóstico de demanda diseñada especialmente para panaderías como la tuya.
</p>
<div style="background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 25px 0;">
<h3 style="color: #495057; margin-top: 0; font-size: 18px;">🎯 Tu cuenta está lista</h3>
<p style="margin-bottom: 15px;">Ya puedes comenzar a:</p>
<ul style="color: #495057; padding-left: 20px;">
<li style="margin-bottom: 8px;"><strong>📊 Subir datos de ventas</strong> - Importa tu historial de ventas</li>
<li style="margin-bottom: 8px;"><strong>🔮 Generar pronósticos</strong> - Obtén predicciones precisas de demanda</li>
<li style="margin-bottom: 8px;"><strong> Optimizar producción</strong> - Reduce desperdicios y maximiza ganancias</li>
</ul>
</div>
<div style="text-align: center; margin: 35px 0;">
<a href="{{dashboard_url}}"
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 30px;
text-decoration: none;
border-radius: 25px;
font-weight: bold;
font-size: 16px;
display: inline-block;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
transition: all 0.3s ease;">
🚀 Ir al Dashboard
</a>
</div>
<div style="background: #e7f3ff; border-left: 4px solid #0066cc; padding: 15px; margin: 25px 0; border-radius: 0 8px 8px 0;">
<p style="margin: 0; color: #004085;">
<strong>💡 Consejo:</strong> Para obtener mejores pronósticos, te recomendamos subir al menos 3 meses de datos históricos de ventas.
</p>
</div>
<p style="font-size: 16px;">
Si tienes alguna pregunta o necesitas ayuda, nuestro equipo está aquí para apoyarte en cada paso.
</p>
<p style="font-size: 16px; margin-bottom: 0;">
¡Éxito en tu panadería! 🥐<br>
<strong>El equipo de Bakery Forecast</strong>
</p>
</div>
<div style="background: #6c757d; color: white; padding: 20px; text-align: center; font-size: 12px; border-radius: 0 0 10px 10px;">
<p style="margin: 0;">© 2025 Bakery Forecast. Todos los derechos reservados.</p>
<p style="margin: 5px 0 0 0;">Madrid, España 🇪🇸</p>
</div>
</body>
</html>
""".strip(),
"language": "es",
"is_system": True,
"is_active": True,
"default_priority": "normal"
},
{
"template_key": "forecast_alert_email",
"name": "Alerta de Pronóstico - Email",
"description": "Alerta por email cuando hay cambios significativos en la demanda",
"category": "alert",
"type": "email",
"subject_template": "🚨 Alerta: Variación significativa en {{product_name}}",
"body_template": """
ALERTA DE PRONÓSTICO - {{bakery_name}}
Se ha detectado una variación significativa en la demanda prevista:
📦 Producto: {{product_name}}
📅 Fecha: {{forecast_date}}
📊 Demanda prevista: {{predicted_demand}} unidades
📈 Variación: {{variation_percentage}}%
{{alert_message}}
Te recomendamos revisar los pronósticos y ajustar la producción según sea necesario.
Ver pronósticos completos: {{dashboard_url}}
Saludos,
El equipo de Bakery Forecast
""".strip(),
"html_template": """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Alerta de Pronóstico</title>
</head>
<body style="font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; background-color: #f4f4f4;">
<div style="background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%); padding: 25px; text-align: center; border-radius: 10px 10px 0 0;">
<h1 style="color: white; margin: 0; font-size: 24px;">🚨 Alerta de Pronóstico</h1>
<p style="color: #ffe8e8; margin: 10px 0 0 0;">{{bakery_name}}</p>
</div>
<div style="background: white; padding: 30px; border-radius: 0 0 10px 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
<div style="background: #fff5f5; border: 2px solid #ff6b6b; padding: 20px; border-radius: 8px; margin-bottom: 25px;">
<h2 style="color: #c53030; margin-top: 0; font-size: 18px;"> Variación Significativa Detectada</h2>
<p style="color: #c53030; margin-bottom: 0;">Se requiere tu atención para ajustar la producción.</p>
</div>
<div style="background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0;">
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; font-weight: bold; color: #495057; width: 40%;">📦 Producto:</td>
<td style="padding: 8px 0; color: #212529;">{{product_name}}</td>
</tr>
<tr>
<td style="padding: 8px 0; font-weight: bold; color: #495057;">📅 Fecha:</td>
<td style="padding: 8px 0; color: #212529;">{{forecast_date}}</td>
</tr>
<tr>
<td style="padding: 8px 0; font-weight: bold; color: #495057;">📊 Demanda prevista:</td>
<td style="padding: 8px 0; color: #212529; font-weight: bold;">{{predicted_demand}} unidades</td>
</tr>
<tr>
<td style="padding: 8px 0; font-weight: bold; color: #495057;">📈 Variación:</td>
<td style="padding: 8px 0; color: #ff6b6b; font-weight: bold; font-size: 18px;">{{variation_percentage}}%</td>
</tr>
</table>
</div>
<div style="background: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 8px; margin: 20px 0;">
<h4 style="margin-top: 0; color: #856404; font-size: 16px;">💡 Recomendación:</h4>
<p style="margin-bottom: 0; color: #856404;">{{alert_message}}</p>
</div>
<div style="text-align: center; margin: 30px 0;">
<a href="{{dashboard_url}}"
style="background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%);
color: white;
padding: 15px 30px;
text-decoration: none;
border-radius: 25px;
font-weight: bold;
font-size: 16px;
display: inline-block;
box-shadow: 0 4px 15px rgba(255, 107, 107, 0.4);">
📊 Ver Pronósticos Completos
</a>
</div>
<p style="font-size: 14px; color: #6c757d; text-align: center; margin-top: 30px;">
<strong>El equipo de Bakery Forecast</strong>
</p>
</div>
</body>
</html>
""".strip(),
"language": "es",
"is_system": True,
"is_active": True,
"default_priority": "high"
},
{
"template_key": "weekly_report_email",
"name": "Reporte Semanal - Email",
"description": "Reporte semanal de rendimiento y estadísticas",
"category": "transactional",
"type": "email",
"subject_template": "📊 Reporte Semanal - {{bakery_name}} ({{week_start}} - {{week_end}})",
"body_template": """
REPORTE SEMANAL - {{bakery_name}}
Período: {{week_start}} - {{week_end}}
RESUMEN DE VENTAS:
- Total de ventas: {{total_sales}} unidades
- Precisión del pronóstico: {{forecast_accuracy}}%
- Productos más vendidos:
{{#top_products}}
{{name}}: {{quantity}} unidades
{{/top_products}}
ANÁLISIS:
{{recommendations}}
Ver reporte completo: {{report_url}}
Saludos,
El equipo de Bakery Forecast
""".strip(),
"html_template": """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reporte Semanal</title>
</head>
<body style="font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; background-color: #f4f4f4;">
<div style="background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%); padding: 25px; text-align: center; border-radius: 10px 10px 0 0;">
<h1 style="color: white; margin: 0; font-size: 24px;">📊 Reporte Semanal</h1>
<p style="color: #ddd; margin: 10px 0 0 0;">{{bakery_name}}</p>
<p style="color: #bbb; margin: 5px 0 0 0; font-size: 14px;">{{week_start}} - {{week_end}}</p>
</div>
<div style="background: white; padding: 30px; border-radius: 0 0 10px 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
<div style="display: flex; gap: 15px; margin: 25px 0; flex-wrap: wrap;">
<div style="background: linear-gradient(135deg, #00b894 0%, #00a085 100%); padding: 20px; border-radius: 10px; flex: 1; text-align: center; min-width: 120px; color: white;">
<h3 style="margin: 0; font-size: 28px;">{{total_sales}}</h3>
<p style="margin: 5px 0 0 0; font-size: 14px; opacity: 0.9;">Ventas Totales</p>
</div>
<div style="background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%); padding: 20px; border-radius: 10px; flex: 1; text-align: center; min-width: 120px; color: white;">
<h3 style="margin: 0; font-size: 28px;">{{forecast_accuracy}}%</h3>
<p style="margin: 5px 0 0 0; font-size: 14px; opacity: 0.9;">Precisión</p>
</div>
</div>
<h3 style="color: #333; margin: 30px 0 15px 0; font-size: 18px;">🏆 Productos más vendidos:</h3>
<div style="background: #f8f9fa; padding: 20px; border-radius: 8px;">
{{#top_products}}
<div style="display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid #e9ecef;">
<span style="font-weight: 500; color: #495057;">{{name}}</span>
<span style="background: #007bff; color: white; padding: 4px 12px; border-radius: 15px; font-size: 12px; font-weight: bold;">{{quantity}} unidades</span>
</div>
{{/top_products}}
</div>
<div style="background: #e7f3ff; border-left: 4px solid #007bff; padding: 20px; margin: 25px 0; border-radius: 0 8px 8px 0;">
<h4 style="margin-top: 0; color: #004085; font-size: 16px;">📈 Análisis y Recomendaciones:</h4>
<p style="margin-bottom: 0; color: #004085; line-height: 1.6;">{{recommendations}}</p>
</div>
<div style="text-align: center; margin: 30px 0;">
<a href="{{report_url}}"
style="background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%);
color: white;
padding: 15px 30px;
text-decoration: none;
border-radius: 25px;
font-weight: bold;
font-size: 16px;
display: inline-block;
box-shadow: 0 4px 15px rgba(116, 185, 255, 0.4);">
📋 Ver Reporte Completo
</a>
</div>
<p style="font-size: 14px; color: #6c757d; text-align: center; margin-top: 30px; border-top: 1px solid #e9ecef; padding-top: 20px;">
<strong>El equipo de Bakery Forecast</strong><br>
<span style="font-size: 12px;">Optimizando panaderías en Madrid desde 2025</span>
</p>
</div>
</body>
</html>
""".strip(),
"language": "es",
"is_system": True,
"is_active": True,
"default_priority": "normal"
}
]
# Create template objects
from app.models.notifications import NotificationTemplate, NotificationType, NotificationPriority
for template_data in default_templates:
template = NotificationTemplate(
template_key=template_data["template_key"],
name=template_data["name"],
description=template_data["description"],
category=template_data["category"],
type=NotificationType(template_data["type"]),
subject_template=template_data["subject_template"],
body_template=template_data["body_template"],
html_template=template_data["html_template"],
language=template_data["language"],
is_system=template_data["is_system"],
is_active=template_data["is_active"],
default_priority=NotificationPriority(template_data["default_priority"])
)
db.add(template)
await db.commit()
logger.info(f"Created {len(default_templates)} default templates")
except Exception as e:
logger.error(f"Failed to seed default templates: {e}")
raise
async def _test_database_connection():
"""Test database connection"""
try:
async for db in get_db():
result = await db.execute(text("SELECT 1"))
if result.scalar() == 1:
logger.info("Database connection test successful")
else:
raise Exception("Database connection test failed")
except Exception as e:
logger.error(f"Database connection test failed: {e}")
raise
# Health check function for the database
async def check_database_health() -> bool:
"""Check if database is healthy"""
try:
await _test_database_connection()
return True
except Exception as e:
logger.error(f"Database health check failed: {e}")
return False