2025-07-21 22:44:11 +02:00
|
|
|
|
# ================================================================
|
|
|
|
|
|
# services/notification/app/core/database.py - COMPLETE IMPLEMENTATION
|
|
|
|
|
|
# ================================================================
|
2025-07-17 13:09:24 +02:00
|
|
|
|
"""
|
2025-07-21 22:44:11 +02:00
|
|
|
|
Database configuration and initialization for notification service
|
2025-07-17 13:09:24 +02:00
|
|
|
|
"""
|
|
|
|
|
|
|
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
|
2025-07-17 13:09:24 +02:00
|
|
|
|
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
|
2025-07-17 13:09:24 +02:00
|
|
|
|
database_manager = DatabaseManager(settings.DATABASE_URL)
|
|
|
|
|
|
|
2025-07-21 22:44:11 +02:00
|
|
|
|
# Convenience alias for dependency injection
|
2025-07-17 13:09:24 +02:00
|
|
|
|
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
|