Initial commit - production deployment

This commit is contained in:
2026-01-21 17:17:16 +01:00
commit c23d00dd92
2289 changed files with 638440 additions and 0 deletions

View File

@@ -0,0 +1,104 @@
# ================================================================
# NOTIFICATION SERVICE CONFIGURATION
# services/notification/app/core/config.py
# ================================================================
"""
Notification service configuration
Email and WhatsApp notification handling
"""
from shared.config.base import BaseServiceSettings
import os
class NotificationSettings(BaseServiceSettings):
"""Notification service specific settings"""
# Service Identity
APP_NAME: str = "Notification Service"
SERVICE_NAME: str = "notification-service"
DESCRIPTION: str = "Email and WhatsApp notification service"
# Database configuration (secure approach - build from components)
@property
def DATABASE_URL(self) -> str:
"""Build database URL from secure components"""
# Try complete URL first (for backward compatibility)
complete_url = os.getenv("NOTIFICATION_DATABASE_URL")
if complete_url:
return complete_url
# Build from components (secure approach)
user = os.getenv("NOTIFICATION_DB_USER", "notification_user")
password = os.getenv("NOTIFICATION_DB_PASSWORD", "notification_pass123")
host = os.getenv("NOTIFICATION_DB_HOST", "localhost")
port = os.getenv("NOTIFICATION_DB_PORT", "5432")
name = os.getenv("NOTIFICATION_DB_NAME", "notification_db")
return f"postgresql+asyncpg://{user}:{password}@{host}:{port}/{name}"
# Redis Database (dedicated for notification queue)
REDIS_DB: int = 5
# Email Configuration
SMTP_HOST: str = os.getenv("SMTP_HOST", "smtp.gmail.com")
SMTP_PORT: int = int(os.getenv("SMTP_PORT", "587"))
SMTP_USER: str = os.getenv("SMTP_USER", "")
SMTP_PASSWORD: str = os.getenv("SMTP_PASSWORD", "")
SMTP_TLS: bool = os.getenv("SMTP_TLS", "true").lower() == "true"
SMTP_SSL: bool = os.getenv("SMTP_SSL", "false").lower() == "true"
# Email Settings
DEFAULT_FROM_EMAIL: str = os.getenv("DEFAULT_FROM_EMAIL", "noreply@bakeryforecast.es")
DEFAULT_FROM_NAME: str = os.getenv("DEFAULT_FROM_NAME", "Bakery Forecast")
EMAIL_TEMPLATES_PATH: str = os.getenv("EMAIL_TEMPLATES_PATH", "/app/templates/email")
# WhatsApp Business Cloud API Configuration (Meta/Facebook)
WHATSAPP_ACCESS_TOKEN: str = os.getenv("WHATSAPP_ACCESS_TOKEN", "")
WHATSAPP_PHONE_NUMBER_ID: str = os.getenv("WHATSAPP_PHONE_NUMBER_ID", "")
WHATSAPP_BUSINESS_ACCOUNT_ID: str = os.getenv("WHATSAPP_BUSINESS_ACCOUNT_ID", "")
WHATSAPP_API_VERSION: str = os.getenv("WHATSAPP_API_VERSION", "v18.0")
WHATSAPP_WEBHOOK_VERIFY_TOKEN: str = os.getenv("WHATSAPP_WEBHOOK_VERIFY_TOKEN", "")
WHATSAPP_TEMPLATES_PATH: str = os.getenv("WHATSAPP_TEMPLATES_PATH", "/app/templates/whatsapp")
# Legacy Twilio Configuration (deprecated, for backward compatibility)
WHATSAPP_API_KEY: str = os.getenv("WHATSAPP_API_KEY", "") # Deprecated
WHATSAPP_BASE_URL: str = os.getenv("WHATSAPP_BASE_URL", "https://api.twilio.com") # Deprecated
WHATSAPP_FROM_NUMBER: str = os.getenv("WHATSAPP_FROM_NUMBER", "") # Deprecated
# Notification Queuing
MAX_RETRY_ATTEMPTS: int = int(os.getenv("MAX_RETRY_ATTEMPTS", "3"))
RETRY_DELAY_SECONDS: int = int(os.getenv("RETRY_DELAY_SECONDS", "60"))
BATCH_SIZE: int = int(os.getenv("NOTIFICATION_BATCH_SIZE", "100"))
# Rate Limiting
EMAIL_RATE_LIMIT_PER_HOUR: int = int(os.getenv("EMAIL_RATE_LIMIT_PER_HOUR", "1000"))
WHATSAPP_RATE_LIMIT_PER_HOUR: int = int(os.getenv("WHATSAPP_RATE_LIMIT_PER_HOUR", "100"))
# Spanish Localization
DEFAULT_LANGUAGE: str = os.getenv("DEFAULT_LANGUAGE", "es")
TIMEZONE: str = "Europe/Madrid"
DATE_FORMAT: str = "%d/%m/%Y"
TIME_FORMAT: str = "%H:%M"
# Notification Types
ENABLE_EMAIL_NOTIFICATIONS: bool = os.getenv("ENABLE_EMAIL_NOTIFICATIONS", "true").lower() == "true"
ENABLE_WHATSAPP_NOTIFICATIONS: bool = os.getenv("ENABLE_WHATSAPP_NOTIFICATIONS", "true").lower() == "true"
ENABLE_PUSH_NOTIFICATIONS: bool = os.getenv("ENABLE_PUSH_NOTIFICATIONS", "false").lower() == "true"
# Template Categories
ALERT_TEMPLATES_ENABLED: bool = True
MARKETING_TEMPLATES_ENABLED: bool = os.getenv("MARKETING_TEMPLATES_ENABLED", "false").lower() == "true"
TRANSACTIONAL_TEMPLATES_ENABLED: bool = True
# Delivery Configuration
IMMEDIATE_DELIVERY: bool = os.getenv("IMMEDIATE_DELIVERY", "true").lower() == "true"
SCHEDULED_DELIVERY_ENABLED: bool = os.getenv("SCHEDULED_DELIVERY_ENABLED", "true").lower() == "true"
BULK_DELIVERY_ENABLED: bool = os.getenv("BULK_DELIVERY_ENABLED", "true").lower() == "true"
# Analytics
DELIVERY_TRACKING_ENABLED: bool = os.getenv("DELIVERY_TRACKING_ENABLED", "true").lower() == "true"
OPEN_TRACKING_ENABLED: bool = os.getenv("OPEN_TRACKING_ENABLED", "true").lower() == "true"
CLICK_TRACKING_ENABLED: bool = os.getenv("CLICK_TRACKING_ENABLED", "true").lower() == "true"
settings = NotificationSettings()

View File

@@ -0,0 +1,430 @@
# ================================================================
# services/notification/app/core/database.py - COMPLETE IMPLEMENTATION
# ================================================================
"""
Database configuration and initialization for notification service
"""
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
logger = structlog.get_logger()
# Initialize database manager with notification service configuration
database_manager = DatabaseManager(settings.DATABASE_URL)
# Convenience alias for dependency injection
get_db = database_manager.get_db
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