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 import FastAPI
 from fastapi.middleware.cors import CORSMiddleware
 from fastapi.middleware.cors import CORSMiddleware
 from starlette.middleware.sessions import SessionMiddleware
 from starlette.middleware.sessions import SessionMiddleware
-from config.settings import SECRET_KEY
+from config.settings import DEVELOPMENT, SECRET_KEY
 from routes import sales
 from routes import sales
 from services.email_service import initialize_email_sender
 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:
 def create_app() -> FastAPI:
     """Create and configure FastAPI application"""
     """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):
 def setup_routes(app: FastAPI):
     """Setup all application routes"""
     """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 datetime import datetime, timedelta
 from typing import Union
 from typing import Union
-from venv import logger
 from fastapi import Depends, HTTPException
 from fastapi import Depends, HTTPException
 from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
 from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
 from logging import getLogger
 from logging import getLogger
@@ -12,7 +11,7 @@ from passlib.context import CryptContext
 
 
 from models.user import User
 from models.user import User
 from services.data_service import UserDataService
 from services.data_service import UserDataService
-
+from services.logging_service import structured_logger, LogLevel
 
 
 logger = getLogger(__name__)
 logger = getLogger(__name__)
 
 
@@ -30,13 +29,43 @@ def hash_password(password: str) -> str:
     """Hash a password using bcrypt."""
     """Hash a password using bcrypt."""
     return pwd_context.hash(password)
     return pwd_context.hash(password)
 
 
-def generate_token(email: str):
+def generate_token(email: str): 
     """Generate a JWT token for user authentication."""
     """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:
 def create_access_token(data: dict, expires_delta: timedelta) -> str:
     """Create a JWT access token."""
     """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:
 def authenticate_user(email: str, password: str) -> bool:
     """Authenticate a user by email and password."""
     """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 False
-    return True
 
 
 async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> User:
 async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> User:
     credentials_exception = HTTPException(
     credentials_exception = HTTPException(
@@ -67,20 +138,86 @@ async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(s
     
     
     try:
     try:
         token = credentials.credentials
         token = credentials.credentials
-        logger.debug(f"Decoding token: {token}")
+        logger.debug(f"Decoding token: {token[:20]}...")
+        
         payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
         payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
         email = payload.get("sub")
         email = payload.get("sub")
+        
         if email is None:
         if email is None:
             logger.error("Token does not contain email")
             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
             raise credentials_exception
+            
         token_data = TokenData(email=email)
         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
         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
         raise credentials_exception
-    return user
 
 

+ 2 - 1
config/messages.py

@@ -27,7 +27,7 @@ class SuccessResponse:
     PRODUCT_EDIT_SUCCESS = "Producto editado exitosamente."
     PRODUCT_EDIT_SUCCESS = "Producto editado exitosamente."
     PRODUCT_CREATE_SUCCESS = "Producto creado exitosamente."
     PRODUCT_CREATE_SUCCESS = "Producto creado exitosamente."
     PRODUCT_DELETE_SUCCESS = "Producto eliminado exitosamente."
     PRODUCT_DELETE_SUCCESS = "Producto eliminado exitosamente."
-
+    REWARD_SUCCESS = "Recompensa aplicada exitosamente."
 class UserResponse:
 class UserResponse:
     """Class to handle user-related messages in the response."""
     """Class to handle user-related messages in the response."""
 
 
@@ -37,3 +37,4 @@ class UserResponse:
     USER_ALREADY_EXISTS = "El usuario ya está registrado."
     USER_ALREADY_EXISTS = "El usuario ya está registrado."
     USER_FORMAT_BLOCKED = "Demasiados intentos de inicio de sesión. Usuario bloqueado por {time}."
     USER_FORMAT_BLOCKED = "Demasiados intentos de inicio de sesión. Usuario bloqueado por {time}."
     NOT_PERMITTED = "No tienes permisos para realizar esta acción."
     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"
 APPNAME = "Pedidos Express"
-MAIL = os.getenv("MAIL","")
+MAIL = os.getenv("MAIL_DIRECTION","")
 MAIL_PASSWORD = os.getenv("MAIL_PASSWORD","")
 MAIL_PASSWORD = os.getenv("MAIL_PASSWORD","")
 LOGS_FOLDER = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'logs')
 LOGS_FOLDER = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'logs')
 if not os.path.exists(LOGS_FOLDER):
 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
 # Data paths
 BG_DATA_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)),'data', 'llm_data.json')
 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')
 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():
 def validate_config():
     logger = logging.getLogger(__name__)
     logger = logging.getLogger(__name__)
     """Validate configuration and show warnings"""
     """Validate configuration and show warnings"""
     if SECRET_KEY == "your_very_very_secret_key_for_signing_cookies_python_v2":
     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.")
         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:
     if not OPENAI_API_KEY:
         logger.critical("CRITICAL ERROR: OPENAI_API_KEY environment variable not set. The application will not work correctly.")
         logger.critical("CRITICAL ERROR: OPENAI_API_KEY environment variable not set. The application will not work correctly.")
         return False
         return False
     return True
     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 os
 import uvicorn
 import uvicorn
 from app import create_app, setup_routes
 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 logging import getLogger
 from threading import Thread
 from threading import Thread
+from services.logging_service import structured_logger, LogLevel
 
 
 from services.data_service import initialize_db
 from services.data_service import initialize_db
 
 
@@ -11,38 +12,127 @@ logger = getLogger("main")
 
 
 def main():
 def main():
     """Main application entry point"""
     """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()
 app = main()
 
 
 if not app:
 if not app:
     logger.critical("FATAL: Failed to create FastAPI 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)
     exit(1)
 
 
+logger.info("Application initialized successfully")
 
 
 if __name__ == "__main__":
 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)
     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
     image: Optional[str] = None
     status: int = 1  # 0: Inactive, 1: Active
     status: int = 1  # 0: Inactive, 1: Active
     quantity: Optional[int] = 1  # Optional quantity for the product
     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):
 class ProductEditRequest(BaseModel):
     """Request model for editing a product"""
     """Request model for editing a product"""
@@ -49,4 +49,24 @@ class ProductCreateRequest(BaseModel):
     price: int
     price: int
     image: str
     image: str
     status: Optional[int] = 1  # 0: Inactive, 1: Active
     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
 from models.items import Product
 
 
+
+class Promotion(BaseModel):
+    id: Optional[int] = None
+    price: Optional[float] = None
+
 class ItemWeb(BaseModel):
 class ItemWeb(BaseModel):
     id: int
     id: int
     price: int
     price: int
     quantity: int
     quantity: int
+    promotion: Optional[Promotion] = None
 
 
 
 
 class OrderWeb(BaseModel):
 class OrderWeb(BaseModel):

+ 3 - 0
models/user.py

@@ -9,6 +9,9 @@ class RegisterUserRequest(BaseModel):
     email: str
     email: str
     rut: str
     rut: str
 
 
+class UserRewardRequest(BaseModel):
+    tableNumber: int
+
 class PinUserRequest(BaseModel):
 class PinUserRequest(BaseModel):
     pin: str = Field(min_length=4, max_length=4, description="4-digit PIN for user authentication")
     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% {
       100% {
         transform: scale(0) translateX(-50%);
         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>
   <script src="express/js/app.js" type="module"></script>
   <link rel="stylesheet" href="express/styles.css">
   <link rel="stylesheet" href="express/styles.css">
 </head>
 </head>
-
 <body class="h-[100dvh] max-h-[100dvh] flex flex-col bg-gray-50 overflow-x-hidden"
 <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;'>
       style='font-family:"Spline Sans","Noto Sans",sans-serif;'>
 
 
@@ -44,46 +43,116 @@
   <!-- ---------- MAIN  ---------- -->
   <!-- ---------- MAIN  ---------- -->
   <main class="relative flex-1 flex flex-col min-h-0 overflow-x-hidden">  
   <main class="relative flex-1 flex flex-col min-h-0 overflow-x-hidden">  
     <!-- ===== MENÚ tab ===== -->
     <!-- ===== 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>
+        
+        <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 ===== -->
     <!-- ===== CHAT ===== -->
     <section id="chatTab" data-index="2" data-tab class="flex hidden flex-col flex-1 min-h-0">
     <section id="chatTab" data-index="2" data-tab class="flex hidden flex-col flex-1 min-h-0">
@@ -237,10 +306,10 @@
               origin-left">
               origin-left">
   </div>
   </div>
 
 
-  
+  <!-- MODALS -->
   <!-- === MODAL INICIO DE SESIÓN === -->
   <!-- === MODAL INICIO DE SESIÓN === -->
 <div id="sessionModal"
 <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">
   <form id="loginForm" class="bg-white w-full max-w-md p-8 rounded-xl shadow-xl space-y-6">
     <div class="text-center">
     <div class="text-center">
       <h2 class="text-2xl font-bold text-gray-900">¡Bienvenido!</h2>
       <h2 class="text-2xl font-bold text-gray-900">¡Bienvenido!</h2>
@@ -299,6 +368,185 @@
   </form>
   </form>
 </div>
 </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 ---------- -->
               <!-- ---------- JS: conmutar tabs + toast ---------- -->
   <script>
   <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 { getProducts, sendOrder } from './service/product.js';
 import { login } from './service/auth.js'
 import { login } from './service/auth.js'
 import { createGlobalLoader, showGlobalLoader, hideGlobalLoader } from './utils/loader.js';
 import { createGlobalLoader, showGlobalLoader, hideGlobalLoader } from './utils/loader.js';
+import { updateProgress, claimReward } from './utils/progressBar.js';
 import { showError } from './utils/error.js';
 import { showError } from './utils/error.js';
 import { addHistoryRow, setupShoppingCart } from './utils/shoppingCart.js';
 import { addHistoryRow, setupShoppingCart } from './utils/shoppingCart.js';
+import { hideGUI, showGUI } from './utils/gui.js';
 // --- Variables de Usuario ---
 // --- Variables de Usuario ---
 let userId = -1;
 let userId = -1;
 let userName = "Cliente";
 let userName = "Cliente";
@@ -45,6 +47,7 @@ async function initializeApp() {
     showGlobalLoader("Cargando productos...");
     showGlobalLoader("Cargando productos...");
     setupShoppingCart(userId, userToken,userName);
     setupShoppingCart(userId, userToken,userName);
     await renderProducts();
     await renderProducts();
+    showGUI();
     hideGlobalLoader();
     hideGlobalLoader();
 
 
     const chatSuggestions = Array.from(chatSuggestionsElement.children);
     const chatSuggestions = Array.from(chatSuggestionsElement.children);
@@ -60,8 +63,10 @@ function initializeLoginModal() {
     const sessionModal = document.getElementById('sessionModal');
     const sessionModal = document.getElementById('sessionModal');
     const loginForm = document.getElementById('loginForm');
     const loginForm = document.getElementById('loginForm');
     sessionModal.classList.remove('hidden');
     sessionModal.classList.remove('hidden');
+
     loginForm.addEventListener('submit', async (event) => {
     loginForm.addEventListener('submit', async (event) => {
         event.preventDefault();
         event.preventDefault();
+        event.stopPropagation();
 
 
         const fd = new FormData(loginForm);
         const fd = new FormData(loginForm);
         const email = fd.get('email').trim();
         const email = fd.get('email').trim();
@@ -77,6 +82,7 @@ function initializeLoginModal() {
             userToken = data.token;
             userToken = data.token;
             userName = data.name;
             userName = data.name;
             userId = data.id;
             userId = data.id;
+            updateProgress(data.reward_progress || 0);
             if (!userToken || data.id === undefined) {
             if (!userToken || data.id === undefined) {
                 showError("Error al iniciar sesión.");
                 showError("Error al iniciar sesión.");
                 return;
                 return;
@@ -86,11 +92,10 @@ function initializeLoginModal() {
 
 
             initializeApp();
             initializeApp();
         }catch (error) {
         }catch (error) {
-        }
-
-    });
+            console.error(error)
+    }
+})
 }
 }
-
 function initializeChat() {
 function initializeChat() {
     if (!chatForm) return;
     if (!chatForm) return;
     chatForm.addEventListener("submit", (event) => {
     chatForm.addEventListener("submit", (event) => {
@@ -111,7 +116,7 @@ function initializeChat() {
 function setupBasicListeners() {
 function setupBasicListeners() {
     if (!checkoutButton) return;
     if (!checkoutButton) return;
     checkoutButton.addEventListener("click", processOrder);
     checkoutButton.addEventListener("click", processOrder);
-
+    initializeRewards();
 }
 }
 //#endregion
 //#endregion
 //#region ===== Utilidad =====
 //#region ===== Utilidad =====
@@ -305,7 +310,10 @@ async function processOrder() {
             totalAmount: cart.reduce((sum, item) => sum + item.price * item.quantity, 0),
             totalAmount: cart.reduce((sum, item) => sum + item.price * item.quantity, 0),
             orderDate: new Date().toLocaleString('sv-SE').replace(' ', 'T')
             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.");
         alert("Pedido enviado con éxito.");
         cart.forEach(item => {
         cart.forEach(item => {
             addHistoryRow({
             addHistoryRow({
@@ -387,12 +395,151 @@ async function sendMessageToAI() {
 }
 }
 
 
 //#endregion
 //#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 ---
 // --- APP initialization ---
 document.addEventListener("DOMContentLoaded", async () => {
 document.addEventListener("DOMContentLoaded", async () => {
-
     createGlobalLoader();
     createGlobalLoader();
     initializeLoginModal();
     initializeLoginModal();
+    hideGUI();
     // initializeApp()
     // 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.");
     showError("Error al iniciar sesión, Intenta mas tarde.");
     throw new Error("Error al iniciar sesión.");
     throw new Error("Error al iniciar sesión.");
   }
   }
+  console.log("Inicio de sesión exitoso:", data);
   return {data: data.data};
   return {data: data.data};
 }
 }
 
 

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

@@ -21,6 +21,29 @@ async function sendOrder(order, token) {
     throw error;
     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){
 async function getProducts(token){
   const response = await fetch("/api/products?status=1", {
   const response = await fetch("/api/products?status=1", {
     headers: {
     headers: {

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

@@ -1,6 +1,42 @@
 import { showError } from "../utils/error.js";
 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) {
 async function fetchUserSales(userId, token) {
   try {
   try {
     const response = await fetch(`/api/sales/user/${userId}`, {
     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)
         body: JSON.stringify(data)
     })
     })
         .then(response => {
         .then(response => {
-            if (!response.ok) {
+            if (response.status === 400) {
                 return response.json().then(errorData => {
                 return response.json().then(errorData => {
                     showErrorMessage(errorData.message || 'Error al registrar el usuario.');
                     showErrorMessage(errorData.message || 'Error al registrar el usuario.');
                 });
                 });
@@ -59,9 +59,14 @@ document.getElementById('registerForm').addEventListener('submit', function (e)
             return response.json();
             return response.json();
         })
         })
         .then(data => {
         .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(() => {
         }).finally(() => {
             hideGlobalLoader();
             hideGlobalLoader();
         })
         })

+ 78 - 4
routes/chat.py

@@ -4,11 +4,12 @@ from httpx import get
 from models.chat import ChatCompletionRequest
 from models.chat import ChatCompletionRequest
 from models.user import User
 from models.user import User
 from services.openai_service.openai_service import generate_completion
 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
 from auth.security import get_current_user
 import logging
 import logging
 from fastapi import APIRouter
 from fastapi import APIRouter
 from config.messages import SuccessResponse
 from config.messages import SuccessResponse
+
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
 chat_router = APIRouter()
 chat_router = APIRouter()
@@ -19,13 +20,86 @@ async def chat_completions(request_data: ChatCompletionRequest, request: Request
     """Get chat completions from OpenAI"""
     """Get chat completions from OpenAI"""
     # Uses session_token (which is the antiAbuseToken) as an identifier for logging
     # Uses session_token (which is the antiAbuseToken) as an identifier for logging
     session_identifier = request.session.get("antiAbuseToken", "unknown_session")
     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:
     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)
         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})
         return JSONResponse({"response": openai_response, "message": SuccessResponse.CHAT_RESPONSE_SUCCESS})
+        
     except HTTPException as e:
     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:
     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.")
         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
 import time
+from logging import getLogger
+from threading import Thread
 from uuid import uuid4
 from uuid import uuid4
-from fastapi import HTTPException, APIRouter
+
+from fastapi import HTTPException, APIRouter, Depends
 from fastapi.responses import JSONResponse
 from fastapi.responses import JSONResponse
+
 from fudo import fudo
 from fudo import fudo
 from models.sales import ItemWeb, OrderWeb
 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.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 services.data_service import DataServiceFactory
 from config.mails import PRINTER_DISCONNECTED_MAIL
 from config.mails import PRINTER_DISCONNECTED_MAIL
 from config.messages import ErrorResponse, SuccessResponse, UserResponse
 from config.messages import ErrorResponse, SuccessResponse, UserResponse
 from config.settings import DEVELOPMENT
 from config.settings import DEVELOPMENT
-from models.items import Item, Order
+from auth.security import get_current_user
+
 logger = getLogger(__name__)
 logger = getLogger(__name__)
+
+# Data services initialization
 user_data_service = DataServiceFactory.get_user_service()
 user_data_service = DataServiceFactory.get_user_service()
 product_data_service = DataServiceFactory.get_product_service()
 product_data_service = DataServiceFactory.get_product_service()
 sale_data_service = DataServiceFactory.get_sales_service()
 sale_data_service = DataServiceFactory.get_sales_service()
+
+# Global variables
 printer_orders = []
 printer_orders = []
 order_router = APIRouter()
 order_router = APIRouter()
 
 
+
 @order_router.post("/send")
 @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"""
     """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 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
     items = order.items
     table = order.table
     table = order.table
 
 
+    # Input validation
     if not items or not table:
     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})
         return JSONResponse(status_code=400, content={"message": ErrorResponse.MISSING_FIELDS})
 
 
     if not isinstance(table, int):
     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})
         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
     # Add products to Fudo
     product_errors = []
     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()
         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:
     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(
         return JSONResponse(
             status_code=424, 
             status_code=424, 
             content={"message": ErrorResponse.PRODUCT_ADD_ERROR, "errors": product_errors}
             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,
             table=table,
             items=[Item(name=product.name, price=product.price or 0, quantity=item.quantity) for product, item in zip(products, items)],
             items=[Item(name=product.name, price=product.price or 0, quantity=item.quantity) for product, item in zip(products, items)],
             customerName=user.name,
             customerName=user.name,
             totalAmount=order.totalAmount,
             totalAmount=order.totalAmount,
             orderDate=order.orderDate
             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
 Product Routes Module
 
 
@@ -25,10 +27,11 @@ from h11 import Data
 
 
 # Local imports
 # Local imports
 from auth.security import get_current_user
 from auth.security import get_current_user
-from models import user
+from models.user import User
 from models.items import Product, ProductCreateRequest, ProductEditRequest
 from models.items import Product, ProductCreateRequest, ProductEditRequest
 from services.data_service import DataServiceFactory
 from services.data_service import DataServiceFactory
 from config.messages import ErrorResponse, SuccessResponse, UserResponse
 from config.messages import ErrorResponse, SuccessResponse, UserResponse
+from services.print_service import print_ticket
 
 
 # Initialize logger for this module
 # Initialize logger for this module
 logger = getLogger(__name__)
 logger = getLogger(__name__)
@@ -44,11 +47,14 @@ def apply_promo_price(product: Product):
     """Apply promotional price to a product if applicable."""
     """Apply promotional price to a product if applicable."""
     #dia de la semana 1-7
     #dia de la semana 1-7
     day_of_week = time.localtime().tm_wday + 1  # Convert to 1-7 range
     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:
     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("/")
 @product_router.get("/")
 async def get_products(status: Optional[int] = Query(None), current_user = Depends(get_current_user)):
 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
     # Retrieve all products and convert to dictionary format
     all_products =  product_data_service.get_all()
     all_products =  product_data_service.get_all()
     all_products = list(map(apply_promo_price, all_products))
     all_products = list(map(apply_promo_price, all_products))
-    all_products = [product.model_dump() for product in all_products]   
 
 
 
 
     if status is not None:
     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 404 if product not found
     return JSONResponse({"message": UserResponse.USER_NOT_FOUND.format(user_id=product_id)}, status_code=404)
     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)
 # MODERATE RISK OPERATIONS - Requires permissions >= 1 (Manager level or above)
 
 
 @product_router.patch("/{product_id}/edit")
 @product_router.patch("/{product_id}/edit")

+ 340 - 80
routes/users.py

@@ -1,27 +1,25 @@
-from email.policy import HTTP
+
 import json
 import json
 from logging import getLogger
 from logging import getLogger
-from math import log
-from os import name
 from uuid import uuid4
 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
 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 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 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.mails import REGISTER_MAIL
-from config.settings import APPNAME
 from config.messages import ErrorResponse, SuccessResponse, UserResponse
 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
 from utils.rut import validate_rut
+
 fernet = Fernet(PIN_KEY.encode())
 fernet = Fernet(PIN_KEY.encode())
 logger = getLogger(__name__)
 logger = getLogger(__name__)
 user_data_service = UserDataService()
 user_data_service = UserDataService()
@@ -47,30 +45,149 @@ async def exists_user(request: UserIDRequest):
 @user_router.post("/register")
 @user_router.post("/register")
 async def register_user(request: RegisterUserRequest):
 async def register_user(request: RegisterUserRequest):
     """Register a new user"""
     """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):
     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})
         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}")
     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")
 @user_router.post("/create-user")
 async def create_user(request: PinUserRequest, q: str):
 async def create_user(request: PinUserRequest, q: str):
@@ -103,55 +220,176 @@ async def create_user(request: PinUserRequest, q: str):
 @user_router.post("/login")
 @user_router.post("/login")
 async def login_user(request: LoginRequest, http_request: Request):
 async def login_user(request: LoginRequest, http_request: Request):
     """Login user with email and PIN"""
     """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:
         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")
 @user_router.delete("/delete")
 async def delete_user(request: UserIDRequest):
 async def delete_user(request: UserIDRequest):
@@ -162,6 +400,28 @@ async def delete_user(request: UserIDRequest):
     else:
     else:
         return JSONResponse(status_code=404, content={"message": UserResponse.USER_NOT_FOUND})
         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")
 @user_router.get("/all")
 async def get_all_users():
 async def get_all_users():
     """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 config.settings import POSTGRESQL_DB_CONFIG
 from typing import List, Dict, Optional, Any
 from typing import List, Dict, Optional, Any
 from abc import ABC, abstractmethod
 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 logging import getLogger
 from datetime import datetime
 from datetime import datetime
 from cryptography.fernet import Fernet
 from cryptography.fernet import Fernet
@@ -1044,7 +1044,10 @@ class SalesDataService(BaseDataService):
                 price=product[4],
                 price=product[4],
                 image=product[5],
                 image=product[5],
                 status=product[6],
                 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
             ) for product in products
         ]
         ]
     
     

+ 173 - 42
services/email_service.py

@@ -1,74 +1,205 @@
+import email
 from config.settings import MAIL, MAIL_PASSWORD
 from config.settings import MAIL, MAIL_PASSWORD
 import smtplib
 import smtplib
 from email.message import EmailMessage
 from email.message import EmailMessage
 from logging import getLogger
 from logging import getLogger
-from contextlib import contextmanager
 from typing import Optional
 from typing import Optional
-logger = getLogger(__name__)
+from services.logging_service import structured_logger, LogLevel
 
 
-email_sender = None
+logger = getLogger(__name__)
 class EmailSender:
 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
         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:
         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:
             try:
                 self._smtp = smtplib.SMTP_SSL('smtp.gmail.com', 465)
                 self._smtp = smtplib.SMTP_SSL('smtp.gmail.com', 465)
                 self._smtp.login(self.email, self.password)
                 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:
             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
                 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:
         if self._smtp:
+            logger.info("Closing SMTP connection.")
+            structured_logger.log_email_event(
+                "Closing SMTP connection",
+                LogLevel.INFO,
+                {"sender_email": self.email}
+            )
             try:
             try:
-                logger.info("Closing SMTP connection.")
                 self._smtp.quit()
                 self._smtp.quit()
-                logger.info("SMTP connection closed.")
+                self._smtp = None
             except Exception as e:
             except Exception as e:
                 logger.warning(f"Error closing SMTP connection: {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):
     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:
         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:
         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
             raise
 
 
+email_sender: Optional[EmailSender] = None
 
 
 def initialize_email_sender():
 def initialize_email_sender():
     global 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 csv
 import os
 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
 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]):
 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 = 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):
 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
 import requests
+from logging import getLogger
+from config.settings import DEVELOPMENT
 from models.sales import OrderWeb, ItemWeb
 from models.sales import OrderWeb, ItemWeb
 from services.data_service import DataServiceFactory
 from services.data_service import DataServiceFactory
 from models.items import Order
 from models.items import Order
+from services.logging_service import structured_logger, LogLevel
+
+logger = getLogger(__name__)
 user_data_service = DataServiceFactory.get_user_service()
 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"""
     """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():
 def get_status():
     """Get the status of the printer service"""
     """Get the status of the printer service"""
+    logger.info("Checking printer service status")
+    
     try:
     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()
         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:
     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)