latapp 9 месяцев назад
Родитель
Сommit
13552f0894

+ 205 - 0
LOGGING_IMPROVEMENTS.md

@@ -0,0 +1,205 @@
+# Mejoras en el Sistema de Logging - Pedidos Express
+
+## Resumen de Mejoras Implementadas
+
+Este documento describe las mejoras implementadas en el sistema de logging del proyecto Pedidos Express.
+
+## 1. Nuevo Sistema de Logging Estructurado
+
+### Archivo: `services/logging_service.py`
+
+Se implementó un sistema de logging estructurado con las siguientes características:
+
+- **Logging por categorías**: ORDER, USER, PAYMENT, SECURITY, API, DATABASE, EMAIL, PRINT, CHAT, SYSTEM
+- **Niveles de log**: DEBUG, INFO, WARNING, ERROR, CRITICAL
+- **Logs estructurados en JSON**: Fáciles de procesar y analizar
+- **Archivos de log específicos**: Un archivo por categoría para mejor organización
+- **Metadatos enriquecidos**: Incluye user_id, user_email, timestamp, datos contextuales
+
+### Funciones principales:
+- `log_order_event()`: Para eventos relacionados con pedidos
+- `log_user_event()`: Para eventos de usuarios
+- `log_security_event()`: Para eventos de seguridad
+- `log_api_event()`: Para eventos de API
+- `log_database_event()`: Para eventos de base de datos
+- `log_email_event()`: Para eventos de email
+- `log_print_event()`: Para eventos de impresión
+- `log_chat_event()`: Para eventos de chat/LLM
+- `log_system_event()`: Para eventos del sistema
+
+## 2. Mejoras por Archivo
+
+### `services/print_service.py`
+- ✅ Logging detallado de operaciones de impresión
+- ✅ Manejo robusto de errores con contexto
+- ✅ Logging de status de impresora
+- ✅ Timeouts en requests para evitar bloqueos
+- ✅ Logging estructurado para troubleshooting
+
+### `auth/security.py`
+- ✅ Logging de eventos de autenticación
+- ✅ Logging de generación de tokens
+- ✅ Logging de validación de tokens
+- ✅ Logging de errores de JWT
+- ✅ Logging de intentos de acceso no autorizado
+
+### `services/email_service.py`
+- ✅ Logging de conexiones SMTP
+- ✅ Logging de autenticación SMTP
+- ✅ Logging de envío de emails
+- ✅ Logging de reintentos automáticos
+- ✅ Logging de errores de conexión
+
+### `routes/orders.py`
+- ✅ Logging completo del flujo de pedidos
+- ✅ Logging de validaciones de datos
+- ✅ Logging de integración con Fudo
+- ✅ Logging de creación de ventas
+- ✅ Logging de actualización de recompensas
+- ✅ Logging de impresión de pedidos
+- ✅ Manejo robusto de errores con contexto
+
+### `routes/users.py`
+- ✅ Logging de registro de usuarios
+- ✅ Logging de intentos de login
+- ✅ Logging de bloqueos por intentos fallidos
+- ✅ Logging de validaciones RUT
+- ✅ Logging de verificaciones por email
+- ✅ Logging de eventos de seguridad
+
+### `routes/chat.py`
+- ✅ Logging de requests de chat
+- ✅ Logging de respuestas de OpenAI
+- ✅ Logging de errores de procesamiento
+- ✅ Logging de métricas de uso
+
+### `main.py`
+- ✅ Logging de inicio de aplicación
+- ✅ Logging de validación de configuración
+- ✅ Logging de inicialización de componentes
+- ✅ Logging de errores críticos de startup
+
+### `app.py`
+- ✅ Logging de creación de app FastAPI
+- ✅ Logging de configuración de middleware
+- ✅ Logging de setup de rutas
+- ✅ Logging de montaje de archivos estáticos
+
+## 3. Beneficios del Nuevo Sistema
+
+### Troubleshooting Mejorado
+- **Logs estructurados** facilitan el análisis automático
+- **Contexto enriquecido** con user_id, emails, datos de request
+- **Categorización** permite filtrar por tipo de evento
+- **Timestamps precisos** para correlación temporal
+
+### Seguridad
+- **Logging de eventos de seguridad** (login failures, admin access attempts)
+- **Tracking de intentos de acceso** no autorizado
+- **Logging de generación y validación de tokens**
+
+### Monitoreo Operacional
+- **Status de servicios externos** (printer, SMTP)
+- **Métricas de rendimiento** y tiempo de respuesta
+- **Tracking de errores** con stack traces contextuales
+
+### Cumplimiento y Auditoría
+- **Trazabilidad completa** de acciones de usuario
+- **Logs de transacciones** de pedidos y pagos
+- **Registro de accesos administrativos**
+
+## 4. Estructura de Archivos de Log
+
+```
+logs/
+├── app.log              # Log principal de la aplicación
+├── order.log            # Eventos de pedidos
+├── user.log             # Eventos de usuarios
+├── security.log         # Eventos de seguridad
+├── api.log              # Eventos de API
+├── database.log         # Eventos de base de datos
+├── email.log            # Eventos de email
+├── print.log            # Eventos de impresión
+├── chat.log             # Eventos de chat/LLM
+├── system.log           # Eventos del sistema
+├── orders.csv           # Log legacy de pedidos (CSV)
+└── llm_responses.txt    # Log legacy de respuestas LLM
+```
+
+## 5. Ejemplo de Log Estructurado
+
+```json
+{
+  "timestamp": "2025-08-07T10:30:15.123456",
+  "category": "ORDER",
+  "level": "INFO",
+  "message": "Order completed successfully for table 5",
+  "user_id": 123,
+  "user_email": "user@example.com",
+  "data": {
+    "table": 5,
+    "customer_id": 123,
+    "total_amount": 25000,
+    "items": [
+      {"name": "Cerveza", "quantity": 2, "price": 5000},
+      {"name": "Hamburguesa", "quantity": 1, "price": 15000}
+    ],
+    "sale_id": 456,
+    "new_reward_progress": 85,
+    "beers_for_promo": 2
+  }
+}
+```
+
+## 6. Compatibilidad con Sistema Anterior
+
+- ✅ **Funciones legacy mantenidas** para evitar breaking changes
+- ✅ **Archivos CSV y TXT** se siguen generando para compatibilidad
+- ✅ **Migración gradual** sin afectar funcionalidad existente
+
+## 7. Recomendaciones de Uso
+
+### Para Desarrollo
+- Utilizar nivel `DEBUG` para troubleshooting detallado
+- Revisar `logs/security.log` para eventos de autenticación
+- Monitorear `logs/print.log` para problemas de impresión
+
+### Para Producción
+- Configurar nivel `INFO` o `WARNING` para logs principales
+- Implementar rotación de logs para gestión de espacio
+- Configurar alertas para eventos `ERROR` y `CRITICAL`
+- Monitorear `logs/system.log` para salud de la aplicación
+
+### Para Análisis
+- Procesar logs JSON con herramientas como `jq`, ELK stack, o Datadog
+- Correlacionar eventos por `user_id` para tracking de usuario
+- Analizar métricas de performance y errores
+
+## 8. Próximos Pasos Recomendados
+
+1. **Implementar rotación de logs** con `logrotate` o similar
+2. **Configurar alertas** para eventos críticos
+3. **Integrar con sistema de monitoreo** (Grafana, Datadog, etc.)
+4. **Implementar dashboards** para métricas operacionales
+5. **Configurar backup** de logs importantes
+6. **Documentar alertas** y procedimientos de respuesta
+
+---
+
+## Comandos Útiles para Análisis de Logs
+
+```bash
+# Ver logs de errores en tiempo real
+tail -f logs/app.log | grep ERROR
+
+# Analizar logs de seguridad
+jq '. | select(.category == "SECURITY")' logs/security.log
+
+# Contar pedidos por usuario
+jq '. | select(.category == "ORDER") | .user_email' logs/order.log | sort | uniq -c
+
+# Ver errores de los últimos 30 minutos
+jq '. | select(.level == "ERROR" and (.timestamp | fromdateiso8601) > (now - 1800))' logs/app.log
+```
+
+Esta implementación proporciona una base sólida para el monitoreo, troubleshooting y análisis operacional del sistema Pedidos Express.

+ 174 - 49
app.py

@@ -1,69 +1,194 @@
 from fastapi import FastAPI
 from fastapi.middleware.cors import CORSMiddleware
 from starlette.middleware.sessions import SessionMiddleware
-from config.settings import SECRET_KEY
+from config.settings import DEVELOPMENT, SECRET_KEY
 from routes import sales
 from services.email_service import initialize_email_sender
+from services.logging_service import structured_logger, LogLevel
+from logging import getLogger
+
+logger = getLogger(__name__)
 
 def create_app() -> FastAPI:
     """Create and configure FastAPI application"""
-    app = FastAPI(title="Web Pedidos Klein - FastAPI Backend",
-                  description="Backend for the Web Pedidos Klein application using FastAPI",)
+    logger.info("Creating FastAPI application")
+    
+    try:
+        if DEVELOPMENT:
+            logger.info("Creating FastAPI app in development mode")
+            app = FastAPI(
+                title="Web Pedidos Klein - FastAPI Backend",
+                description="Backend for the Web Pedidos Klein application using FastAPI"
+            )
+        else:
+            logger.info("Creating FastAPI app in production mode")
+            app = FastAPI(
+                title="Web Pedidos Klein - FastAPI Backend",
+                description="Backend for the Web Pedidos Klein application using FastAPI",
+                version="1.0.0", 
+                docs_url=None, 
+                redoc_url=None
+            )
 
-    # Initialize email sender
-    initialize_email_sender()
+        structured_logger.log_system_event(
+            "FastAPI application created",
+            LogLevel.INFO,
+            {
+                "development_mode": DEVELOPMENT,
+                "docs_enabled": DEVELOPMENT,
+                "title": "Web Pedidos Klein - FastAPI Backend"
+            }
+        )
 
-    # Add CORS middleware
-    app.add_middleware(
-        CORSMiddleware,
-        allow_origins=["https://admin.kleinexpress.store"],  # Permite solo este origen
-        allow_credentials=True,
-        allow_methods=["*"],
-        allow_headers=["*"],
-    )
-    
-    # Add SessionMiddleware
-    app.add_middleware(
-        SessionMiddleware,
-        secret_key=SECRET_KEY,
-        max_age=60 * 60  # max_age in seconds for Starlette
-    )
-    
-    return app
+        # Initialize email sender
+        logger.info("Initializing email sender")
+        initialize_email_sender()
+        
+        structured_logger.log_email_event(
+            "Email sender initialized",
+            LogLevel.INFO
+        )
+
+        # Add CORS middleware
+        logger.info("Adding CORS middleware")
+        app.add_middleware(
+            CORSMiddleware,
+            allow_origins=["https://admin.kleinexpress.store"],
+            allow_credentials=True,
+            allow_methods=["*"],
+            allow_headers=["*"],
+        )
+        
+        structured_logger.log_system_event(
+            "CORS middleware configured",
+            LogLevel.INFO,
+            {
+                "allowed_origins": ["https://admin.kleinexpress.store"],
+                "allow_credentials": True
+            }
+        )
+        
+        # Add SessionMiddleware
+        logger.info("Adding session middleware")
+        app.add_middleware(
+            SessionMiddleware,
+            secret_key=SECRET_KEY,
+            max_age=60 * 60  # max_age in seconds for Starlette
+        )
+        
+        structured_logger.log_system_event(
+            "Session middleware configured",
+            LogLevel.INFO,
+            {"max_age_seconds": 3600}
+        )
+        
+        logger.info("FastAPI application created successfully")
+        return app
+        
+    except Exception as e:
+        error_msg = f"Error creating FastAPI application: {e}"
+        logger.error(error_msg)
+        structured_logger.log_system_event(
+            "FastAPI application creation failed",
+            LogLevel.ERROR,
+            {
+                "error": str(e),
+                "error_type": type(e).__name__
+            }
+        )
+        raise
 
 
 def setup_routes(app: FastAPI):
     """Setup all application routes"""
-    from routes import chat, users, products, orders, static
-    from fastapi import Depends
-    from auth.security import get_current_user
+    logger.info("Setting up application routes")
     
-    # Chat routes
-    app.include_router(chat.chat_router, prefix="/api/chat",tags=["Chat"], dependencies=[Depends(get_current_user)])
+    try:
+        from routes import chat, users, products, orders, static
+        from fastapi import Depends
+        from auth.security import get_current_user
+        
+        # Chat routes
+        logger.info("Setting up chat routes")
+        app.include_router(
+            chat.chat_router, 
+            prefix="/api/chat",
+            tags=["Chat"], 
+            dependencies=[Depends(get_current_user)]
+        )
 
-    # User routes
-    app.include_router(users.user_router, prefix="/api/users", tags=["Users"])
+        # User routes
+        logger.info("Setting up user routes")
+        app.include_router(users.user_router, prefix="/api/users", tags=["Users"])
 
-    # Product routes
-    app.include_router(products.product_router, prefix="/api/products", tags=["Products"],dependencies=[Depends(get_current_user)])
+        # Product routes
+        logger.info("Setting up product routes")
+        app.include_router(
+            products.product_router, 
+            prefix="/api/products", 
+            tags=["Products"],
+            dependencies=[Depends(get_current_user)]
+        )
 
-    # Order routes
-    app.include_router(orders.order_router, prefix="/api/orders", tags=["Orders"], dependencies=[Depends(get_current_user)])
-    
-    # Sales routes
-    app.include_router(sales.sales_router, prefix="/api/sales", tags=["Sales"], dependencies=[Depends(get_current_user)])
+        # Order routes
+        logger.info("Setting up order routes")
+        app.include_router(
+            orders.order_router, 
+            prefix="/api/orders", 
+            tags=["Orders"], 
+            dependencies=[Depends(get_current_user)]
+        )
+        
+        # Sales routes
+        logger.info("Setting up sales routes")
+        app.include_router(
+            sales.sales_router, 
+            prefix="/api/sales", 
+            tags=["Sales"], 
+            dependencies=[Depends(get_current_user)]
+        )
+
+        # Verification routes
+        logger.info("Setting up verification routes")
+        app.include_router(users.verify_router, prefix="/verify", tags=["Verification"])
+        
+        # Static routes
+        logger.info("Setting up static routes")
+        from fastapi.responses import HTMLResponse
+        app.add_api_route("/", static.serve_app_html, methods=["GET"], 
+                         response_class=HTMLResponse, include_in_schema=False)
+        app.add_api_route("/register", static.serve_register_html, methods=["GET"],
+                         response_class=HTMLResponse, include_in_schema=False)
+        app.add_api_route("/images/{image_path:path}", static.serve_image, methods=["GET"],
+                         response_class=HTMLResponse, include_in_schema=False)
 
-    # Verification routes
-    app.include_router(users.verify_router, prefix="/verify", tags=["Verification"])
-    # Static routes
-    from fastapi.responses import HTMLResponse
-    app.add_api_route("/", static.serve_app_html, methods=["GET"], 
-                     response_class=HTMLResponse, include_in_schema=False)
-    app.add_api_route("/register", static.serve_register_html, methods=["GET"],
-                     response_class=HTMLResponse, include_in_schema=False)
-    app.add_api_route("/images/{image_path:path}", static.serve_image, methods=["GET"],
-                     response_class=HTMLResponse, include_in_schema=False)
+        # Mount static files
+        logger.info("Mounting static file directories")
+        static.mount_main_static_files(app)
+        static.mount_register_static_files(app)
 
-    # Mount static files
-    static.mount_main_static_files(app)
-    static.mount_register_static_files(app)
+        structured_logger.log_system_event(
+            "All application routes configured successfully",
+            LogLevel.INFO,
+            {
+                "route_groups": [
+                    "chat", "users", "products", "orders", 
+                    "sales", "verification", "static"
+                ]
+            }
+        )
+        
+        logger.info("Application routes setup completed successfully")
+        
+    except Exception as e:
+        error_msg = f"Error setting up application routes: {e}"
+        logger.error(error_msg)
+        structured_logger.log_system_event(
+            "Route setup failed",
+            LogLevel.ERROR,
+            {
+                "error": str(e),
+                "error_type": type(e).__name__
+            }
+        )
+        raise

+ 155 - 18
auth/security.py

@@ -1,6 +1,5 @@
 from datetime import datetime, timedelta
 from typing import Union
-from venv import logger
 from fastapi import Depends, HTTPException
 from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
 from logging import getLogger
@@ -12,7 +11,7 @@ from passlib.context import CryptContext
 
 from models.user import User
 from services.data_service import UserDataService
-
+from services.logging_service import structured_logger, LogLevel
 
 logger = getLogger(__name__)
 
@@ -30,13 +29,43 @@ def hash_password(password: str) -> str:
     """Hash a password using bcrypt."""
     return pwd_context.hash(password)
 
-def generate_token(email: str):
+def generate_token(email: str): 
     """Generate a JWT token for user authentication."""
-    data = {"sub": email}
-    expires_delta = timedelta(days=ACCESS_TOKEN_EXPIRE_DAYS)
-    token = create_access_token(data=data, expires_delta=expires_delta)
-    logger.debug(f"Generated token for email {email}: {token}")
-    return token
+    logger.info(f"Generating token for user: {email}")
+    
+    try:
+        data = {"sub": email}
+        expires_delta = timedelta(days=ACCESS_TOKEN_EXPIRE_DAYS)
+        token = create_access_token(data=data, expires_delta=expires_delta)
+        
+        structured_logger.log_security_event(
+            f"JWT token generated for user {email}",
+            LogLevel.INFO,
+            {
+                "email": email,
+                "token_length": len(token),
+                "expires_in_days": ACCESS_TOKEN_EXPIRE_DAYS
+            },
+            user_email=email
+        )
+        
+        logger.debug(f"Generated token for email {email}: {token[:20]}...")
+        return token
+        
+    except Exception as e:
+        error_msg = f"Failed to generate token for {email}: {e}"
+        logger.error(error_msg)
+        structured_logger.log_security_event(
+            f"Token generation failed for user {email}",
+            LogLevel.ERROR,
+            {
+                "email": email,
+                "error": str(e),
+                "error_type": type(e).__name__
+            },
+            user_email=email
+        )
+        raise
 
 def create_access_token(data: dict, expires_delta: timedelta) -> str:
     """Create a JWT access token."""
@@ -53,10 +82,52 @@ def create_access_token(data: dict, expires_delta: timedelta) -> str:
 
 def authenticate_user(email: str, password: str) -> bool:
     """Authenticate a user by email and password."""
-    user = user_data_service.login(email, password)
-    if not user:
+    logger.info(f"Authentication attempt for user: {email}")
+    
+    try:
+        user = user_data_service.login(email, password)
+        
+        if user:
+            structured_logger.log_security_event(
+                f"Successful authentication for user {email}",
+                LogLevel.INFO,
+                {
+                    "email": email,
+                    "user_id": user.id,
+                    "user_name": user.name
+                },
+                user_id=user.id,
+                user_email=email
+            )
+            logger.info(f"Authentication successful for user: {email}")
+            return True
+        else:
+            structured_logger.log_security_event(
+                f"Failed authentication attempt for user {email}",
+                LogLevel.WARNING,
+                {
+                    "email": email,
+                    "reason": "Invalid credentials"
+                },
+                user_email=email
+            )
+            logger.warning(f"Authentication failed for user: {email}")
+            return False
+            
+    except Exception as e:
+        error_msg = f"Authentication error for {email}: {e}"
+        logger.error(error_msg)
+        structured_logger.log_security_event(
+            f"Authentication error for user {email}",
+            LogLevel.ERROR,
+            {
+                "email": email,
+                "error": str(e),
+                "error_type": type(e).__name__
+            },
+            user_email=email
+        )
         return False
-    return True
 
 async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> User:
     credentials_exception = HTTPException(
@@ -67,20 +138,86 @@ async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(s
     
     try:
         token = credentials.credentials
-        logger.debug(f"Decoding token: {token}")
+        logger.debug(f"Decoding token: {token[:20]}...")
+        
         payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
         email = payload.get("sub")
+        
         if email is None:
             logger.error("Token does not contain email")
+            structured_logger.log_security_event(
+                "Token validation failed: missing email",
+                LogLevel.WARNING,
+                {"reason": "Token missing email subject"}
+            )
             raise credentials_exception
+            
         token_data = TokenData(email=email)
-    except JWTError:
-        logger.error("JWTError: Invalid token")
+        
+    except JWTError as e:
+        logger.error(f"JWTError: Invalid token - {e}")
+        structured_logger.log_security_event(
+            "Token validation failed: JWT error",
+            LogLevel.WARNING,
+            {
+                "error": str(e),
+                "error_type": "JWTError"
+            }
+        )
+        raise credentials_exception
+    except Exception as e:
+        logger.error(f"Unexpected error during token validation: {e}")
+        structured_logger.log_security_event(
+            "Token validation failed: unexpected error",
+            LogLevel.ERROR,
+            {
+                "error": str(e),
+                "error_type": type(e).__name__
+            }
+        )
         raise credentials_exception
     
-    user = user_data_service.get_by_email(token_data.email)
-    if user is None:
-        logger.error(f"User not found: {token_data.email}")
+    try:
+        user = user_data_service.get_by_email(token_data.email)
+        
+        if user is None:
+            logger.error(f"User not found: {token_data.email}")
+            structured_logger.log_security_event(
+                f"Token validation failed: user not found",
+                LogLevel.WARNING,
+                {
+                    "email": token_data.email,
+                    "reason": "User not found in database"
+                },
+                user_email=token_data.email
+            )
+            raise credentials_exception
+            
+        logger.debug(f"User authenticated successfully: {user.email}")
+        structured_logger.log_security_event(
+            f"Token validation successful for user {user.email}",
+            LogLevel.DEBUG,
+            {
+                "user_id": user.id,
+                "email": user.email,
+                "permissions": user.permissions
+            },
+            user_id=user.id,
+            user_email=user.email
+        )
+        
+        return user
+        
+    except Exception as e:
+        logger.error(f"Database error during user lookup: {e}")
+        structured_logger.log_security_event(
+            "Token validation failed: database error",
+            LogLevel.ERROR,
+            {
+                "email": token_data.email if 'token_data' in locals() else "unknown",
+                "error": str(e),
+                "error_type": type(e).__name__
+            }
+        )
         raise credentials_exception
-    return user
 

+ 2 - 1
config/messages.py

@@ -27,7 +27,7 @@ class SuccessResponse:
     PRODUCT_EDIT_SUCCESS = "Producto editado exitosamente."
     PRODUCT_CREATE_SUCCESS = "Producto creado exitosamente."
     PRODUCT_DELETE_SUCCESS = "Producto eliminado exitosamente."
-
+    REWARD_SUCCESS = "Recompensa aplicada exitosamente."
 class UserResponse:
     """Class to handle user-related messages in the response."""
 
@@ -37,3 +37,4 @@ class UserResponse:
     USER_ALREADY_EXISTS = "El usuario ya está registrado."
     USER_FORMAT_BLOCKED = "Demasiados intentos de inicio de sesión. Usuario bloqueado por {time}."
     NOT_PERMITTED = "No tienes permisos para realizar esta acción."
+    REWARD_INSUFFICIENT_PROGRESS = "Progreso insuficiente para reclamar la recompensa. Progreso actual: {progress}."

+ 22 - 11
config/settings.py

@@ -9,7 +9,7 @@ load_dotenv()
 
 
 APPNAME = "Pedidos Express"
-MAIL = os.getenv("MAIL","")
+MAIL = os.getenv("MAIL_DIRECTION","")
 MAIL_PASSWORD = os.getenv("MAIL_PASSWORD","")
 LOGS_FOLDER = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'logs')
 if not os.path.exists(LOGS_FOLDER):
@@ -43,24 +43,35 @@ SECRET_KEY = os.getenv("SECRET_KEY", "your_very_very_secret_key_for_signing_cook
 # Data paths
 BG_DATA_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)),'data', 'llm_data.json')
 PRODUCT_DATA_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)),'data', 'products.json')
-DB_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)),'data', 'data.db')
+
+
+#postgresql connection settings
+POSTGRESQL_DB_CONFIG = {
+    'dbname': os.getenv('POSTGRES_DB', 'pedidos_express_pruebas'),
+    'user': os.getenv('POSTGRES_USER', 'superti'),
+    'password': os.getenv('POSTGRES_PASSWORD', 'BTD2DALN55N'),
+    'host': os.getenv('POSTGRES_HOST', 'localhost'),
+    'port': os.getenv('POSTGRES_PORT', '5432')
+}
 
 def validate_config():
     logger = logging.getLogger(__name__)
     """Validate configuration and show warnings"""
     if SECRET_KEY == "your_very_very_secret_key_for_signing_cookies_python_v2":
         logger.warning("Using default SECRET_KEY. Please set a strong SECRET_KEY in your .env file for production.")
+    
+    if POSTGRESQL_DB_CONFIG['dbname'] == 'pedidos_express_pruebas':
+        logger.warning("Using default database name 'pedidos_express_pruebas'. Please set a valid database name in your .env file for production.")
+
+    if not POSTGRESQL_DB_CONFIG['user'] or not POSTGRESQL_DB_CONFIG['password']:
+        logger.critical("CRITICAL ERROR: POSTGRES_USER and POSTGRES_PASSWORD environment variables not set. The application will not work correctly.")
+        return False
+
+    if not MAIL or not MAIL_PASSWORD:
+        logger.critical("CRITICAL ERROR: MAIL and MAIL_PASSWORD environment variables not set. The application will not work correctly.")
+        return False
 
     if not OPENAI_API_KEY:
         logger.critical("CRITICAL ERROR: OPENAI_API_KEY environment variable not set. The application will not work correctly.")
         return False
     return True
-
-#postgresql connection settings
-POSTGRESQL_DB_CONFIG = {
-    'dbname': os.getenv('POSTGRES_DB', 'pedidos_express'),
-    'user': os.getenv('POSTGRES_USER', 'superti'),
-    'password': os.getenv('POSTGRES_PASSWORD', 'BTD2DALN55N'),
-    'host': os.getenv('POSTGRES_HOST', 'localhost'),
-    'port': os.getenv('POSTGRES_PORT', '5432')
-}

+ 113 - 23
main.py

@@ -1,9 +1,10 @@
 import os
 import uvicorn
 from app import create_app, setup_routes
-from config.settings import PORT, OPENAI_API_KEY, BG_DATA_PATH, validate_config
+from config.settings import PORT, OPENAI_API_KEY, BG_DATA_PATH,DEVELOPMENT, validate_config
 from logging import getLogger
 from threading import Thread
+from services.logging_service import structured_logger, LogLevel
 
 from services.data_service import initialize_db
 
@@ -11,38 +12,127 @@ logger = getLogger("main")
 
 def main():
     """Main application entry point"""
-    # Validate configuration
-    if not validate_config():
-        logger.critical("FATAL: Configuration validation failed.")
-        if not OPENAI_API_KEY:
-            logger.error("Please create a .env file with OPENAI_API_KEY='your_key_here'")
-            with open(".env", "w") as f:
-                f.write("OPENAI_API_KEY='your_key_here'")
-        return
+    logger.info("Starting Pedidos Express application")
     
-    # Create and configure app
-    app = create_app()
-    setup_routes(app)
-    initialize_db()
+    structured_logger.log_system_event(
+        "Application startup initiated",
+        LogLevel.INFO,
+        {
+            "port": PORT,
+            "development_mode": DEVELOPMENT,
+            "bg_data_path": BG_DATA_PATH
+        }
+    )
     
-    # Display startup information
-    logger.info(f"Servidor corriendo en http://localhost:{PORT}")
-    if not os.path.exists(BG_DATA_PATH):
-        logger.warning(f"ADVERTENCIA: {BG_DATA_PATH} no encontrado. El asistente de IA no tendrá datos específicos del menú.")
-    else:
-        logger.info(f"Datos del asistente cargados desde: {os.path.abspath(BG_DATA_PATH)}")
+    try:
+        # Validate configuration
+        if not validate_config():
+            logger.critical("FATAL: Configuration validation failed.")
+            structured_logger.log_system_event(
+                "Application startup failed: configuration validation error",
+                LogLevel.CRITICAL,
+                {"validation_step": "configuration"}
+            )
+            if not OPENAI_API_KEY:
+                logger.error("Please create a .env file with OPENAI_API_KEY='your_key_here'")
+                with open(".env", "w") as f:
+                    f.write("OPENAI_API_KEY='your_key_here'")
+            return None
+        
+        structured_logger.log_system_event(
+            "Configuration validation successful",
+            LogLevel.INFO
+        )
+        
+        # Create and configure app
+        logger.info("Creating FastAPI application")
+        app = create_app()
+        
+        logger.info("Setting up application routes")
+        setup_routes(app)
+        
+        structured_logger.log_system_event(
+            "FastAPI application created and routes configured",
+            LogLevel.INFO
+        )
+        
+        # Initialize database
+        logger.info("Initializing database")
+        initialize_db()
+        
+        structured_logger.log_system_event(
+            "Database initialization completed",
+            LogLevel.INFO
+        )
+        
+        # Display startup information
+        logger.info(f"Server starting on http://localhost:{PORT}")
+        
+        if not os.path.exists(BG_DATA_PATH):
+            logger.warning(f"WARNING: {BG_DATA_PATH} not found. AI assistant will not have specific menu data.")
+            structured_logger.log_system_event(
+                "AI assistant data file not found",
+                LogLevel.WARNING,
+                {
+                    "bg_data_path": BG_DATA_PATH,
+                    "impact": "AI assistant will have limited functionality"
+                }
+            )
+        else:
+            logger.info(f"AI assistant data loaded from: {os.path.abspath(BG_DATA_PATH)}")
+            structured_logger.log_system_event(
+                "AI assistant data loaded successfully",
+                LogLevel.INFO,
+                {"bg_data_path": os.path.abspath(BG_DATA_PATH)}
+            )
 
-
-    # Start the server
-    #rut without logs
-    return app
+        structured_logger.log_system_event(
+            "Application startup completed successfully",
+            LogLevel.INFO,
+            {
+                "server_url": f"http://localhost:{PORT}",
+                "development_mode": DEVELOPMENT
+            }
+        )
+        
+        logger.info("Pedidos Express application ready to serve requests")
+        return app
+        
+    except Exception as e:
+        error_msg = f"Critical error during application startup: {e}"
+        logger.critical(error_msg)
+        
+        structured_logger.log_system_event(
+            "Application startup failed with critical error",
+            LogLevel.CRITICAL,
+            {
+                "error": str(e),
+                "error_type": type(e).__name__
+            }
+        )
+        return None
 
 app = main()
 
 if not app:
     logger.critical("FATAL: Failed to create FastAPI app.")
+    structured_logger.log_system_event(
+        "Application failed to start - FastAPI app creation failed",
+        LogLevel.CRITICAL
+    )
     exit(1)
 
+logger.info("Application initialized successfully")
 
 if __name__ == "__main__":
+    logger.info(f"Starting server with uvicorn on port {PORT}")
+    structured_logger.log_system_event(
+        "Starting uvicorn server",
+        LogLevel.INFO,
+        {
+            "host": "0.0.0.0",
+            "port": PORT,
+            "log_level": "info"
+        }
+    )
     uvicorn.run(app, host="0.0.0.0", port=PORT, log_level="info", access_log=False)

+ 25 - 5
models/items.py

@@ -25,10 +25,10 @@ class Product(BaseModel):
     image: Optional[str] = None
     status: int = 1  # 0: Inactive, 1: Active
     quantity: Optional[int] = 1  # Optional quantity for the product
-    promo_id: Optional[int] = None  # ID of the promotional offer if applicable
-    promo_price: Optional[int] = None  # Promotional price if applicable
-    promo_day: Optional[int] = None  # Day of the week for promo (1-7)
-
+    promo_id:Optional[int]  # ID of the promotional offer if applicable
+    promo_price:Optional[int]  # Promotional price if applicable
+    promo_day:Optional[int]  # Day of the week for promo (1-7)
+    
 
 class ProductEditRequest(BaseModel):
     """Request model for editing a product"""
@@ -49,4 +49,24 @@ class ProductCreateRequest(BaseModel):
     price: int
     image: str
     status: Optional[int] = 1  # 0: Inactive, 1: Active
-    quantity: Optional[int] = 1  # Optional quantity for the product
+    quantity: Optional[int] = 1  # Optional quantity for the product
+
+class ProductTypes:
+    """Enumeration of product types"""
+    BEER = "Cerveza"
+    FOOD = "Coctel"
+
+    @staticmethod
+    def get_all_types() -> List[str]:
+        """Returns a list of all product types"""
+        atributos = {}
+        for nombre in dir(ProductTypes):
+            if not nombre.startswith('_'):  # Excluir atributos privados/especiales
+                valor = getattr(ProductTypes, nombre)
+                if not callable(valor):  # Excluir métodos
+                    atributos[nombre] = valor
+        return list(atributos.values())
+    
+
+if __name__ == "__main__":
+    print(ProductTypes.get_all_types())

+ 6 - 0
models/sales.py

@@ -3,10 +3,16 @@ from pydantic import BaseModel
 
 from models.items import Product
 
+
+class Promotion(BaseModel):
+    id: Optional[int] = None
+    price: Optional[float] = None
+
 class ItemWeb(BaseModel):
     id: int
     price: int
     quantity: int
+    promotion: Optional[Promotion] = None
 
 
 class OrderWeb(BaseModel):

+ 3 - 0
models/user.py

@@ -9,6 +9,9 @@ class RegisterUserRequest(BaseModel):
     email: str
     rut: str
 
+class UserRewardRequest(BaseModel):
+    tableNumber: int
+
 class PinUserRequest(BaseModel):
     pin: str = Field(min_length=4, max_length=4, description="4-digit PIN for user authentication")
 

+ 40 - 1
public/main/animations.css

@@ -50,4 +50,43 @@
       100% {
         transform: scale(0) translateX(-50%);
       }
-    }
+    }
+
+    @keyframes confetti-fall {
+            0% { transform: translateY(-100vh) rotate(0deg); opacity: 1; }
+            100% { transform: translateY(100vh) rotate(720deg); opacity: 0; }
+        }
+        
+        .confetti {
+            position: absolute;
+            width: 10px;
+            height: 10px;
+            animation: confetti-fall 3s linear infinite;
+        }
+        
+        @keyframes pulse-glow {
+            0%, 100% { box-shadow: 0 0 20px rgba(59, 130, 246, 0.5); }
+            50% { box-shadow: 0 0 40px rgba(59, 130, 246, 0.8); }
+        }
+        
+        .glow-effect {
+            animation: pulse-glow 2s ease-in-out infinite;
+        }
+
+        @keyframes float {
+            0%, 100% { transform: translateY(0px) rotate(0deg); }
+            50% { transform: translateY(-10px) rotate(5deg); }
+        }
+
+        .float-animation {
+            animation: float 3s ease-in-out infinite;
+        }
+
+        @keyframes sparkle {
+            0%, 100% { opacity: 1; transform: scale(1); }
+            50% { opacity: 0.7; transform: scale(1.2); }
+        }
+
+        .sparkle {
+            animation: sparkle 1.5s ease-in-out infinite;
+        }

+ 288 - 40
public/main/index.html

@@ -30,7 +30,6 @@
   <script src="express/js/app.js" type="module"></script>
   <link rel="stylesheet" href="express/styles.css">
 </head>
-
 <body class="h-[100dvh] max-h-[100dvh] flex flex-col bg-gray-50 overflow-x-hidden"
       style='font-family:"Spline Sans","Noto Sans",sans-serif;'>
 
@@ -44,46 +43,116 @@
   <!-- ---------- MAIN  ---------- -->
   <main class="relative flex-1 flex flex-col min-h-0 overflow-x-hidden">  
     <!-- ===== MENÚ tab ===== -->
-    <section id="menuTab" data-index="0" class=" min-h-0 overflow-y-auto h-full" data-tab>
-      <div class="pt-4 pb-3 ">
-        <h2 class="text-[19px] mx-4 font-bold text-[#101419]">
-          Pide tu shop express 🍺
-        </h2>
-        <p class="product-type mx-4 text-[#58728d] text-sm pb-4 mb-4 border-b border-gray-200">*solo lo más vendido</p>
-      </div>
-      <div class="px-4 overflow-y-auto">
-      <ul id="productList" class="space-y-6"></ul>
-      </div>
-
-      <template id="product-card-template">
-        <li class="flex items-stretch justify-between gap-4 rounded-xl">
-          <div class="flex flex-[2_2_0px] flex-col gap-4">
-            <div class="flex flex-col gap-1">
-              <p class="product-type text-[#58728d] text-sm"></p>
-              <p class="product-name text-[#101419] text-base font-bold leading-tight"></p>
-              <p class="product-description text-[#58728d] text-sm"></p>
-            </div>
-
-            <!-- Botón Añadir -->
-            <div class="flex items-center gap-3">
-              <button class="add-to-cart-btn flex items-center gap-1 w-fit h-8 px-3
-                             rounded-full bg-[#101419] hover:bg-[#37404a] text-white text-sm font-medium">
-                             
+    <section id="menuTab" data-index="0" class="min-h-0 overflow-y-auto h-full" data-tab>
+        <div class="pt-4 pb-3">
+            <h2 class="text-[19px] mx-4 font-bold text-[#101419]">
+                Pide tu shop express 🍺
+            </h2>
+            <p class="product-type mx-4 text-[#58728d] text-sm pb-4 mb-4 border-b border-gray-200">*solo lo más vendido</p>
+            
+            <!-- Barra de progreso para cerveza gratis -->
+            <div class="mx-4 mb-6 p-4 bg-gradient-to-r from-amber-50 to-orange-50 rounded-xl border border-amber-200">
+                <div class="flex items-center justify-between mb-2">
+                    <div class="flex items-center gap-2">
+                        <span class="text-lg">🍺</span>
+                        <span class="text-sm font-semibold text-amber-800">¡Cerveza gratis al 100%!</span>
+                    </div>
+                    <span id="progressText" class="text-sm font-bold text-amber-700">100%</span>
+                </div>
                 
-                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
-                  <path d="M12 5v14m7-7H5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-                </svg>
-                Añadir
-              </button>
-              <!-- Precio -->
-              <span class="product-price text-sm font-semibold text-[#101419]"></span>
+                <div class="relative w-full h-3 bg-amber-100 rounded-full overflow-hidden shadow-inner">
+                    <!-- Barra de progreso animada -->
+                    <div id="progressBar" class="h-full bg-gradient-to-r from-amber-400 to-orange-500 rounded-full transition-all duration-500 ease-out shadow-sm relative overflow-hidden" style="width: 100%">
+                        <!-- Efecto de brillo -->
+                        <div class="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent skew-x-12 animate-pulse"></div>
+                    </div>
+                    
+                    <!-- Indicador de meta -->
+                    <div class="absolute right-1 top-1/2 transform -translate-y-1/2">
+                        <div class="w-2 h-2 bg-amber-600 rounded-full border border-white shadow-sm"></div>
+                    </div>
+                </div>
+                
+                <div class="flex items-center justify-between mt-2">
+                    <span class="text-xs text-amber-700">Cada compra suma puntos</span>
+                    <button id="rewardBtn" class="text-xs bg-amber-500 hover:bg-amber-600 text-white px-3 py-1 rounded-full font-medium transition-colors duration-200 opacity-50 cursor-not-allowed" disabled>
+                        🎉 ¡Reclamar!
+                    </button>
+                </div>
             </div>
+        </div>
+        
+        <div class="px-4 overflow-y-auto">
+            <ul id="productList" class="space-y-6">
+                <!-- Productos de ejemplo para demostrar la funcionalidad -->
+                <li class="flex items-stretch justify-between gap-4 rounded-xl">
+                    <div class="flex flex-[2_2_0px] flex-col gap-4">
+                        <div class="flex flex-col gap-1">
+                            <p class="product-type text-[#58728d] text-sm">Cerveza</p>
+                            <p class="product-name text-[#101419] text-base font-bold leading-tight">Corona Extra</p>
+                            <p class="product-description text-[#58728d] text-sm">Cerveza mexicana refrescante</p>
+                        </div>
+                        <div class="flex items-center gap-3">
+                            <button class="add-to-cart-btn flex items-center gap-1 w-fit h-8 px-3 rounded-full bg-[#101419] hover:bg-[#37404a] text-white text-sm font-medium transition-colors">
+                                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
+                                    <path d="M12 5v14m7-7H5" stroke-linecap="round" stroke-linejoin="round"/>
+                                </svg>
+                                Añadir
+                            </button>
+                            <span class="product-price text-sm font-semibold text-[#101419]">$2.500</span>
+                        </div>
+                    </div>
+                    <div class="product-image flex-1 aspect-video bg-gradient-to-br from-yellow-200 to-amber-400 rounded-xl flex items-center justify-center">
+                        <span class="text-2xl">🍺</span>
+                    </div>
+                </li>
+                
+                <li class="flex items-stretch justify-between gap-4 rounded-xl">
+                    <div class="flex flex-[2_2_0px] flex-col gap-4">
+                        <div class="flex flex-col gap-1">
+                            <p class="product-type text-[#58728d] text-sm">Snacks</p>
+                            <p class="product-name text-[#101419] text-base font-bold leading-tight">Papas Fritas</p>
+                            <p class="product-description text-[#58728d] text-sm">Crujientes y saladas</p>
+                        </div>
+                        <div class="flex items-center gap-3">
+                            <button class="add-to-cart-btn flex items-center gap-1 w-fit h-8 px-3 rounded-full bg-[#101419] hover:bg-[#37404a] text-white text-sm font-medium transition-colors">
+                                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
+                                    <path d="M12 5v14m7-7H5" stroke-linecap="round" stroke-linejoin="round"/>
+                                </svg>
+                                Añadir
+                            </button>
+                            <span class="product-price text-sm font-semibold text-[#101419]">$1.200</span>
+                        </div>
+                    </div>
+                    <div class="product-image flex-1 aspect-video bg-gradient-to-br from-orange-200 to-red-300 rounded-xl flex items-center justify-center">
+                        <span class="text-2xl">🍟</span>
+                    </div>
+                </li>
+            </ul>
+        </div>
 
-          </div>
-          <div class="product-image flex-1 aspect-video bg-cover bg-center rounded-xl"></div>
-        </li>
-      </template>
-    </section>  
+        <template id="product-card-template">
+            <li class="flex items-stretch justify-between gap-4 rounded-xl">
+                <div class="flex flex-[2_2_0px] flex-col gap-4">
+                    <div class="flex flex-col gap-1">
+                        <p class="product-type text-[#58728d] text-sm"></p>
+                        <p class="product-name text-[#101419] text-base font-bold leading-tight"></p>
+                        <p class="product-description text-[#58728d] text-sm"></p>
+                    </div>
+                    <div class="flex items-center gap-3">
+                        <button class="add-to-cart-btn flex items-center gap-1 w-fit h-8 px-3 rounded-full bg-[#101419] hover:bg-[#37404a] text-white text-sm font-medium">
+                            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
+                                <path d="M12 5v14m7-7H5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+                            </svg>
+                            Añadir
+                        </button>
+                        <span class="product-price text-sm font-semibold text-[#101419]"></span>
+                    </div>
+                </div>
+                <div class="product-image flex-1 aspect-video bg-cover bg-center rounded-xl"></div>
+            </li>
+        </template>
+    </section>
 
     <!-- ===== CHAT ===== -->
     <section id="chatTab" data-index="2" data-tab class="flex hidden flex-col flex-1 min-h-0">
@@ -237,10 +306,10 @@
               origin-left">
   </div>
 
-  
+  <!-- MODALS -->
   <!-- === MODAL INICIO DE SESIÓN === -->
 <div id="sessionModal"
-     class="fixed hidden inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
+     class="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
   <form id="loginForm" class="bg-white w-full max-w-md p-8 rounded-xl shadow-xl space-y-6">
     <div class="text-center">
       <h2 class="text-2xl font-bold text-gray-900">¡Bienvenido!</h2>
@@ -299,6 +368,185 @@
   </form>
 </div>
 
+<!-- Modal de Términos y Condiciones del Regalo -->
+<div id="rewardModal" class="fixed hidden inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
+  <div class="bg-white w-full max-w-lg max-h-[90vh] rounded-xl shadow-xl overflow-hidden">
+    <!-- Header -->
+    <div class="bg-gradient-to-r from-amber-500 to-orange-500 px-6 py-4 text-white">
+      <div class="flex items-center justify-between">
+        <div class="flex items-center gap-3">
+          <span class="text-2xl">🍺</span>
+          <h2 class="text-xl font-bold">¡Cerveza Gratis!</h2>
+        </div>
+        <button id="closeRewardModal" class="text-white/80 hover:text-white text-2xl font-bold transition-colors">
+          ×
+        </button>
+      </div>
+    </div>
+
+    <!-- Content -->
+    <div class="overflow-y-auto max-h-[60vh] px-6 py-4">
+      <div class="space-y-4">
+        <!-- Descripción del premio -->
+        <div class="bg-amber-50 border border-amber-200 rounded-lg p-4">
+          <h3 class="font-semibold text-amber-800 mb-2">🎉 ¡Felicitaciones!</h3>
+          <p class="text-amber-700 text-sm">
+            Has alcanzado el 100% de progreso y puedes reclamar una cerveza gratis de tu elección.
+          </p>
+        </div>
+
+        <!-- Términos y Condiciones -->
+        <div class="space-y-3">
+          <h4 class="font-bold text-gray-800 text-base">Términos y Condiciones:</h4>
+          
+          <div class="text-sm text-gray-600 space-y-2">
+            <div class="flex items-start gap-2">
+              <span class="text-amber-500 font-bold">•</span>
+              <p>Válido únicamente en Biergarten Klein para consumo en el local.</p>
+            </div>
+            
+            <div class="flex items-start gap-2">
+              <span class="text-amber-500 font-bold">•</span>
+              <p>La cerveza gratis debe ser reclamada el mismo día que se alcanza el 100%.</p>
+            </div>
+            
+            <div class="flex items-start gap-2">
+              <span class="text-amber-500 font-bold">•</span>
+              <p>Aplica para cervezas de barril regulares (no incluye cervezas premium o importadas).</p>
+            </div>
+            
+            <div class="flex items-start gap-2">
+              <span class="text-amber-500 font-bold">•</span>
+              <p>No acumulable con otras promociones o descuentos.</p>
+            </div>
+            
+            <div class="flex items-start gap-2">
+              <span class="text-amber-500 font-bold">•</span>
+              <p>El premio no tiene valor en efectivo y no es transferible.</p>
+            </div>
+            
+            <div class="flex items-start gap-2">
+              <span class="text-amber-500 font-bold">•</span>
+              <p>Biergarten Klein se reserva el derecho de modificar o cancelar esta promoción sin previo aviso.</p>
+            </div>
+            
+            <div class="flex items-start gap-2">
+              <span class="text-amber-500 font-bold">•</span>
+              <p>Debes ser mayor de 18 años para reclamar bebidas alcohólicas.</p>
+            </div>
+          </div>
+        </div>
+
+        <!-- Instrucciones -->
+        <div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
+          <h4 class="font-semibold text-blue-800 mb-2">📋 Instrucciones:</h4>
+          <p class="text-blue-700 text-sm">
+            Una vez aceptes los términos, se generará un código que deberás mostrar al mesero junto con tu identificación para reclamar tu cerveza gratis.
+          </p>
+        </div>
+      </div>
+    </div>
+
+    <!-- Footer con checkbox y botones -->
+    <div class="border-t border-gray-200 px-6 py-4 space-y-4">
+      <!-- Checkbox de aceptación -->
+      <label class="flex items-start gap-3 cursor-pointer">
+        <input id="acceptTermsCheckbox" type="checkbox" class="mt-1 w-4 h-4 text-amber-600 bg-gray-100 border-gray-300 rounded focus:ring-amber-500 focus:ring-2">
+        <span class="text-sm text-gray-700">
+          He leído y acepto los términos y condiciones para reclamar mi cerveza gratis.
+        </span>
+      </label>
+
+      <!-- Botones -->
+      <div class="flex gap-3">
+        <button id="cancelRewardBtn" class="flex-1 bg-gray-100 hover:bg-gray-200 text-gray-700 py-3 rounded-lg font-medium transition-colors duration-200">
+          Cancelar
+        </button>
+        <button id="claimRewardBtn" class="flex-1 bg-amber-500 hover:bg-amber-600 text-white py-3 rounded-lg font-medium transition-colors duration-200 opacity-50 cursor-not-allowed" disabled>
+          🎉 Reclamar Premio
+        </button>
+      </div>
+    </div>
+  </div>
+</div>
+<div id="successRewardModal" class="fixed hidden inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
+   <div class="bg-white w-full max-w-md rounded-xl shadow-xl overflow-hidden">
+       <!-- Header -->
+       <div class="bg-[#101419] px-6 py-8 text-white text-center relative">
+           <!-- Elementos decorativos sutiles -->
+           <div class="absolute top-3 left-4 text-amber-300 text-lg">✨</div>
+           <div class="absolute top-4 right-6 text-amber-300 text-sm">🎉</div>
+           <div class="absolute bottom-3 right-4 text-amber-300 text-sm">💫</div>
+           
+           <!-- Icono principal -->
+           <div class="text-6xl mb-3">🍺</div>
+           
+           <!-- Título -->
+           <h2 class="text-2xl font-bold mb-2">¡FELICIDADES!</h2>
+           <div class="bg-white/20 rounded-full px-3 py-1 inline-block">
+               <p class="text-gray-100 text-sm font-medium">🎁 Premio Reclamado</p>
+           </div>
+       </div>
+
+       <!-- Content -->
+       <div class="px-6 py-6 space-y-4">
+           <!-- Mensaje de bienvenida -->
+           <div class="text-center">
+               <div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
+                   <h3 class="text-lg font-bold text-[#101419] mb-1">🍻 ¡Tu cerveza gratis te espera!</h3>
+                   <p class="text-[#58728d] text-sm">Sigue estos pasos para reclamar tu premio</p>
+               </div>
+           </div>
+
+           <!-- Instrucciones -->
+           <div class="space-y-3">
+               <div class="flex items-start gap-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
+                   <div class="bg-[#101419] text-white rounded-full w-7 h-7 flex items-center justify-center text-sm font-bold flex-shrink-0">1</div>
+                   <div class="text-sm">
+                       <div class="font-bold text-[#101419] mb-1">👨‍💼 Espera al mesero</div>
+                       <div class="text-[#58728d]">Este se acercará con tu comprobante de premio</div>
+                   </div>
+               </div>
+               
+               <div class="flex items-start gap-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
+                   <div class="bg-[#101419] text-white rounded-full w-7 h-7 flex items-center justify-center text-sm font-bold flex-shrink-0">2</div>
+                   <div class="text-sm">
+                       <div class="font-bold text-[#101419] mb-1">🍺 Elige tu cerveza favorita</div>
+                       <div class="text-[#58728d]">Selecciona cualquier cerveza hasta <span class="font-semibold">$5,000</span></div>
+                   </div>
+               </div>
+               
+               <div class="flex items-start gap-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
+                   <div class="bg-[#101419] text-white rounded-full w-7 h-7 flex items-center justify-center text-sm font-bold flex-shrink-0">3</div>
+                   <div class="text-sm">
+                       <div class="font-bold text-[#101419] mb-1">🎉 ¡Disfruta al máximo!</div>
+                       <div class="text-[#58728d]">Tu cerveza gratis está lista para disfrutar 🥳</div>
+                   </div>
+               </div>
+           </div>
+
+           <!-- Nota importante -->
+           <div class="bg-amber-50 border border-amber-200 rounded-lg p-3">
+               <div class="flex items-center gap-2 mb-1">
+                   <span class="text-base">⏰</span>
+                   <h4 class="font-semibold text-amber-800 text-sm">Importante</h4>
+               </div>
+               <p class="text-amber-700 text-sm">Este premio es válido solo para el día de hoy. ¡No pierdas esta oportunidad!</p>
+           </div>
+       </div>
+
+       <!-- Footer -->
+       <div class="px-6 py-4 bg-gray-50 border-t border-gray-200">
+           <button id="closeSuccessRewardModal" class="w-full bg-[#101419] hover:bg-[#37404a] text-white py-3 rounded-lg font-medium transition-colors duration-200">
+               <span class="flex items-center justify-center gap-2">
+                   <span>🎊</span>
+                   Aceptar
+                   <span>🍻</span>
+               </span>
+           </button>
+       </div>
+   </div>
+</div>
               <!-- ---------- JS: conmutar tabs + toast ---------- -->
   <script>
 

+ 154 - 7
public/main/js/app.js

@@ -2,8 +2,10 @@ import { sendMessage as serviceSendMessage } from './service/chat.js';
 import { getProducts, sendOrder } from './service/product.js';
 import { login } from './service/auth.js'
 import { createGlobalLoader, showGlobalLoader, hideGlobalLoader } from './utils/loader.js';
+import { updateProgress, claimReward } from './utils/progressBar.js';
 import { showError } from './utils/error.js';
 import { addHistoryRow, setupShoppingCart } from './utils/shoppingCart.js';
+import { hideGUI, showGUI } from './utils/gui.js';
 // --- Variables de Usuario ---
 let userId = -1;
 let userName = "Cliente";
@@ -45,6 +47,7 @@ async function initializeApp() {
     showGlobalLoader("Cargando productos...");
     setupShoppingCart(userId, userToken,userName);
     await renderProducts();
+    showGUI();
     hideGlobalLoader();
 
     const chatSuggestions = Array.from(chatSuggestionsElement.children);
@@ -60,8 +63,10 @@ function initializeLoginModal() {
     const sessionModal = document.getElementById('sessionModal');
     const loginForm = document.getElementById('loginForm');
     sessionModal.classList.remove('hidden');
+
     loginForm.addEventListener('submit', async (event) => {
         event.preventDefault();
+        event.stopPropagation();
 
         const fd = new FormData(loginForm);
         const email = fd.get('email').trim();
@@ -77,6 +82,7 @@ function initializeLoginModal() {
             userToken = data.token;
             userName = data.name;
             userId = data.id;
+            updateProgress(data.reward_progress || 0);
             if (!userToken || data.id === undefined) {
                 showError("Error al iniciar sesión.");
                 return;
@@ -86,11 +92,10 @@ function initializeLoginModal() {
 
             initializeApp();
         }catch (error) {
-        }
-
-    });
+            console.error(error)
+    }
+})
 }
-
 function initializeChat() {
     if (!chatForm) return;
     chatForm.addEventListener("submit", (event) => {
@@ -111,7 +116,7 @@ function initializeChat() {
 function setupBasicListeners() {
     if (!checkoutButton) return;
     checkoutButton.addEventListener("click", processOrder);
-
+    initializeRewards();
 }
 //#endregion
 //#region ===== Utilidad =====
@@ -305,7 +310,10 @@ async function processOrder() {
             totalAmount: cart.reduce((sum, item) => sum + item.price * item.quantity, 0),
             orderDate: new Date().toLocaleString('sv-SE').replace(' ', 'T')
         };
-        await sendOrder(orderData,userToken);
+        const data = await sendOrder(orderData,userToken);
+        if (data && data.new_progress) {
+            updateProgress(data.new_progress);
+        }
         alert("Pedido enviado con éxito.");
         cart.forEach(item => {
             addHistoryRow({
@@ -387,12 +395,151 @@ async function sendMessageToAI() {
 }
 
 //#endregion
+//#region ===== Rewards =====
+ // Referencias a elementos del DOM
+    const rewardBtn = document.getElementById('rewardBtn');
+    const rewardModal = document.getElementById('rewardModal');
+    const closeRewardModal = document.getElementById('closeRewardModal');
+    const closeSuccessRewardModalButton = document.getElementById('closeSuccessRewardModal');
+    const cancelRewardBtn = document.getElementById('cancelRewardBtn');
+    const acceptTermsCheckbox = document.getElementById('acceptTermsCheckbox');
+    const claimRewardBtn = document.getElementById('claimRewardBtn');
+
+
+    function initializeRewards() {
+     // Abrir modal cuando se hace clic en el botón de recompensa
+
+        closeSuccessRewardModalButton.addEventListener("click", closeSuccessRewardModal);
+
+    rewardBtn.addEventListener('click', function() {
+        if (!rewardBtn.disabled) {
+            rewardModal.classList.remove('hidden');
+            document.body.style.overflow = 'hidden'; // Evitar scroll del fondo
+        }
+    });
+
+    // Cerrar modal - botón X
+    closeRewardModal.addEventListener('click', function() {
+        closeModal();
+    });
+
+    // Cerrar modal - botón Cancelar
+    cancelRewardBtn.addEventListener('click', function() {
+        closeModal();
+    });
+
+    // Cerrar modal haciendo clic fuera de él
+    rewardModal.addEventListener('click', function(e) {
+        if (e.target === rewardModal) {
+            closeModal();
+        }
+    });
+
+    // Cerrar modal con tecla Escape
+    document.addEventListener('keydown', function(e) {
+        if (e.key === 'Escape' && !rewardModal.classList.contains('hidden')) {
+            closeModal();
+        }
+    });
+
+        // Manejar el reclamo del premio
+    claimRewardBtn.addEventListener('click', function() {
+        if (!this.disabled && acceptTermsCheckbox.checked) {
+            // Generar código de premio (puedes cambiar esta lógica)
+
+            // Mostrar mensaje de éxito
+            claimReward(userToken, userTable);
+            // Cerrar modal
+            closeModal();
+            updateProgress(0);
+        }
+    });
+
+
+    // Generar más confetti dinámicamente
+        console.log("Rewards initialized");
+    }
+   
+    function closeSuccessRewardModal() {
+        const successRewardModal = document.getElementById("successRewardModal");
+        successRewardModal.classList.add("hidden");
+        document.body.style.overflow = ''; // Restaurar scroll
+    }
+
+    // Función para cerrar el modal
+    function closeModal() {
+        rewardModal.classList.add('hidden');
+        document.body.style.overflow = ''; // Restaurar scroll
+        
+        // Reset del formulario al cerrar
+        acceptTermsCheckbox.checked = false;
+        claimRewardBtn.disabled = true;
+        claimRewardBtn.classList.add('opacity-50', 'cursor-not-allowed');
+    }
+
+    // Manejar el checkbox de términos y condiciones
+    acceptTermsCheckbox.addEventListener('change', function() {
+        if (this.checked) {
+            // Habilitar botón de reclamar
+            claimRewardBtn.disabled = false;
+            claimRewardBtn.classList.remove('opacity-50', 'cursor-not-allowed');
+        } else {
+            // Deshabilitar botón de reclamar
+            claimRewardBtn.disabled = true;
+            claimRewardBtn.classList.add('opacity-50', 'cursor-not-allowed');
+        }
+    });
+
+
 
+    // Función para mostrar mensaje de éxito con el código
+    function showRewardSuccess(code) {
+        // Crear elemento de notificación de éxito
+        const successToast = document.createElement('div');
+        successToast.className = `
+            fixed top-4 left-1/2 transform -translate-x-1/2 
+            bg-green-500 text-white text-sm rounded-lg px-6 py-4 
+            shadow-lg z-50 max-w-sm text-center
+        `;
+        successToast.innerHTML = `
+            <div class="font-bold mb-1">🎉 ¡Premio Reclamado!</div>
+            <div class="text-xs opacity-90">Tu código: <strong>${code}</strong></div>
+            <div class="text-xs mt-1 opacity-80">Muéstralo al mesero</div>
+        `;
+        
+        document.body.appendChild(successToast);
+        
+        // Remover después de 5 segundos
+        setTimeout(() => {
+            if (successToast.parentNode) {
+                successToast.remove();
+            }
+        }, 5000);
+    }
+
+    // Función para resetear el progreso de recompensa
+    function resetRewardProgress() {
+        const progressBar = document.getElementById('progressBar');
+        const progressText = document.getElementById('progressText');
+        const rewardBtn = document.getElementById('rewardBtn');
+        
+        // Resetear a 0%
+        progressBar.style.width = '0%';
+        progressText.textContent = '0%';
+        
+        // Deshabilitar botón de recompensa
+        rewardBtn.disabled = true;
+        rewardBtn.classList.add('opacity-50', 'cursor-not-allowed');
+        rewardBtn.classList.remove('bg-yellow-500', 'hover:bg-yellow-600');
+        rewardBtn.textContent = '🎉 ¡Reclamar!';
+    }
+
+//#endregion
 // --- APP initialization ---
 document.addEventListener("DOMContentLoaded", async () => {
-
     createGlobalLoader();
     initializeLoginModal();
+    hideGUI();
     // initializeApp()
 
 });

+ 1 - 0
public/main/js/service/auth.js

@@ -33,6 +33,7 @@ async function login(email,pin){
     showError("Error al iniciar sesión, Intenta mas tarde.");
     throw new Error("Error al iniciar sesión.");
   }
+  console.log("Inicio de sesión exitoso:", data);
   return {data: data.data};
 }
 

+ 23 - 0
public/main/js/service/product.js

@@ -21,6 +21,29 @@ async function sendOrder(order, token) {
     throw error;
   }
 }
+
+export async function freeBeer(token, tableNumber) {
+  try {
+    const response = await fetch("/api/users/reward", {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+        "Authorization": `Bearer ${token}`
+      },
+      body: JSON.stringify({ tableNumber })
+    });
+    if (!response.ok) {
+      const errorData = await response.json().catch(() => ({ message: "Respuesta no válida del servidor." }));
+      throw new Error(errorData.message || `Error del servidor: ${response.status}`);
+    }
+    const data = await response.json();
+    return data;
+  } catch (error) {
+    console.error("Error al obtener cerveza gratis:", error);
+    throw error;
+  }
+}
+
 async function getProducts(token){
   const response = await fetch("/api/products?status=1", {
     headers: {

+ 36 - 0
public/main/js/service/user.js

@@ -1,6 +1,42 @@
 import { showError } from "../utils/error.js";
 
 
+export async function getUserData(token) {
+  try {
+    const response = await fetch('/api/users/user', {
+      headers: {
+        'Content-Type': 'application/json',
+        'Authorization': `Bearer ${token}` 
+      }
+    });
+    if (response.status === 401) {
+      showError("No autorizado. Verifica tu sesión.");
+      throw new Error("Unauthorized access");
+    }
+    if (response.status === 429) {
+      showError("Demasiados intentos. Intenta más tarde.");
+      throw new Error("Too many requests");
+    }
+    if (response.status === 500) {
+      showError("Error interno del servidor. Intenta más tarde.");
+      throw new Error("Internal server error");
+    }
+    if (response.status === 403) {
+      showError("Acceso prohibido. Verifica tus permisos.");
+      throw new Error("Forbidden access");
+    }
+    if (response.status !== 200) {
+      showError(response.message);
+      throw new Error(`Error fetching user data: ${response.statusText}`);
+    }
+    const userData = await response.json();
+    return userData;
+  } catch (error) {
+    console.error('Error fetching user data:', error);
+    throw error;
+  }
+}
+
 async function fetchUserSales(userId, token) {
   try {
     const response = await fetch(`/api/sales/user/${userId}`, {

+ 16 - 0
public/main/js/utils/gui.js

@@ -0,0 +1,16 @@
+const header = document.querySelector('header');
+const mainContent = document.querySelector('main');
+const footer = document.querySelector('nav');
+
+function hideGUI() {
+    header.classList.add('hidden');
+    mainContent.classList.add('hidden');
+    footer.classList.add('hidden');
+}
+
+function showGUI() {
+    header.classList.remove('hidden');
+    mainContent.classList.remove('hidden');
+    footer.classList.remove('hidden');
+}
+export { hideGUI, showGUI };

+ 62 - 0
public/main/js/utils/progressBar.js

@@ -0,0 +1,62 @@
+import { freeBeer } from "../service/product.js";
+import { getUserData } from "../service/user.js";
+
+const progressBar = document.getElementById('progressBar');
+const progressText = document.getElementById('progressText');
+
+let currentProgress = 0;
+
+// Función para actualizar la barra de progreso
+export function updateProgress(value) {
+    const currentProgress = Math.min(value, 100);
+
+    // Actualizar visualmente
+    progressBar.style.width = currentProgress + '%';
+    progressText.textContent = currentProgress + '%';
+    
+    // Cambiar colores según el progreso
+    if (currentProgress >= 100) {
+        progressBar.className = 'h-full bg-gradient-to-r from-green-400 to-emerald-500 rounded-full transition-all duration-500 ease-out shadow-sm relative overflow-hidden';
+        progressText.textContent = '¡100% - Cerveza lista!';
+        progressText.className = 'text-sm font-bold text-green-700';
+        
+        // Activar botón de recompensa
+        rewardBtn.disabled = false;
+        rewardBtn.className = 'text-xs bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded-full font-medium transition-all duration-200 animate-bounce';
+        
+        // Efecto de confeti (simulado con animación)
+        document.querySelector('.bg-gradient-to-r.from-amber-50').className = 'mx-4 mb-6 p-4 bg-gradient-to-r from-green-50 to-emerald-50 rounded-xl border border-green-200 animate-pulse';
+        
+    } else if (currentProgress >= 75) {
+        progressBar.className = 'h-full bg-gradient-to-r from-orange-400 to-red-500 rounded-full transition-all duration-500 ease-out shadow-sm relative overflow-hidden';
+    }
+}
+
+export async function claimReward(token, userTable) {
+            const userData = await getUserData(token);
+            const progress = userData.rewardProgress || 0;
+            if (currentProgress >= progress) {
+
+                const response = await freeBeer(token, userTable);
+                if (!response || response.error) {
+                    alert("Error al reclamar la cerveza gratis. Por favor, inténtalo de nuevo más tarde.");
+                    return;
+                }
+
+                progressBar.style.width = '0%';
+                progressBar.className = 'h-full bg-gradient-to-r from-amber-400 to-orange-500 rounded-full transition-all duration-500 ease-out shadow-sm relative overflow-hidden';
+                progressText.textContent = '0%';
+                progressText.className = 'text-sm font-bold text-amber-700';
+                
+                // Desactivar botón
+                rewardBtn.disabled = true;
+                rewardBtn.className = 'text-xs bg-amber-500 hover:bg-amber-600 text-white px-3 py-1 rounded-full font-medium transition-colors duration-200 opacity-50 cursor-not-allowed';
+                
+                // Restaurar colores originales
+                document.querySelector('.bg-gradient-to-r.from-green-50').className = 'mx-4 mb-6 p-4 bg-gradient-to-r from-amber-50 to-orange-50 rounded-xl border border-amber-200';
+
+                document.querySelector("#successRewardModal").classList.remove("hidden");
+        }else {
+            alert("Te pedimos disculpas, aún no has alcanzado el progreso necesario para reclamar tu recompensa. Notifica este error al administrador del sistema.");
+        }
+}

+ 9 - 4
public/register/app.js

@@ -51,7 +51,7 @@ document.getElementById('registerForm').addEventListener('submit', function (e)
         body: JSON.stringify(data)
     })
         .then(response => {
-            if (!response.ok) {
+            if (response.status === 400) {
                 return response.json().then(errorData => {
                     showErrorMessage(errorData.message || 'Error al registrar el usuario.');
                 });
@@ -59,9 +59,14 @@ document.getElementById('registerForm').addEventListener('submit', function (e)
             return response.json();
         })
         .then(data => {
-            console.log('Registro exitoso:', data);
-            document.querySelector('#successPage').classList.remove('hidden');
-            hideRegisterModal();
+            if (!data) {
+                showErrorMessage("El usuario ya existe.");
+                throw new Error("El usuario ya existe.");
+            }else{
+                console.log('Registro exitoso:', data);
+                document.querySelector('#successPage').classList.remove('hidden');
+                hideRegisterModal();
+            }
         }).finally(() => {
             hideGlobalLoader();
         })

+ 78 - 4
routes/chat.py

@@ -4,11 +4,12 @@ from httpx import get
 from models.chat import ChatCompletionRequest
 from models.user import User
 from services.openai_service.openai_service import generate_completion
-from services.logging_service import log_llm_response
+from services.logging_service import log_llm_response, structured_logger, LogLevel
 from auth.security import get_current_user
 import logging
 from fastapi import APIRouter
 from config.messages import SuccessResponse
+
 logger = logging.getLogger(__name__)
 
 chat_router = APIRouter()
@@ -19,13 +20,86 @@ async def chat_completions(request_data: ChatCompletionRequest, request: Request
     """Get chat completions from OpenAI"""
     # Uses session_token (which is the antiAbuseToken) as an identifier for logging
     session_identifier = request.session.get("antiAbuseToken", "unknown_session")
+    
+    logger.info(f"Chat completion request from user {current_user.email}")
+    
+    structured_logger.log_chat_event(
+        f"Chat completion request from user {current_user.email}",
+        LogLevel.INFO,
+        {
+            "user_id": current_user.id,
+            "user_email": current_user.email,
+            "session_identifier": session_identifier,
+            "messages_count": len(request_data.messages),
+            "user_agent": request.headers.get("user-agent", "unknown"),
+            "request_user": request_data.user
+        },
+        user_id=current_user.id,
+        user_email=current_user.email
+    )
 
     try:
-        openai_response = await generate_completion(request_data.messages, session_identifier, current_user.name, current_user.email)
+        openai_response = await generate_completion(
+            request_data.messages, 
+            session_identifier, 
+            current_user.name, 
+            current_user.email
+        )
+        
+        structured_logger.log_chat_event(
+            f"Chat completion generated successfully",
+            LogLevel.INFO,
+            {
+                "user_id": current_user.id,
+                "user_email": current_user.email,
+                "session_identifier": session_identifier,
+                "response_length": len(openai_response),
+                "messages_processed": len(request_data.messages)
+            },
+            user_id=current_user.id,
+            user_email=current_user.email
+        )
+        
+        # Legacy logging function
         log_llm_response(request_data.user, openai_response)
+        
+        logger.info(f"Chat completion successful for user {current_user.email}")
         return JSONResponse({"response": openai_response, "message": SuccessResponse.CHAT_RESPONSE_SUCCESS})
+        
     except HTTPException as e:
-        raise e
+        logger.error(f"HTTP error in chat completion for user {current_user.email}: {e.detail}")
+        structured_logger.log_chat_event(
+            f"Chat completion HTTP error",
+            LogLevel.ERROR,
+            {
+                "user_id": current_user.id,
+                "user_email": current_user.email,
+                "session_identifier": session_identifier,
+                "status_code": e.status_code,
+                "error_detail": e.detail
+            },
+            user_id=current_user.id,
+            user_email=current_user.email
+        )
+        raise
+        
     except Exception as e:
-        logger.error(f"Unexpected error in /api/chat/completions: {e}")
+        error_msg = f"Unexpected error in /api/chat/completions for user {current_user.email}: {e}"
+        logger.error(error_msg)
+        
+        structured_logger.log_chat_event(
+            f"Chat completion unexpected error",
+            LogLevel.ERROR,
+            {
+                "user_id": current_user.id,
+                "user_email": current_user.email,
+                "session_identifier": session_identifier,
+                "error": str(e),
+                "error_type": type(e).__name__,
+                "messages_count": len(request_data.messages)
+            },
+            user_id=current_user.id,
+            user_email=current_user.email
+        )
+        
         raise HTTPException(status_code=500, detail="Error interno del servidor al procesar el chat.")

+ 539 - 51
routes/orders.py

@@ -1,102 +1,590 @@
-from threading import Thread
-from math import log
 import time
+from logging import getLogger
+from threading import Thread
 from uuid import uuid4
-from fastapi import HTTPException, APIRouter
+
+from fastapi import HTTPException, APIRouter, Depends
 from fastapi.responses import JSONResponse
+
 from fudo import fudo
 from models.sales import ItemWeb, OrderWeb
+from models.items import Item, Order
+from models.user import User
 from services.fudo_service import add_product_to_fudo
-from services.email_service import send_email
-from services.logging_service import log_order
-import services.print_service  as ps
-from logging import getLogger
+from services.email_service import get_email_sender
+from services.logging_service import log_order, structured_logger, LogLevel
+import services.print_service as ps
 from services.data_service import DataServiceFactory
 from config.mails import PRINTER_DISCONNECTED_MAIL
 from config.messages import ErrorResponse, SuccessResponse, UserResponse
 from config.settings import DEVELOPMENT
-from models.items import Item, Order
+from auth.security import get_current_user
+
 logger = getLogger(__name__)
+
+# Data services initialization
 user_data_service = DataServiceFactory.get_user_service()
 product_data_service = DataServiceFactory.get_product_service()
 sale_data_service = DataServiceFactory.get_sales_service()
+
+# Global variables
 printer_orders = []
 order_router = APIRouter()
 
+
 @order_router.post("/send")
-async def printer_order(order: OrderWeb):
+async def printer_order(order: OrderWeb, current_user: User = Depends(get_current_user)):
     """Process printer order"""
-    logger.info("Printer order received")
-    logger.info(order)
+    logger.info(f"Printer order received from user {current_user.email} for table {order.table}")
+    
+    structured_logger.log_order_event(
+        f"Print order received for table {order.table}",
+        LogLevel.INFO,
+        {
+            "table": order.table,
+            "items_count": len(order.items),
+            "customer_id": order.customerId,
+            "total_amount": order.totalAmount,
+            "order_date": order.orderDate
+        },
+        user_id=current_user.id,
+        user_email=current_user.email
+    )
+    
+    # Printer status validation
     if not DEVELOPMENT:
-        if not ps.get_status():
-            logger.error("Printer is not connected.")
-            email_thread = Thread(
-                target=send_email,
-                args=(PRINTER_DISCONNECTED_MAIL["subject"], PRINTER_DISCONNECTED_MAIL["body"], ["erwinjacimino2003@gmail.com"]),
-                daemon=True
+        try:
+            printer_status = ps.get_status()
+            if not printer_status:
+                logger.error(f"Printer is not connected. Order from user {current_user.email} cannot be processed.")
+                
+                structured_logger.log_print_event(
+                    f"Order rejected due to printer disconnection",
+                    LogLevel.ERROR,
+                    {
+                        "table": order.table,
+                        "user_id": current_user.id,
+                        "user_email": current_user.email,
+                        "items_count": len(order.items)
+                    },
+                    user_id=current_user.id,
+                    user_email=current_user.email
+                )
+                
+                # Send notification email to admins
+                email_thread = Thread(
+                    target=get_email_sender().send_email,
+                    args=(
+                        PRINTER_DISCONNECTED_MAIL["subject"], 
+                        PRINTER_DISCONNECTED_MAIL["body"], 
+                        ["erwinjacimino2003@gmail.com", "mompyn@gmail.com"]
+                    ),
+                    daemon=True
+                )
+                email_thread.start()
+                
+                structured_logger.log_email_event(
+                    "Printer disconnection notification sent to admins",
+                    LogLevel.WARNING,
+                    {
+                        "trigger_user": current_user.email,
+                        "admin_emails": ["erwinjacimino2003@gmail.com", "mompyn@gmail.com"]
+                    },
+                    user_id=current_user.id,
+                    user_email=current_user.email
+                )
+                
+                return JSONResponse(status_code=424, content={"message": ErrorResponse.PRINTER_DISCONNECTED})
+                
+        except Exception as e:
+            logger.error(f"Error checking printer status: {e}")
+            structured_logger.log_print_event(
+                "Printer status check failed",
+                LogLevel.ERROR,
+                {
+                    "error": str(e),
+                    "error_type": type(e).__name__,
+                    "user_id": current_user.id
+                },
+                user_id=current_user.id,
+                user_email=current_user.email
             )
-            email_thread.start()
-            logger.error("Email sent to admin about printer issue.")
-            return JSONResponse(status_code=424, content={"message": ErrorResponse.PRINTER_DISCONNECTED})
+            return JSONResponse(status_code=500, content={"message": "Error interno del servidor"})
 
+    # Extract order data
     items = order.items
     table = order.table
 
+    # Input validation
     if not items or not table:
+        logger.warning(f"Invalid order data from user {current_user.email}: missing items or table")
+        structured_logger.log_order_event(
+            "Order validation failed: missing items or table",
+            LogLevel.WARNING,
+            {
+                "has_items": bool(items),
+                "table": table,
+                "items_count": len(items) if items else 0
+            },
+            user_id=current_user.id,
+            user_email=current_user.email
+        )
         return JSONResponse(status_code=400, content={"message": ErrorResponse.MISSING_FIELDS})
 
     if not isinstance(table, int):
+        logger.warning(f"Invalid table type from user {current_user.email}: {type(table)}")
+        structured_logger.log_order_event(
+            "Order validation failed: invalid table type",
+            LogLevel.WARNING,
+            {
+                "table": table,
+                "table_type": str(type(table))
+            },
+            user_id=current_user.id,
+            user_email=current_user.email
+        )
         return JSONResponse(status_code=400, content={"message": ErrorResponse.INVALID_TABLE_TYPE})
 
+    logger.info(f"Processing order for table {table} with {len(items)} items")
+    structured_logger.log_order_event(
+        f"Starting order processing for table {table}",
+        LogLevel.INFO,
+        {
+            "table": table,
+            "items_count": len(items),
+            "item_ids": [item.id for item in items],
+            "item_quantities": [item.quantity for item in items]
+        },
+        user_id=current_user.id,
+        user_email=current_user.email
+    )
+
     # Add products to Fudo
     product_errors = []
-    for item in items:
+
+    # Get products data
+    try:
+        products = product_data_service.get_products([item.id for item in items])
+        logger.info(f"Retrieved {len(products)} products from database")
+        
+        structured_logger.log_order_event(
+            f"Retrieved products for order processing",
+            LogLevel.INFO,
+            {
+                "table": table,
+                "requested_products": len(items),
+                "found_products": len(products),
+                "product_ids": [p.id for p in products]
+            },
+            user_id=current_user.id,
+            user_email=current_user.email
+        )
+        
+    except Exception as e:
+        error_msg = f"Error retrieving products: {e}"
+        logger.error(error_msg)
+        structured_logger.log_database_event(
+            "Failed to retrieve products for order",
+            LogLevel.ERROR,
+            {
+                "table": table,
+                "requested_product_ids": [item.id for item in items],
+                "error": str(e),
+                "error_type": type(e).__name__
+            },
+            user_id=current_user.id,
+            user_email=current_user.email
+        )
+        return JSONResponse(status_code=500, content={"message": "Error interno del servidor"})
+
+    beers_for_promo = 0
+    
+    try:
         fudo.get_token()
-        product = add_product_to_fudo(item.id, item.quantity, table)
-        if not product:
-            product_errors.append(f"Error adding product {item.id} to table {table}.")
+        logger.info("Fudo token obtained successfully")
+        
+        for item, product in zip(items, products):
+            try:
+                # Si es dia de promo
+                if time.localtime().tm_wday + 1 == product.promo_day and product.promo_id:
+                    logger.info(f"Applying promotion for product {product.id} on table {table}")
+                    fudo_product = add_product_to_fudo(product.promo_id, item.quantity, table)
+                    
+                    structured_logger.log_order_event(
+                        f"Promotion applied for product {product.name}",
+                        LogLevel.INFO,
+                        {
+                            "product_id": product.id,
+                            "promo_id": product.promo_id,
+                            "table": table,
+                            "quantity": item.quantity,
+                            "promo_day": product.promo_day,
+                            "current_day": time.localtime().tm_wday + 1
+                        },
+                        user_id=current_user.id,
+                        user_email=current_user.email
+                    )
+                #en caso contrario
+                else:
+                    if product.type == "Cerveza":
+                        beers_for_promo += item.quantity
+                        logger.debug(f"Added {item.quantity} beers for promotion calculation")
+                    
+                    fudo_product = add_product_to_fudo(item.id, item.quantity, table)
+                    
+                    structured_logger.log_order_event(
+                        f"Added product {product.name} to Fudo",
+                        LogLevel.INFO,
+                        {
+                            "product_id": item.id,
+                            "product_name": product.name,
+                            "product_type": product.type,
+                            "table": table,
+                            "quantity": item.quantity,
+                            "is_beer": product.type == "Cerveza"
+                        },
+                        user_id=current_user.id,
+                        user_email=current_user.email
+                    )
+                
+                if not fudo_product:
+                    error_msg = f"Error adding product {item.id} to table {table}."
+                    product_errors.append(error_msg)
+                    logger.error(error_msg)
+                    
+                    structured_logger.log_order_event(
+                        f"Failed to add product to Fudo",
+                        LogLevel.ERROR,
+                        {
+                            "product_id": item.id,
+                            "table": table,
+                            "quantity": item.quantity
+                        },
+                        user_id=current_user.id,
+                        user_email=current_user.email
+                    )
+                    
+            except Exception as e:
+                error_msg = f"Error processing product {item.id}: {e}"
+                logger.error(error_msg)
+                product_errors.append(error_msg)
+                
+                structured_logger.log_order_event(
+                    f"Error processing product {item.id}",
+                    LogLevel.ERROR,
+                    {
+                        "product_id": item.id,
+                        "table": table,
+                        "error": str(e),
+                        "error_type": type(e).__name__
+                    },
+                    user_id=current_user.id,
+                    user_email=current_user.email
+                )
+                
+    except Exception as e:
+        error_msg = f"Error with Fudo integration: {e}"
+        logger.error(error_msg)
+        structured_logger.log_order_event(
+            "Fudo integration error",
+            LogLevel.ERROR,
+            {
+                "table": table,
+                "error": str(e),
+                "error_type": type(e).__name__
+            },
+            user_id=current_user.id,
+            user_email=current_user.email
+        )
+        return JSONResponse(status_code=500, content={"message": "Error interno del servidor"})
 
     if product_errors:
+        logger.error(f"Product errors occurred: {product_errors}")
+        structured_logger.log_order_event(
+            "Order processing failed due to product errors",
+            LogLevel.ERROR,
+            {
+                "table": table,
+                "product_errors": product_errors,
+                "error_count": len(product_errors)
+            },
+            user_id=current_user.id,
+            user_email=current_user.email
+        )
         return JSONResponse(
             status_code=424, 
             content={"message": ErrorResponse.PRODUCT_ADD_ERROR, "errors": product_errors}
         )
 
-    user = user_data_service.get_by_id(order.customerId)
+    # User validation
+    try:
+        user = user_data_service.get_by_id(order.customerId)
+        if not user:
+            logger.warning(f"User not found: {order.customerId}")
+            structured_logger.log_user_event(
+                f"Order rejected: user not found",
+                LogLevel.WARNING,
+                {
+                    "requested_user_id": order.customerId,
+                    "table": table
+                },
+                user_id=current_user.id,
+                user_email=current_user.email
+            )
+            return JSONResponse(status_code=404, content={"message": UserResponse.USER_NOT_FOUND.format(user_id=order.customerId)})
+        
+        logger.info(f"Order customer validated: {user.email}")
+        structured_logger.log_user_event(
+            f"Order customer validated",
+            LogLevel.INFO,
+            {
+                "customer_id": user.id,
+                "customer_email": user.email,
+                "customer_name": user.name,
+                "table": table
+            },
+            user_id=current_user.id,
+            user_email=current_user.email
+        )
+        
+    except Exception as e:
+        error_msg = f"Error validating user {order.customerId}: {e}"
+        logger.error(error_msg)
+        structured_logger.log_database_event(
+            "User validation error during order processing",
+            LogLevel.ERROR,
+            {
+                "requested_user_id": order.customerId,
+                "table": table,
+                "error": str(e),
+                "error_type": type(e).__name__
+            },
+            user_id=current_user.id,
+            user_email=current_user.email
+        )
+        return JSONResponse(status_code=500, content={"message": "Error interno del servidor"})
     
-    if not user:
-        return JSONResponse(status_code=404, content={"message": UserResponse.USER_NOT_FOUND.format(user_id=order.customerId)})
-    active_sale_id = fudo.get_active_sale(fudo.get_table(table))
-    if not active_sale_id:
-        logger.error(f"Error: No active sale found for table {table}.")
-        raise HTTPException(status_code=404)
-    active_sale_id = active_sale_id['id']
-    user_data_service.set_reward_progress(user.id, user.reward_progress + 10)  # Increment reward progress by 10%
-    products = product_data_service.get_products([item.id for item in items])
-
-    sale = sale_data_service.create(
-        order.customerId,
-        active_sale_id or uuid4().hex,
-        order.totalAmount,
-        order.table,
-        [item.id for item in items],
-        [item.quantity for item in items]
-    )   
-    logger.info(f"Sale created: {sale}")
-
-    ps.print_order(
-        Order(
+    # Get active sale
+    try:
+        active_sale_id = fudo.get_active_sale(fudo.get_table(table))
+        if not active_sale_id:
+            error_msg = f"No active sale found for table {table}"
+            logger.error(error_msg)
+            structured_logger.log_order_event(
+                "Order failed: no active sale found",
+                LogLevel.ERROR,
+                {
+                    "table": table,
+                    "customer_id": order.customerId
+                },
+                user_id=current_user.id,
+                user_email=current_user.email
+            )
+            raise HTTPException(status_code=404, detail=error_msg)
+            
+        active_sale_id = active_sale_id['id']
+        logger.info(f"Active sale found for table {table}: {active_sale_id}")
+        
+        structured_logger.log_order_event(
+            f"Active sale retrieved for table {table}",
+            LogLevel.INFO,
+            {
+                "table": table,
+                "sale_id": active_sale_id
+            },
+            user_id=current_user.id,
+            user_email=current_user.email
+        )
+        
+    except HTTPException:
+        raise
+    except Exception as e:
+        error_msg = f"Error retrieving active sale for table {table}: {e}"
+        logger.error(error_msg)
+        structured_logger.log_order_event(
+            "Error retrieving active sale",
+            LogLevel.ERROR,
+            {
+                "table": table,
+                "error": str(e),
+                "error_type": type(e).__name__
+            },
+            user_id=current_user.id,
+            user_email=current_user.email
+        )
+        raise HTTPException(status_code=500, detail="Error interno del servidor")
+
+    # Update user reward progress
+    try:
+        new_progress = user.reward_progress + beers_for_promo * 10
+        user_data_service.set_reward_progress(user.id, new_progress)
+        
+        logger.info(f"Updated reward progress for user {user.email}: {user.reward_progress} -> {new_progress}")
+        structured_logger.log_user_event(
+            f"Reward progress updated",
+            LogLevel.INFO,
+            {
+                "user_id": user.id,
+                "old_progress": user.reward_progress,
+                "new_progress": new_progress,
+                "beers_count": beers_for_promo,
+                "points_added": beers_for_promo * 10,
+                "table": table
+            },
+            user_id=user.id,
+            user_email=user.email
+        )
+        
+    except Exception as e:
+        error_msg = f"Error updating reward progress for user {user.id}: {e}"
+        logger.error(error_msg)
+        structured_logger.log_database_event(
+            "Failed to update reward progress",
+            LogLevel.ERROR,
+            {
+                "user_id": user.id,
+                "beers_for_promo": beers_for_promo,
+                "error": str(e),
+                "error_type": type(e).__name__
+            },
+            user_id=user.id,
+            user_email=user.email
+        )
+        # Don't fail the order for this, just log it
+        new_progress = user.reward_progress
+
+    # Create sale record
+    try:
+        sale = sale_data_service.create(
+            order.customerId,
+            active_sale_id or uuid4().hex,
+            order.totalAmount,
+            order.table,
+            [item.id for item in items],
+            [item.quantity for item in items]
+        )   
+        
+        if sale > 0:
+            logger.info(f"Sale created successfully: ID {sale}")
+            structured_logger.log_order_event(
+                f"Sale record created successfully",
+                LogLevel.INFO,
+                {
+                    "sale_id": sale,
+                    "customer_id": order.customerId,
+                    "table": table,
+                    "total_amount": order.totalAmount,
+                    "fudo_sale_id": active_sale_id,
+                    "items_count": len(items)
+                },
+                user_id=current_user.id,
+                user_email=current_user.email
+            )
+        else:
+            error_msg = "Failed to create sale record"
+            logger.error(error_msg)
+            structured_logger.log_database_event(
+                "Sale creation failed",
+                LogLevel.ERROR,
+                {
+                    "customer_id": order.customerId,
+                    "table": table,
+                    "total_amount": order.totalAmount,
+                    "fudo_sale_id": active_sale_id
+                },
+                user_id=current_user.id,
+                user_email=current_user.email
+            )
+            return JSONResponse(status_code=500, content={"message": "Error interno del servidor"})
+            
+    except Exception as e:
+        error_msg = f"Error creating sale record: {e}"
+        logger.error(error_msg)
+        structured_logger.log_database_event(
+            "Sale creation error",
+            LogLevel.ERROR,
+            {
+                "customer_id": order.customerId,
+                "table": table,
+                "error": str(e),
+                "error_type": type(e).__name__
+            },
+            user_id=current_user.id,
+            user_email=current_user.email
+        )
+        return JSONResponse(status_code=500, content={"message": "Error interno del servidor"})
+
+    # Print order
+    try:
+        order_for_print = Order(
             table=table,
             items=[Item(name=product.name, price=product.price or 0, quantity=item.quantity) for product, item in zip(products, items)],
             customerName=user.name,
             totalAmount=order.totalAmount,
             orderDate=order.orderDate
         )
-    )
+        
+        ps.print_order(order_for_print)
+        
+        logger.info(f"Order printed successfully for table {table}")
+        structured_logger.log_print_event(
+            f"Order printed successfully for table {table}",
+            LogLevel.INFO,
+            {
+                "table": table,
+                "customer_name": user.name,
+                "total_amount": order.totalAmount,
+                "items_count": len(items),
+                "sale_id": sale
+            },
+            user_id=current_user.id,
+            user_email=current_user.email
+        )
+        
+    except Exception as e:
+        error_msg = f"Error printing order for table {table}: {e}"
+        logger.error(error_msg)
+        structured_logger.log_print_event(
+            f"Order print failed for table {table}",
+            LogLevel.ERROR,
+            {
+                "table": table,
+                "customer_name": user.name,
+                "error": str(e),
+                "error_type": type(e).__name__,
+                "sale_id": sale
+            },
+            user_id=current_user.id,
+            user_email=current_user.email
+        )
+        # Don't fail the order for print issues, just log it
 
-    # Log order
-    log_order(user.name, order.table, order_date=order.orderDate, items=[product.name for product in products])
+    # Log order (legacy function)
+    try:
+        log_order(user.name, order.table, order_date=order.orderDate, items=[product.name for product in products])
+        
+        structured_logger.log_order_event(
+            f"Order completed successfully for table {table}",
+            LogLevel.INFO,
+            {
+                "table": table,
+                "customer_id": user.id,
+                "customer_name": user.name,
+                "customer_email": user.email,
+                "total_amount": order.totalAmount,
+                "items": [{"name": product.name, "quantity": item.quantity, "price": product.price} for product, item in zip(products, items)],
+                "sale_id": sale,
+                "new_reward_progress": new_progress,
+                "beers_for_promo": beers_for_promo
+            },
+            user_id=user.id,
+            user_email=user.email
+        )
+        
+    except Exception as e:
+        logger.error(f"Error in legacy order logging: {e}")
+        # Don't fail the order for logging issues
 
-    return JSONResponse({"message": SuccessResponse.ORDER_SUCCESS})
+    logger.info(f"Order processing completed successfully for table {table}, sale ID: {sale}")
+    return JSONResponse({"message": SuccessResponse.ORDER_SUCCESS, "new_progress": new_progress})
 

+ 28 - 6
routes/products.py

@@ -1,3 +1,5 @@
+
+
 """
 Product Routes Module
 
@@ -25,10 +27,11 @@ from h11 import Data
 
 # Local imports
 from auth.security import get_current_user
-from models import user
+from models.user import User
 from models.items import Product, ProductCreateRequest, ProductEditRequest
 from services.data_service import DataServiceFactory
 from config.messages import ErrorResponse, SuccessResponse, UserResponse
+from services.print_service import print_ticket
 
 # Initialize logger for this module
 logger = getLogger(__name__)
@@ -44,11 +47,14 @@ def apply_promo_price(product: Product):
     """Apply promotional price to a product if applicable."""
     #dia de la semana 1-7
     day_of_week = time.localtime().tm_wday + 1  # Convert to 1-7 range
+    product_dict = product.model_dump(exclude={"promo_id", "promo_price", "promo_day"})
     if product.promo_id and product.promo_price and product.promo_day == day_of_week:
-        product.price = product.promo_price
-        product.id = product.promo_id
-    return product
-    
+        product_dict['promotion'] = {
+            "id": product.promo_id,
+            "price": product.promo_price,
+        }
+
+    return product_dict
 
 @product_router.get("/")
 async def get_products(status: Optional[int] = Query(None), current_user = Depends(get_current_user)):
@@ -64,7 +70,6 @@ async def get_products(status: Optional[int] = Query(None), current_user = Depen
     # Retrieve all products and convert to dictionary format
     all_products =  product_data_service.get_all()
     all_products = list(map(apply_promo_price, all_products))
-    all_products = [product.model_dump() for product in all_products]   
 
 
     if status is not None:
@@ -96,6 +101,23 @@ async def get_product(product_id: int, current_user = Depends(get_current_user))
     # Return 404 if product not found
     return JSONResponse({"message": UserResponse.USER_NOT_FOUND.format(user_id=product_id)}, status_code=404)
 
+@product_router.get("/free-beer/{table_id}")
+async def get_free_beer(table_id: int, current_user:User = Depends(get_current_user)):
+    """
+    Get the free beer product - Available to all authenticated users
+    
+    Returns:
+        JSONResponse: Free beer product data if found, error message if not found
+    """
+    logger.debug(f"Current user: {current_user.email if current_user else 'Anonymous'}")
+    logger.info("Fetching free beer ticket")
+    
+    if current_user.reward_progress >= 100:
+        print_ticket(table_id)
+        return JSONResponse({"message": SuccessResponse.REWARD_SUCCESS}, status_code=200)    
+
+    # Return 404 if free beer product not found
+    return JSONResponse({"message": UserResponse.USER_NOT_FOUND.format(user_id="free_beer")}, status_code=404)
 # MODERATE RISK OPERATIONS - Requires permissions >= 1 (Manager level or above)
 
 @product_router.patch("/{product_id}/edit")

+ 340 - 80
routes/users.py

@@ -1,27 +1,25 @@
-from email.policy import HTTP
+
 import json
 from logging import getLogger
-from math import log
-from os import name
 from uuid import uuid4
-from click import File
-from fastapi import APIRouter, HTTPException, Request
-from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
-from httpx import RequestError
+
 import redis
-from rsa import verify
-import config
-from models import user
-from models.user import  LoginRequest, PinUserRequest, UserIDRequest, RegisterUserRequest
-from services.data_service import UserDataService
 from cryptography.fernet import Fernet
-from config.settings import PIN_KEY
+from fastapi import APIRouter, Depends, Request
+from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
+
 from auth.security import generate_token
-from services.email_service import send_email
+from auth.security import get_current_user
 from config.mails import REGISTER_MAIL
-from config.settings import APPNAME
 from config.messages import ErrorResponse, SuccessResponse, UserResponse
+from config.settings import APPNAME, PIN_KEY
+from models.user import LoginRequest, PinUserRequest, RegisterUserRequest, User, User, UserIDRequest, UserRewardRequest
+from services.data_service import UserDataService
+from services.email_service import get_email_sender
+from services.print_service import print_ticket
+from services.logging_service import structured_logger, LogLevel
 from utils.rut import validate_rut
+
 fernet = Fernet(PIN_KEY.encode())
 logger = getLogger(__name__)
 user_data_service = UserDataService()
@@ -47,30 +45,149 @@ async def exists_user(request: UserIDRequest):
 @user_router.post("/register")
 async def register_user(request: RegisterUserRequest):
     """Register a new user"""
+    logger.info(f"Registration attempt for email: {request.email}")
+    
+    structured_logger.log_user_event(
+        f"User registration attempt",
+        LogLevel.INFO,
+        {
+            "email": request.email,
+            "name": request.name,
+            "rut": request.rut
+        },
+        user_email=request.email
+    )
+    
+    # Validate RUT
     if not validate_rut(request.rut):
+        logger.warning(f"Registration failed for {request.email}: invalid RUT {request.rut}")
+        structured_logger.log_user_event(
+            "Registration failed: invalid RUT",
+            LogLevel.WARNING,
+            {
+                "email": request.email,
+                "rut": request.rut,
+                "reason": "Invalid RUT format"
+            },
+            user_email=request.email
+        )
         return JSONResponse(status_code=400, content={"message": ErrorResponse.INVALID_RUT})
-    user = user_data_service.get_by_email(request.email)
-    if user:
-        return JSONResponse(status_code=400, content={"message": UserResponse.USER_ALREADY_EXISTS})
-    user = user_data_service.get_by_rut(request.rut)
-    if user:
-        return JSONResponse(status_code=400, content={"message": UserResponse.USER_ALREADY_EXISTS})
+    
+    # Check if user already exists by email
+    try:
+        user = user_data_service.get_by_email(request.email)
+        if user:
+            logger.warning(f"Registration failed for {request.email}: user already exists")
+            structured_logger.log_user_event(
+                "Registration failed: user already exists",
+                LogLevel.WARNING,
+                {
+                    "email": request.email,
+                    "existing_user_id": user.id,
+                    "reason": "Email already registered"
+                },
+                user_id=user.id,
+                user_email=request.email
+            )
+            return JSONResponse(status_code=400, content={"message": UserResponse.USER_ALREADY_EXISTS})
+            
+        # Check if RUT already exists
+        user = user_data_service.get_by_rut(request.rut)
+        if user:
+            logger.warning(f"Registration failed for {request.email}: RUT already exists")
+            structured_logger.log_user_event(
+                "Registration failed: RUT already exists",
+                LogLevel.WARNING,
+                {
+                    "email": request.email,
+                    "rut": request.rut,
+                    "existing_user_id": user.id,
+                    "reason": "RUT already registered"
+                },
+                user_email=request.email
+            )
+            return JSONResponse(status_code=400, content={"message": UserResponse.USER_ALREADY_EXISTS})
+            
+    except Exception as e:
+        error_msg = f"Database error during user validation: {e}"
+        logger.error(error_msg)
+        structured_logger.log_database_event(
+            "User validation error during registration",
+            LogLevel.ERROR,
+            {
+                "email": request.email,
+                "error": str(e),
+                "error_type": type(e).__name__
+            },
+            user_email=request.email
+        )
+        return JSONResponse(status_code=500, content={"message": "Error interno del servidor"})
+    
     logger.info(f"Registering user: {request.email}")
-    redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
-    verification_code = str(uuid4())
-    redis_client.set(f"verify:{verification_code}", json.dumps({
-        "name": request.name,
-        "email": request.email,
-        "rut": request.rut
-    }))
-    redis_client.expire(f"verify:{verification_code}", 3600)  # Expire in 1 hour
-
-    send_email(
-        REGISTER_MAIL["subject"],
-        REGISTER_MAIL["body"],
-        [request.email], name=request.name, app_name=APPNAME, verification_code=verification_code
-    )
-    return JSONResponse(status_code=201, content={"message": SuccessResponse.USER_CREATED_SUCCESS})
+    
+    try:
+        # Setup Redis client and verification code
+        redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
+        verification_code = str(uuid4())
+        
+        user_data = {
+            "name": request.name,
+            "email": request.email,
+            "rut": request.rut
+        }
+        
+        redis_client.set(f"verify:{verification_code}", json.dumps(user_data))
+        redis_client.expire(f"verify:{verification_code}", 3600)  # Expire in 1 hour
+        
+        structured_logger.log_user_event(
+            "Verification code generated for user registration",
+            LogLevel.INFO,
+            {
+                "email": request.email,
+                "verification_code": verification_code,
+                "expires_in": 3600
+            },
+            user_email=request.email
+        )
+
+        # Send verification email
+        get_email_sender().send_email(
+            REGISTER_MAIL["subject"],
+            REGISTER_MAIL["body"],
+            [request.email], 
+            name=request.name, 
+            app_name=APPNAME, 
+            verification_code=verification_code
+        )
+        
+        structured_logger.log_email_event(
+            "Registration verification email sent",
+            LogLevel.INFO,
+            {
+                "email": request.email,
+                "verification_code": verification_code
+            },
+            user_email=request.email
+        )
+        
+        logger.info(f"Registration initiated successfully for {request.email}")
+        return JSONResponse(status_code=201, content={"message": SuccessResponse.USER_CREATED_SUCCESS})
+        
+    except Exception as e:
+        error_msg = f"Error during registration process for {request.email}: {e}"
+        logger.error(error_msg)
+        structured_logger.log_user_event(
+            "Registration process failed",
+            LogLevel.ERROR,
+            {
+                "email": request.email,
+                "error": str(e),
+                "error_type": type(e).__name__,
+                "step": "verification_setup_or_email"
+            },
+            user_email=request.email
+        )
+        return JSONResponse(status_code=500, content={"message": "Error interno del servidor"})
 
 @user_router.post("/create-user")
 async def create_user(request: PinUserRequest, q: str):
@@ -103,55 +220,176 @@ async def create_user(request: PinUserRequest, q: str):
 @user_router.post("/login")
 async def login_user(request: LoginRequest, http_request: Request):
     """Login user with email and PIN"""
-    redis_client = redis.Redis(host='localhost', port=6379, db=0)
-    logger.debug(f"Login attempt for email: {request.email}")
-    is_blocked = redis_client.get(f"blocked:{request.email}")
+    logger.info(f"Login attempt for email: {request.email}")
+    
+    structured_logger.log_security_event(
+        f"Login attempt for user {request.email}",
+        LogLevel.INFO,
+        {
+            "email": request.email,
+            "user_agent": http_request.headers.get("user-agent", "unknown"),
+            "referer": http_request.headers.get("referer", "unknown"),
+            "client_ip": http_request.client.host if http_request.client else "unknown"
+        },
+        user_email=request.email
+    )
+    
+    try:
+        redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
+        is_blocked = redis_client.get(f"blocked:{request.email}")
 
+        if is_blocked:
+            try:
+                blocked_time_raw = redis_client.ttl(f"blocked:{request.email}")
+                blocked_minutes = max(0, int(blocked_time_raw) // 60) if blocked_time_raw and int(blocked_time_raw) > 0 else 0  # type: ignore
+            except (ValueError, TypeError):
+                blocked_minutes = 0
+            
+            logger.warning(f"Login attempt for blocked user: {request.email}, blocked for {blocked_minutes} minutes")
+            structured_logger.log_security_event(
+                f"Login attempt by blocked user",
+                LogLevel.WARNING,
+                {
+                    "email": request.email,
+                    "blocked_time_remaining_minutes": blocked_minutes,
+                    "user_agent": http_request.headers.get("user-agent", "unknown")
+                },
+                user_email=request.email
+            )
+            return JSONResponse(
+                status_code=403, 
+                content={"message": UserResponse.USER_FORMAT_BLOCKED.format(time=f"{blocked_minutes} minutos")}
+            )
 
-    if is_blocked:
-        logger.warning(f"Login attempt for blocked user: {request.email}, locked: {is_blocked}")
-        return JSONResponse(status_code=403, content={"message": UserResponse.USER_FORMAT_BLOCKED.format(time=f"{int(str(redis_client.ttl(f'blocked:{request.email}'))) // 60} minutos")})
-    else:
-        logger.info(f"{redis_client.get(f'blocked:{request.email}')}")
-    
-    user = user_data_service.login(request.email, request.pin)
+        # Attempt login
+        user = user_data_service.login(request.email, request.pin)
 
+        if user:
+            # Successful login
+            logger.info(f"Successful login for user: {request.email}")
+            
+            # Check admin access
+            referer = http_request.headers.get("referer", "")
+            if referer and "admin" in referer:
+                user_permissions = user_data_service.permissions(user.id)
+                if user_permissions == 0:
+                    logger.warning(f"Unauthorized admin access attempt by {request.email}")
+                    structured_logger.log_security_event(
+                        f"Unauthorized admin access attempt",
+                        LogLevel.WARNING,
+                        {
+                            "email": request.email,
+                            "user_id": user.id,
+                            "permissions": user_permissions,
+                            "referer": referer
+                        },
+                        user_id=user.id,
+                        user_email=request.email
+                    )
+                    return JSONResponse(status_code=403, content={"message": UserResponse.NOT_PERMITTED})
 
-    if user:
-        # Successful login, return user data and token
-
-        referer = http_request.headers.get("referer")
-        if referer and "admin" in referer:
-            if user_data_service.permissions(user.id) == 0:
-                logger.warning(f"Unauthorized admin access attempt by {request.email}")
-                return JSONResponse(status_code=403, content={"message": UserResponse.NOT_PERMITTED})
-
-        redis_client.delete(f"login_attempts:{request.email}")
-        return JSONResponse(status_code=200, content={"message": SuccessResponse.LOGIN_SUCCESS, "data": {
-            "id": user.id,
-            "name": user.name,
-            "email": user.email,
-            "kleincoins": user.kleincoins,
-            "created_at": user.created_at,
-            "token": generate_token(user.email)
-        }})
-    else:
-        # Failed login: increment attempts in Redis
-        redis_client.incr(f"login_attempts:{request.email}")
-        redis_client.expire(f"login_attempts:{request.email}", 3600)
-        redis_attempts = redis_client.get(f"login_attempts:{request.email}")
-        attempts = int(redis_attempts) if redis_attempts else 0 # type: ignore
-        if attempts and int(attempts) >= 5:
-            # Too many attempts, block login
-            redis_client.set(f"blocked:{request.email}", "true")
-            redis_client.expire(f"blocked:{request.email}", 3600)
-            logger.warning(f"Too many login attempts for {request.email}. User blocked.")
-            return JSONResponse(status_code=429, content={"message": ErrorResponse.TOO_MANY_ATTEMPTS})
+            # Clear login attempts and log successful login
+            redis_client.delete(f"login_attempts:{request.email}")
+            
+            structured_logger.log_security_event(
+                f"Successful login",
+                LogLevel.INFO,
+                {
+                    "email": request.email,
+                    "user_id": user.id,
+                    "user_name": user.name,
+                    "permissions": user_data_service.permissions(user.id),
+                    "is_admin_login": "admin" in referer,
+                    "reward_progress": user.reward_progress
+                },
+                user_id=user.id,
+                user_email=request.email
+            )
+            
+            return JSONResponse(status_code=200, content={
+                "message": SuccessResponse.LOGIN_SUCCESS, 
+                "data": {
+                    "id": user.id,
+                    "name": user.name,
+                    "email": user.email,
+                    "kleincoins": user.kleincoins,
+                    "created_at": user.created_at,
+                    "token": generate_token(user.email),
+                    "reward_progress": user.reward_progress,
+                }
+            })
         else:
-            logger.warning(f"Failed login attempt for {request.email}. Attempts: {attempts}")
-            logger.error("Invalid email or PIN.")
-        # Return unauthorized with attempts remaining
-        return JSONResponse(status_code=401, content={"message": ErrorResponse.INVALID_CREDENTIALS, "attempts_remaining": 5 - attempts if attempts else 5})
+            # Failed login: increment attempts in Redis
+            redis_client.incr(f"login_attempts:{request.email}")
+            redis_client.expire(f"login_attempts:{request.email}", 3600)
+            redis_attempts = redis_client.get(f"login_attempts:{request.email}")
+            attempts = int(redis_attempts) if redis_attempts else 0  # type: ignore
+            
+            if attempts >= 5:
+                # Too many attempts, block login
+                redis_client.set(f"blocked:{request.email}", "true")
+                redis_client.expire(f"blocked:{request.email}", 3600)
+                
+                logger.warning(f"Too many login attempts for {request.email}. User blocked.")
+                structured_logger.log_security_event(
+                    f"User blocked due to too many failed login attempts",
+                    LogLevel.WARNING,
+                    {
+                        "email": request.email,
+                        "failed_attempts": attempts,
+                        "blocked_duration_seconds": 3600
+                    },
+                    user_email=request.email
+                )
+                return JSONResponse(status_code=429, content={"message": ErrorResponse.TOO_MANY_ATTEMPTS})
+            else:
+                logger.warning(f"Failed login attempt for {request.email}. Attempts: {attempts}")
+                structured_logger.log_security_event(
+                    f"Failed login attempt",
+                    LogLevel.WARNING,
+                    {
+                        "email": request.email,
+                        "failed_attempts": attempts,
+                        "attempts_remaining": 5 - attempts,
+                        "reason": "Invalid credentials"
+                    },
+                    user_email=request.email
+                )
+            
+            # Return unauthorized with attempts remaining
+            return JSONResponse(status_code=401, content={
+                "message": ErrorResponse.INVALID_CREDENTIALS, 
+                "attempts_remaining": 5 - attempts if attempts else 5
+            })
+            
+    except redis.RedisError as e:
+        error_msg = f"Redis error during login for {request.email}: {e}"
+        logger.error(error_msg)
+        structured_logger.log_system_event(
+            "Redis error during login process",
+            LogLevel.ERROR,
+            {
+                "email": request.email,
+                "error": str(e),
+                "error_type": "RedisError"
+            }
+        )
+        return JSONResponse(status_code=500, content={"message": "Error interno del servidor"})
+        
+    except Exception as e:
+        error_msg = f"Unexpected error during login for {request.email}: {e}"
+        logger.error(error_msg)
+        structured_logger.log_security_event(
+            "Unexpected error during login",
+            LogLevel.ERROR,
+            {
+                "email": request.email,
+                "error": str(e),
+                "error_type": type(e).__name__
+            },
+            user_email=request.email
+        )
+        return JSONResponse(status_code=500, content={"message": "Error interno del servidor"})
 
 @user_router.delete("/delete")
 async def delete_user(request: UserIDRequest):
@@ -162,6 +400,28 @@ async def delete_user(request: UserIDRequest):
     else:
         return JSONResponse(status_code=404, content={"message": UserResponse.USER_NOT_FOUND})
 
+@user_router.post("/reward")
+async def reward_user(request: UserRewardRequest, user: User = Depends(get_current_user)):
+    """Reward a user with 1 free beer"""
+    if user.reward_progress < 100:
+        return JSONResponse(status_code=400, content={"message": UserResponse.REWARD_INSUFFICIENT_PROGRESS.format(progress=user.reward_progress)})
+    if not user:
+        return JSONResponse(status_code=404, content={"message": UserResponse.USER_NOT_FOUND.format(user_id=request.id)})
+    
+    user_data_service.set_reward_progress(user.id, 0)
+    print_ticket(request.tableNumber)
+    return JSONResponse(status_code=200, content={"message": SuccessResponse.REWARD_SUCCESS, "data": {
+        "id": user.id,
+        "name": user.name,
+        "email": user.email,
+        "reward_progress": 0
+    }})
+
+@user_router.get("/user")
+async def get_cur_user(current_user:User = Depends(get_current_user)):
+    """Get current user information"""
+    return JSONResponse(status_code=200, content={"data": current_user.model_dump(exclude={"pin_hash", "kleincoins", "rut"})})
+
 @user_router.get("/all")
 async def get_all_users():
     """Get all users"""

+ 5 - 2
services/data_service.py

@@ -6,7 +6,7 @@ import psycopg2
 from config.settings import POSTGRESQL_DB_CONFIG
 from typing import List, Dict, Optional, Any
 from abc import ABC, abstractmethod
-from config.settings import BG_DATA_PATH, DB_PATH, IMAGE_PATH, PRODUCT_DATA_PATH, CURRENT_URL
+from config.settings import BG_DATA_PATH, IMAGE_PATH, PRODUCT_DATA_PATH, CURRENT_URL
 from logging import getLogger
 from datetime import datetime
 from cryptography.fernet import Fernet
@@ -1044,7 +1044,10 @@ class SalesDataService(BaseDataService):
                 price=product[4],
                 image=product[5],
                 status=product[6],
-                quantity=product[7]
+                quantity=product[7],
+                promo_id=None,  # Not applicable for sale products
+                promo_price=None,
+                promo_day=None
             ) for product in products
         ]
     

+ 173 - 42
services/email_service.py

@@ -1,74 +1,205 @@
+import email
 from config.settings import MAIL, MAIL_PASSWORD
 import smtplib
 from email.message import EmailMessage
 from logging import getLogger
-from contextlib import contextmanager
 from typing import Optional
-logger = getLogger(__name__)
+from services.logging_service import structured_logger, LogLevel
 
-email_sender = None
+logger = getLogger(__name__)
 class EmailSender:
-    def __init__(self, email: str, password: str):
-        self.email = email
-        self.password = password
+    def __init__(self):
+        self.email = MAIL
+        self.password = MAIL_PASSWORD
         self._smtp: Optional[smtplib.SMTP_SSL] = None
-        logger.debug(f"EmailSender initialized with {self.email}")
+        self.connect()
 
-    @contextmanager
-    def get_connection(self):
+    def connect(self):
         if self._smtp is None:
-            logger.info("Establishing new SMTP connection.")
+            logger.info("Establishing new SMTP connection...")
+            structured_logger.log_email_event(
+                "Establishing SMTP connection",
+                LogLevel.INFO,
+                {"email_server": "smtp.gmail.com", "port": 465, "sender_email": self.email}
+            )
+            
             try:
                 self._smtp = smtplib.SMTP_SSL('smtp.gmail.com', 465)
                 self._smtp.login(self.email, self.password)
-                logger.info("SMTP connection established and logged in.")
+                self._smtp.ehlo()            
+                
+                logger.info("SMTP connection established and authenticated.")
+                structured_logger.log_email_event(
+                    "SMTP authentication successful",
+                    LogLevel.INFO,
+                    {"sender_email": self.email}
+                )
+                
+            except smtplib.SMTPAuthenticationError as e:
+                error_msg = f"SMTP authentication failed: {e}"
+                logger.error(error_msg)
+                structured_logger.log_email_event(
+                    "SMTP authentication failed",
+                    LogLevel.ERROR,
+                    {
+                        "sender_email": self.email,
+                        "error": str(e),
+                        "error_type": "SMTPAuthenticationError"
+                    }
+                )
+                raise
             except Exception as e:
-                logger.error(f"Failed to establish SMTP connection: {e}")
+                error_msg = f"Failed to establish SMTP connection: {e}"
+                logger.error(error_msg)
+                structured_logger.log_email_event(
+                    "SMTP connection failed",
+                    LogLevel.ERROR,
+                    {
+                        "sender_email": self.email,
+                        "error": str(e),
+                        "error_type": type(e).__name__
+                    }
+                )
                 raise
-        try:
-            yield self._smtp
-        except Exception as e:
-            logger.error(f"Error during SMTP operation: {e}")
-            self.close_connection()
-            raise e
 
-    def close_connection(self):
+    def close(self):
         if self._smtp:
+            logger.info("Closing SMTP connection.")
+            structured_logger.log_email_event(
+                "Closing SMTP connection",
+                LogLevel.INFO,
+                {"sender_email": self.email}
+            )
             try:
-                logger.info("Closing SMTP connection.")
                 self._smtp.quit()
-                logger.info("SMTP connection closed.")
+                self._smtp = None
             except Exception as e:
                 logger.warning(f"Error closing SMTP connection: {e}")
-            finally:
-                self._smtp = None
+                self._smtp = None  # Force reset even if quit fails
 
     def send_email(self, subject: str, body: str, to: list[str], **kwargs):
-        logger.debug(f"Preparing to send email to: {to} with subject: '{subject}' and kwargs: {kwargs}")
-        
-        msg = EmailMessage()
-        msg['Subject'] = subject
-        msg['From'] = self.email
-        msg['To'] = ", ".join(to)
-        msg.set_content('Este correo tiene contenido HTML.')
-        msg.add_alternative(body.format(**kwargs), subtype='html')
+        if self._smtp is None:
+            self.connect()
+            
+        logger.debug(f"Preparing to send email to: {to} with subject: '{subject}'")
+        structured_logger.log_email_event(
+            f"Preparing to send email with subject: '{subject}'",
+            LogLevel.INFO,
+            {
+                "subject": subject,
+                "recipients": to,
+                "sender_email": self.email,
+                "body_length": len(body),
+                "kwargs_count": len(kwargs)
+            }
+        )
         
         try:
-            with self.get_connection() as smtp:
-                smtp.send_message(msg)
-                logger.info(f"Email sent to {to} with subject '{subject}'.")
+            msg = EmailMessage()
+            msg['Subject'] = subject
+            msg['From'] = self.email
+            msg['To'] = ", ".join(to)
+            msg.set_content('Este correo tiene contenido HTML.')
+            msg.add_alternative(body.format(**kwargs), subtype='html')
+            
+            if not self._smtp:
+                error_msg = "Cannot send email because SMTP connection is not established"
+                logger.error(error_msg)
+                structured_logger.log_email_event(
+                    "Email send failed: SMTP connection not established",
+                    LogLevel.ERROR,
+                    {
+                        "subject": subject,
+                        "recipients": to,
+                        "sender_email": self.email
+                    }
+                )
+                raise ConnectionError(error_msg)
+
+            self._smtp.send_message(msg)
+            logger.info(f"Email sent to {to} with subject '{subject}'.")
+            structured_logger.log_email_event(
+                f"Email sent successfully",
+                LogLevel.INFO,
+                {
+                    "subject": subject,
+                    "recipients": to,
+                    "sender_email": self.email,
+                    "recipients_count": len(to)
+                }
+            )
+            
+        except smtplib.SMTPServerDisconnected as e:
+            logger.warning("SMTP connection disconnected, retrying...")
+            structured_logger.log_email_event(
+                "SMTP disconnected during send, attempting retry",
+                LogLevel.WARNING,
+                {
+                    "subject": subject,
+                    "recipients": to,
+                    "error": str(e)
+                }
+            )
+            
+            try:
+                self.close()
+                self.connect()
+                self._smtp.send_message(msg)
+                
+                logger.info(f"Email resent successfully to {to}.")
+                structured_logger.log_email_event(
+                    "Email sent successfully after retry",
+                    LogLevel.INFO,
+                    {
+                        "subject": subject,
+                        "recipients": to,
+                        "sender_email": self.email,
+                        "retry_attempt": True
+                    }
+                )
+            except Exception as retry_error:
+                error_msg = f"Failed to resend email after retry: {retry_error}"
+                logger.error(error_msg)
+                structured_logger.log_email_event(
+                    "Email retry failed",
+                    LogLevel.ERROR,
+                    {
+                        "subject": subject,
+                        "recipients": to,
+                        "original_error": str(e),
+                        "retry_error": str(retry_error),
+                        "error_type": type(retry_error).__name__
+                    }
+                )
+                self.close()
+                raise
+                
         except Exception as e:
-            logger.error(f"Failed to send email to {to}: {e}")
+            error_msg = f"Failed to send email to {to}: {e}"
+            logger.error(error_msg)
+            structured_logger.log_email_event(
+                "Email send failed",
+                LogLevel.ERROR,
+                {
+                    "subject": subject,
+                    "recipients": to,
+                    "sender_email": self.email,
+                    "error": str(e),
+                    "error_type": type(e).__name__
+                }
+            )
+            self.close()
             raise
 
+email_sender: Optional[EmailSender] = None
 
 def initialize_email_sender():
     global email_sender
-    if email_sender is None:
-        email_sender = EmailSender(MAIL, MAIL_PASSWORD)
-        logger.info("EmailSender initialized globally.")
-    return email_sender
+    email_sender = EmailSender()
 
-def send_email(subject: str, body: str, to: list[str], **kwargs):
-    email_sender = initialize_email_sender()
-    email_sender.send_email(subject, body, to, **kwargs)
+def get_email_sender() -> EmailSender:
+    if email_sender is None:
+        initialize_email_sender()
+    if not email_sender:
+        raise ValueError("Email sender is not initialized and failed to initialize.")
+    return email_sender

+ 204 - 19
services/logging_service.py

@@ -1,28 +1,213 @@
 import csv
 import os
-from typing import List
+import json
+from typing import List, Dict, Any, Optional
+from datetime import datetime
+from logging import getLogger
+from enum import Enum
 from models.sales import OrderWeb, ItemWeb
 
+logger = getLogger(__name__)
 
+class LogLevel(Enum):
+    """Log levels for structured logging"""
+    DEBUG = "DEBUG"
+    INFO = "INFO"
+    WARNING = "WARNING"
+    ERROR = "ERROR"
+    CRITICAL = "CRITICAL"
+
+class LogCategory(Enum):
+    """Categories for different types of logs"""
+    ORDER = "ORDER"
+    USER = "USER"
+    PAYMENT = "PAYMENT"
+    SECURITY = "SECURITY"
+    API = "API"
+    DATABASE = "DATABASE"
+    EMAIL = "EMAIL"
+    PRINT = "PRINT"
+    CHAT = "CHAT"
+    SYSTEM = "SYSTEM"
+
+class StructuredLogger:
+    """Enhanced logging service with structured logging capabilities"""
+    
+    def __init__(self):
+        self.logger = getLogger(__name__)
+        self.logs_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'logs')
+        self._ensure_logs_directory()
+    
+    def _ensure_logs_directory(self):
+        """Ensure logs directory exists"""
+        if not os.path.exists(self.logs_dir):
+            os.makedirs(self.logs_dir)
+            self.logger.info(f"Created logs directory: {self.logs_dir}")
+    
+    def _write_structured_log(self, category: LogCategory, level: LogLevel, 
+                            message: str, data: Optional[Dict[str, Any]] = None,
+                            user_id: Optional[int] = None, user_email: Optional[str] = None):
+        """Write structured log entry"""
+        log_entry = {
+            "timestamp": datetime.now().isoformat(),
+            "category": category.value,
+            "level": level.value,
+            "message": message,
+            "user_id": user_id,
+            "user_email": user_email,
+            "data": data or {}
+        }
+        
+        # Write to category-specific log file
+        log_file = os.path.join(self.logs_dir, f"{category.value.lower()}.log")
+        try:
+            with open(log_file, 'a', encoding='utf-8') as f:
+                f.write(json.dumps(log_entry, ensure_ascii=False) + '\n')
+        except Exception as e:
+            self.logger.error(f"Failed to write structured log: {e}")
+    
+    def log_order_event(self, message: str, level: LogLevel = LogLevel.INFO,
+                       order_data: Optional[Dict] = None, user_id: Optional[int] = None,
+                       user_email: Optional[str] = None):
+        """Log order-related events"""
+        self._write_structured_log(LogCategory.ORDER, level, message, order_data, user_id, user_email)
+        
+        # Also log to main logger
+        getattr(self.logger, level.value.lower())(
+            f"[ORDER] {message} - User: {user_email or user_id or 'N/A'}"
+        )
+    
+    def log_user_event(self, message: str, level: LogLevel = LogLevel.INFO,
+                      user_data: Optional[Dict] = None, user_id: Optional[int] = None,
+                      user_email: Optional[str] = None):
+        """Log user-related events"""
+        self._write_structured_log(LogCategory.USER, level, message, user_data, user_id, user_email)
+        
+        # Also log to main logger
+        getattr(self.logger, level.value.lower())(
+            f"[USER] {message} - User: {user_email or user_id or 'N/A'}"
+        )
+    
+    def log_security_event(self, message: str, level: LogLevel = LogLevel.WARNING,
+                          security_data: Optional[Dict] = None, user_id: Optional[int] = None,
+                          user_email: Optional[str] = None):
+        """Log security-related events"""
+        self._write_structured_log(LogCategory.SECURITY, level, message, security_data, user_id, user_email)
+        
+        # Security events should always be logged to main logger
+        getattr(self.logger, level.value.lower())(
+            f"[SECURITY] {message} - User: {user_email or user_id or 'N/A'}"
+        )
+    
+    def log_api_event(self, message: str, level: LogLevel = LogLevel.INFO,
+                     api_data: Optional[Dict] = None, user_id: Optional[int] = None,
+                     user_email: Optional[str] = None):
+        """Log API-related events"""
+        self._write_structured_log(LogCategory.API, level, message, api_data, user_id, user_email)
+        
+        getattr(self.logger, level.value.lower())(
+            f"[API] {message} - User: {user_email or user_id or 'N/A'}"
+        )
+    
+    def log_database_event(self, message: str, level: LogLevel = LogLevel.INFO,
+                          db_data: Optional[Dict] = None, user_id: Optional[int] = None,
+                          user_email: Optional[str] = None):
+        """Log database-related events"""
+        self._write_structured_log(LogCategory.DATABASE, level, message, db_data, user_id, user_email)
+        
+        getattr(self.logger, level.value.lower())(
+            f"[DATABASE] {message} - User: {user_email or user_id or 'N/A'}"
+        )
+    
+    def log_email_event(self, message: str, level: LogLevel = LogLevel.INFO,
+                       email_data: Optional[Dict] = None, user_id: Optional[int] = None,
+                       user_email: Optional[str] = None):
+        """Log email-related events"""
+        self._write_structured_log(LogCategory.EMAIL, level, message, email_data, user_id, user_email)
+        
+        getattr(self.logger, level.value.lower())(
+            f"[EMAIL] {message} - User: {user_email or user_id or 'N/A'}"
+        )
+    
+    def log_print_event(self, message: str, level: LogLevel = LogLevel.INFO,
+                       print_data: Optional[Dict] = None, user_id: Optional[int] = None,
+                       user_email: Optional[str] = None):
+        """Log printer-related events"""
+        self._write_structured_log(LogCategory.PRINT, level, message, print_data, user_id, user_email)
+        
+        getattr(self.logger, level.value.lower())(
+            f"[PRINT] {message} - User: {user_email or user_id or 'N/A'}"
+        )
+    
+    def log_chat_event(self, message: str, level: LogLevel = LogLevel.INFO,
+                      chat_data: Optional[Dict] = None, user_id: Optional[int] = None,
+                      user_email: Optional[str] = None):
+        """Log chat/LLM-related events"""
+        self._write_structured_log(LogCategory.CHAT, level, message, chat_data, user_id, user_email)
+        
+        getattr(self.logger, level.value.lower())(
+            f"[CHAT] {message} - User: {user_email or user_id or 'N/A'}"
+        )
+    
+    def log_system_event(self, message: str, level: LogLevel = LogLevel.INFO,
+                        system_data: Optional[Dict] = None):
+        """Log system-related events"""
+        self._write_structured_log(LogCategory.SYSTEM, level, message, system_data)
+        
+        getattr(self.logger, level.value.lower())(f"[SYSTEM] {message}")
+
+# Global instance
+structured_logger = StructuredLogger()
+
+# Legacy functions for backward compatibility
 def log_order(username, table, order_date, items: List[str]):
-    """Log order information to CSV file"""
-    if not os.path.exists('logs.csv'):
-        with open('logs.csv', 'w', newline='') as f:
+    """Log order information to CSV file (legacy function)"""
+    try:
+        structured_logger.log_order_event(
+            f"Order placed for table {table}",
+            LogLevel.INFO,
+            {
+                "username": username,
+                "table": table,
+                "order_date": order_date,
+                "items": items,
+                "item_count": len(items) if isinstance(items, list) else 1
+            },
+            user_email=username
+        )
+        
+        # Still maintain CSV for backward compatibility
+        csv_file = os.path.join(structured_logger.logs_dir, 'orders.csv')
+        if not os.path.exists(csv_file):
+            with open(csv_file, 'w', newline='', encoding='utf-8') as f:
+                writer = csv.writer(f)
+                writer.writerow(['userName', 'table', 'orderDate', 'items'])
+        
+        with open(csv_file, 'a', newline='', encoding='utf-8') as f:
             writer = csv.writer(f)
-            writer.writerow(['userName', 'table', 'orderDate', 'items'])
-    
-    with open('logs.csv', 'a', newline='') as f:
-        writer = csv.writer(f)
-        writer.writerow([
-            username,
-            table,
-            order_date,
-            items
-        ])
-
+            writer.writerow([username, table, order_date, items])
+            
+    except Exception as e:
+        logger.error(f"Failed to log order: {e}")
 
 def log_llm_response(user: str, response: str):
-    """Log LLM response to file"""
-    file_mode = "a" if os.path.exists("llm_logs.txt") else "w"
-    with open("llm_logs.txt", file_mode) as f:
-        f.write(f"{user}: {response}\n")
+    """Log LLM response to file (legacy function)"""
+    try:
+        structured_logger.log_chat_event(
+            f"LLM response generated for user {user}",
+            LogLevel.INFO,
+            {
+                "response_length": len(response),
+                "response_preview": response[:100] + "..." if len(response) > 100 else response
+            },
+            user_email=user
+        )
+        
+        # Still maintain text file for backward compatibility
+        llm_log_file = os.path.join(structured_logger.logs_dir, 'llm_responses.txt')
+        file_mode = "a" if os.path.exists(llm_log_file) else "w"
+        with open(llm_log_file, file_mode, encoding='utf-8') as f:
+            f.write(f"{datetime.now().isoformat()} - {user}: {response}\n")
+            
+    except Exception as e:
+        logger.error(f"Failed to log LLM response: {e}")

+ 227 - 25
services/print_service.py

@@ -1,41 +1,243 @@
 import requests
+from logging import getLogger
+from config.settings import DEVELOPMENT
 from models.sales import OrderWeb, ItemWeb
 from services.data_service import DataServiceFactory
 from models.items import Order
+from services.logging_service import structured_logger, LogLevel
+
+logger = getLogger(__name__)
 user_data_service = DataServiceFactory.get_user_service()
 
+if DEVELOPMENT:
+    printer_url = "http://localhost:5005/"
+else:
+    printer_url = "http://localhost:5004/"
 
-def print_order(order:Order):
+def print_order(order: Order):
     """Send order to printer"""
-    if not order.items or not order.table:
-        raise ValueError("Order must have items and a table number.")
-    # Prepare the order data for printing
-    order_data = {
-        "table": order.table,
-        "items": [{"name": item.name,"price": item.price, "quantity": item.quantity} for item in order.items],
-        "customerName": order.customerName,
-        "totalAmount": order.totalAmount,
-        "orderDate": order.orderDate
-    }
-    print("Order data prepared for printing:", order_data)
-
-    # Send the order data to the printer service
-    response = requests.post("http://localhost:5004/print", json=order_data)
+    logger.info(f"Attempting to print order for table {order.table}")
+    
+    try:
+        if not order.items or not order.table:
+            error_msg = "Order must have items and a table number"
+            logger.error(error_msg)
+            structured_logger.log_print_event(
+                f"Print order validation failed: {error_msg}",
+                LogLevel.ERROR,
+                {"table": order.table, "items_count": len(order.items) if order.items else 0}
+            )
+            raise ValueError(error_msg)
+        
+        # Prepare the order data for printing
+        order_data = {
+            "table": order.table,
+            "items": [{"name": item.name, "price": item.price, "quantity": item.quantity} for item in order.items],
+            "customerName": order.customerName,
+            "totalAmount": order.totalAmount,
+            "orderDate": order.orderDate
+        }
+        
+        logger.info(f"Order data prepared for printing: table={order.table}, items={len(order.items)}, total={order.totalAmount}")
+        structured_logger.log_print_event(
+            f"Sending order to printer for table {order.table}",
+            LogLevel.INFO,
+            {
+                "table": order.table,
+                "items_count": len(order.items),
+                "total_amount": order.totalAmount,
+                "customer_name": order.customerName
+            }
+        )
+
+        # Send the order data to the printer service
+        response = requests.post(
+            f"{printer_url}/print", 
+            json=order_data, 
+            headers={"Authorization": f"Bearer PRINTER123cerveza@"},
+            timeout=10
+        )
+
+        if response.status_code != 200:
+            error_msg = f"Failed to print order: HTTP {response.status_code} - {response.text}"
+            logger.error(error_msg)
+            structured_logger.log_print_event(
+                f"Print order failed for table {order.table}",
+                LogLevel.ERROR,
+                {
+                    "table": order.table,
+                    "status_code": response.status_code,
+                    "error_message": response.text,
+                    "printer_url": printer_url
+                }
+            )
+            raise Exception(error_msg)
+
+        logger.info(f"Order printed successfully for table {order.table}")
+        structured_logger.log_print_event(
+            f"Order printed successfully for table {order.table}",
+            LogLevel.INFO,
+            {
+                "table": order.table,
+                "response": response.json() if response.headers.get('content-type', '').startswith('application/json') else str(response.text)
+            }
+        )
+        
+        return response.json()  # Return the response from the printer service
+        
+    except requests.RequestException as e:
+        error_msg = f"Network error while printing order for table {order.table}: {e}"
+        logger.error(error_msg)
+        structured_logger.log_print_event(
+            f"Network error during print for table {order.table}",
+            LogLevel.ERROR,
+            {
+                "table": order.table,
+                "error": str(e),
+                "printer_url": printer_url
+            }
+        )
+        raise Exception(error_msg)
+    except Exception as e:
+        error_msg = f"Unexpected error while printing order for table {order.table}: {e}"
+        logger.error(error_msg)
+        structured_logger.log_print_event(
+            f"Unexpected print error for table {order.table}",
+            LogLevel.ERROR,
+            {
+                "table": order.table,
+                "error": str(e),
+                "error_type": type(e).__name__
+            }
+        )
+        raise
+
+def print_ticket(number_table: int):
+    """Send a ticket to the printer"""
+    logger.info(f"Attempting to print ticket for table {number_table}")
     
-    if response.status_code != 200:
-        raise Exception(f"Failed to print order: {response.text}")
+    try:
+        structured_logger.log_print_event(
+            f"Sending ticket print request for table {number_table}",
+            LogLevel.INFO,
+            {"table": number_table}
+        )
+        
+        response = requests.get(
+            f"{printer_url}/ticket/{number_table}", 
+            headers={"Authorization": f"Bearer PRINTER123cerveza@"},
+            timeout=10
+        )
 
-    return response.json()  # Return the response from the printer service
+        if response.status_code != 200:
+            error_msg = f"Failed to print ticket for table {number_table}: HTTP {response.status_code} - {response.text}"
+            logger.error(error_msg)
+            structured_logger.log_print_event(
+                f"Ticket print failed for table {number_table}",
+                LogLevel.ERROR,
+                {
+                    "table": number_table,
+                    "status_code": response.status_code,
+                    "error_message": response.text,
+                    "printer_url": printer_url
+                }
+            )
+            raise Exception(error_msg)
+
+        logger.info(f"Ticket printed successfully for table {number_table}")
+        structured_logger.log_print_event(
+            f"Ticket printed successfully for table {number_table}",
+            LogLevel.INFO,
+            {
+                "table": number_table,
+                "response": response.json() if response.headers.get('content-type', '').startswith('application/json') else str(response.text)
+            }
+        )
+        
+        return response.json()  # Return the response from the printer service
+        
+    except requests.RequestException as e:
+        error_msg = f"Network error while printing ticket for table {number_table}: {e}"
+        logger.error(error_msg)
+        structured_logger.log_print_event(
+            f"Network error during ticket print for table {number_table}",
+            LogLevel.ERROR,
+            {
+                "table": number_table,
+                "error": str(e),
+                "printer_url": printer_url
+            }
+        )
+        raise Exception(error_msg)
+    except Exception as e:
+        error_msg = f"Unexpected error while printing ticket for table {number_table}: {e}"
+        logger.error(error_msg)
+        structured_logger.log_print_event(
+            f"Unexpected ticket print error for table {number_table}",
+            LogLevel.ERROR,
+            {
+                "table": number_table,
+                "error": str(e),
+                "error_type": type(e).__name__
+            }
+        )
+        raise
 
 def get_status():
     """Get the status of the printer service"""
+    logger.info("Checking printer service status")
+    
     try:
-        response = requests.get("http://localhost:5004/status")
+        structured_logger.log_print_event(
+            "Checking printer service status",
+            LogLevel.INFO,
+            {"printer_url": printer_url}
+        )
+        
+        response = requests.get(
+            f"{printer_url}/status", 
+            headers={"Authorization": f"Bearer PRINTER123cerveza@"},
+            timeout=5
+        )
+        
         data = response.json()
-        print("Printer service status:", data)
-        if data.get("status"):
-            return True
-        else:
-            return False
+        status = data.get("status", False)
+        
+        logger.info(f"Printer service status: {'online' if status else 'offline'}")
+        structured_logger.log_print_event(
+            f"Printer service status check completed: {'online' if status else 'offline'}",
+            LogLevel.INFO if status else LogLevel.WARNING,
+            {
+                "status": status,
+                "response_data": data,
+                "status_code": response.status_code
+            }
+        )
+        
+        return status
+        
     except requests.RequestException as e:
-        raise Exception(f"Error connecting to printer service: {e}")
+        error_msg = f"Error connecting to printer service: {e}"
+        logger.error(error_msg)
+        structured_logger.log_print_event(
+            "Failed to connect to printer service",
+            LogLevel.ERROR,
+            {
+                "error": str(e),
+                "error_type": type(e).__name__,
+                "printer_url": printer_url
+            }
+        )
+        raise Exception(error_msg)
+    except Exception as e:
+        error_msg = f"Unexpected error checking printer status: {e}"
+        logger.error(error_msg)
+        structured_logger.log_print_event(
+            "Unexpected error during printer status check",
+            LogLevel.ERROR,
+            {
+                "error": str(e),
+                "error_type": type(e).__name__
+            }
+        )
+        raise Exception(error_msg)