Переглянути джерело

Merge pull request #3 from latapp/login_implement

Login implement
Erwin Jacimino 9 місяців тому
батько
коміт
23dc8f0606
51 змінених файлів з 4109 додано та 727 видалено
  1. 3 1
      .env
  2. 2 1
      .gitignore
  3. 348 0
      README.md
  4. 49 0
      app.py
  5. 1 0
      auth/__init__.py
  6. 86 0
      auth/security.py
  7. 1 0
      config/__init__.py
  8. 262 0
      config/mails.py
  9. 51 0
      config/settings.py
  10. BIN
      data/data.db
  11. 7 0
      data/feedback.json
  12. 0 0
      data/llm_data.json
  13. 17 3
      data/products.json
  14. 98 114
      fudo/fudo.py
  15. 27 1
      impresora/printer.py
  16. 731 0
      logs/app.log
  17. 39 368
      main.py
  18. 22 0
      models/__init__.py
  19. 10 0
      models/blacklist.py
  20. 13 0
      models/chat.py
  21. 18 0
      models/items.py
  22. 45 0
      models/sells.py
  23. 25 0
      models/user.py
  24. 0 0
      pin.key
  25. 0 98
      public/js/service.js
  26. 0 0
      public/main/assets/summer.jpeg
  27. 54 19
      public/main/index.html
  28. 173 122
      public/main/js/app.js
  29. 33 0
      public/main/js/service/auth.js
  30. 30 0
      public/main/js/service/chat.js
  31. 39 0
      public/main/js/service/product.js
  32. 0 0
      public/main/styles.css
  33. 62 0
      public/register/app.js
  34. 74 0
      public/register/index.html
  35. 0 0
      public/register/styles.css
  36. 1 0
      routes/__init__.py
  37. 29 0
      routes/chat.py
  38. 98 0
      routes/orders.py
  39. 19 0
      routes/products.py
  40. 2 0
      routes/sells.py
  41. 27 0
      routes/static.py
  42. 105 0
      routes/users.py
  43. 1 0
      services/__init__.py
  44. 1184 0
      services/data_service.py
  45. 30 0
      services/email_service.py
  46. 25 0
      services/fudo_service.py
  47. 28 0
      services/logging_service.py
  48. 0 0
      services/openai_service/__init__.py
  49. 74 0
      services/openai_service/openai_service.py
  50. 53 0
      services/openai_service/openai_tools.py
  51. 113 0
      test_models.py

+ 3 - 1
.env

@@ -3,4 +3,6 @@ SECRET_KEY = 866B3F5EE90BFED7EDAD0FCB0A9C0FC866F03166C05648478ECEF6148C9E13BEBF8
 OPENAI_API_KEY = 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
 NODE_ENV = development
 FUDO_API_KEY=NzZAMTEzMzc4
-FUDO_API_SECRET=FNKYiEbYGTVc3i0jLOTTXL8pVPkUIPLP
+FUDO_API_SECRET=FNKYiEbYGTVc3i0jLOTTXL8pVPkUIPLP
+PIN_KEY='GZUTI02vzKsRM2LLePkDy2mh8YpI5TjDEffRfkRNWLE='
+LOG_LEVEL=DEBUG

+ 2 - 1
.gitignore

@@ -7,4 +7,5 @@ users.json
 logs.csv
 llm_logs.*
 *.pyc
-dksdabjhvjhSADhsbjksf.txt
+dksdabjhvjhSADhsbjksf.txt
+data/data.db

+ 348 - 0
README.md

@@ -0,0 +1,348 @@
+# Biergarten Klein - Sistema de Pedidos Express
+
+## Descripción del Proyecto
+
+**Biergarten Klein** es una aplicación web completa para la gestión de pedidos en un bar cervecero artesanal. El sistema permite a los clientes interactuar con un asistente de IA llamado "Camilo Klein" para consultar el menú, recibir recomendaciones de cervezas artesanales, realizar pedidos y gestionar su experiencia en el establecimiento.
+
+## Características Principales
+
+### 🤖 Asistente de IA Personalizado
+- **Camilo Klein**: Chatbot especializado en el menú del bar
+- Integración con OpenAI GPT-4o-mini
+- Respuestas carismáticas con emojis
+- Base de conocimientos específica sobre cervezas artesanales
+- Sistema anti-abuso con tokens de sesión
+
+### 🍺 Gestión de Productos
+- Catálogo completo de cervezas artesanales (Burlesque, Queen Burlesque, Hoppy Mosh, Black Mamba, etc.)
+- Sistema de carrito de compras interactivo
+- Información detallada de cada cerveza (IBU, SRM, notas de sabor)
+- Precios y disponibilidad en tiempo real
+
+### 🧾 Sistema de Pedidos
+- Procesamiento de pedidos con validación
+- Integración con impresora térmica USB
+- Sincronización con sistema Fudo POS
+- Notificaciones por email en caso de errores
+- Logging completo de transacciones
+
+### 🔐 Seguridad y Autenticación
+- Sistema de tokens anti-abuso
+- Validación de usuarios
+- Protección de endpoints sensibles
+- Middleware de sesiones seguras
+
+## Estructura del Proyecto
+
+```
+pedidos_express/
+├── main.py                    # Punto de entrada principal
+├── app.py                     # Configuración FastAPI y rutas
+├── requirements.txt           # Dependencias del proyecto
+├── tailwind.config.js         # Configuración de Tailwind CSS
+├── config/
+│   ├── __init__.py
+│   └── settings.py           # Variables de entorno y logging
+├── models/
+│   ├── __init__.py
+│   └── schemas.py            # Modelos Pydantic (Message, Order, User)
+├── auth/
+│   ├── __init__.py
+│   └── security.py           # Autenticación y tokens anti-abuso
+├── services/
+│   ├── __init__.py
+│   ├── data_service.py       # Gestión de datos y productos
+│   ├── openai_service.py     # Integración con OpenAI
+│   ├── email_service.py      # Notificaciones por email
+│   ├── fudo_service.py       # Integración con Fudo POS
+│   └── logging_service.py    # Sistema de logs
+├── routes/
+│   ├── __init__.py
+│   ├── chat.py              # Endpoints del chatbot
+│   ├── users.py             # Gestión de usuarios
+│   ├── products.py          # Catálogo de productos
+│   ├── orders.py            # Procesamiento de pedidos
+│   └── static.py            # Archivos estáticos
+├── impresora/               # Módulo de impresión térmica
+│   ├── __init__.py
+│   ├── order.py             # Modelos para órdenes de impresión
+│   └── printer.py           # Control de impresora USB
+├── fudo/                    # Integración con Fudo POS
+│   └── fudo.py              # API y gestión de tokens
+├── data/                    # Almacenamiento de datos
+│   ├── data.db              # Base de datos SQLite
+│   ├── llm_data.json        # Base de conocimientos para IA
+│   └── products.json        # Catálogo de productos
+├── public/                  # Frontend web
+│   ├── index.html           # Página principal
+│   ├── styles.css           # Estilos personalizados
+│   ├── assets/              # Recursos multimedia
+│   └── js/
+│       ├── app.js           # Lógica principal del frontend
+│       ├── interfaces.js    # Interfaces de usuario
+│       └── service/         # Servicios del frontend
+│           ├── auth.js      # Autenticación cliente
+│           ├── chat.js      # Comunicación con chatbot
+│           └── product.js   # Gestión de productos
+└── logs/                    # Archivos de log
+    └── app.log              # Logs de la aplicación
+```
+
+## Tecnologías Utilizadas
+
+### Backend
+- **FastAPI**: Framework web moderno y rápido
+- **Python 3.9+**: Lenguaje de programación principal
+- **OpenAI API**: Integración con GPT-4o-mini para el chatbot
+- **SQLite**: Base de datos local
+- **Redis**: Cache para tokens de autenticación
+- **Uvicorn**: Servidor ASGI para producción
+
+### Frontend
+- **HTML5/CSS3/JavaScript**: Tecnologías web estándar
+- **Tailwind CSS**: Framework de estilos utilitarios
+- **Vanilla JavaScript**: Sin frameworks adicionales para máximo rendimiento
+
+### Integraciones
+- **Fudo POS**: Sistema de punto de venta
+- **Impresora Térmica USB**: Para tickets de pedidos
+- **SMTP**: Envío de notificaciones por email
+
+## Módulos Principales
+
+### config/settings.py
+- Configuración centralizada de la aplicación
+- Gestión de variables de entorno
+- Sistema de logging configurado
+- Validación de configuración crítica
+
+### models/schemas.py
+- **Message**: Estructura para mensajes del chat
+- **ChatCompletionRequest**: Requests para el chatbot
+- **OrderWeb**: Modelo completo de pedidos
+- **ItemWeb**: Elementos individuales del pedido
+- **UserCodeRequest**: Validación de códigos de usuario
+
+### auth/security.py
+- **Sistema de tokens anti-abuso**: Protección contra uso excesivo
+- **Validación de sesiones**: Control de acceso a endpoints
+- **Middleware de seguridad**: Protección de rutas sensibles
+- **Generación segura de tokens**: Utilizando secrets
+
+### services/
+- **data_service.py**: Carga y gestión de productos, usuarios y datos del menú
+- **openai_service.py**: Integración completa con OpenAI para el asistente Camilo Klein
+- **email_service.py**: Sistema de notificaciones por correo electrónico
+- **fudo_service.py**: Integración bidireccional con el sistema Fudo POS
+- **logging_service.py**: Registro detallado de pedidos y respuestas del LLM
+
+### routes/
+- **chat.py**: 
+  - `/api/chat/init-chat` - Inicialización del chat y generación de tokens
+  - `/api/chat/completions` - Procesamiento de mensajes del chatbot
+- **users.py**: 
+  - `/api/existsUser` - Validación de códigos de usuario
+- **products.py**: 
+  - `/api/get_products` - Obtención del catálogo completo
+- **orders.py**: 
+  - `/api/printer/order` - Procesamiento e impresión de pedidos
+- **static.py**: 
+  - Servir archivos estáticos y página principal
+
+## API Endpoints
+
+### Chat y Asistente IA
+```
+GET  /api/chat/init-chat       # Inicializar sesión de chat
+POST /api/chat/completions     # Enviar mensaje al asistente
+```
+
+### Gestión de Productos
+```
+GET  /api/get_products         # Obtener catálogo de productos
+```
+
+### Gestión de Pedidos
+```
+POST /api/printer/order        # Procesar y imprimir pedido
+```
+
+### Usuarios
+```
+POST /api/existsUser          # Validar código de usuario
+```
+
+## Flujo de la Aplicación
+
+1. **Inicialización**: El usuario accede a la página principal
+2. **Autenticación**: Se genera un token anti-abuso para la sesión
+3. **Exploración**: El usuario puede consultar productos y chatear con Camilo Klein
+4. **Pedido**: Selección de productos y agregado al carrito
+5. **Procesamiento**: Validación, impresión y sincronización con Fudo POS
+6. **Confirmación**: Notificación al usuario y registro en logs
+
+## Base de Conocimientos del Asistente
+
+El asistente Camilo Klein utiliza una base de datos especializada (`llm_data.json`) que incluye:
+
+- **Información del establecimiento**: Historia y concepto del Biergarten Klein
+- **Catálogo completo de cervezas**: 15+ variedades artesanales con detalles técnicos
+- **Recomendaciones especializadas**: Sugerencias basadas en preferencias
+- **Información nutricional y técnica**: IBU, SRM, graduación alcohólica
+- **Horarios y ubicación**: Datos prácticos para los clientes
+
+## Características Técnicas Avanzadas
+
+### Sistema de Logging
+- Registro detallado de todas las interacciones
+- Logs estructurados con timestamps
+- Separación por niveles (INFO, WARNING, ERROR)
+- Rotación automática de archivos de log
+
+### Integración con Hardware
+- **Impresora térmica USB**: Detección automática y manejo de errores
+- **Códigos de barras**: Generación para tracking de pedidos
+- **Notificaciones automáticas**: Email en caso de fallos de hardware
+
+### Gestión de Estado
+- **Redis para cache**: Tokens de autenticación y sesiones
+- **SQLite para persistencia**: Datos permanentes y históricos
+- **JSON para configuración**: Datos del menú y configuraciones
+
+## Ventajas de la Arquitectura
+
+1. **Modularidad**: Cada componente tiene responsabilidades específicas
+2. **Escalabilidad**: Fácil agregar nuevas funcionalidades
+3. **Mantenibilidad**: Código organizado y bien documentado
+4. **Testabilidad**: Componentes aislados para pruebas unitarias
+5. **Seguridad**: Múltiples capas de protección
+6. **Performance**: Optimizado para respuestas rápidas
+
+## Instalación y Configuración
+
+### Prerequisitos
+```bash
+Python 3.9+
+Redis Server
+Impresora térmica USB (opcional)
+```
+
+### Instalación
+```bash
+# Clonar el repositorio
+git clone [repository-url]
+cd pedidos_express
+
+# Instalar dependencias
+pip install -r requirements.txt
+
+# Configurar variables de entorno
+cp .env.example .env
+```
+
+### Variables de Entorno Requeridas
+```env
+# OpenAI Configuration
+OPENAI_API_KEY=tu_api_key_de_openai
+
+# Security
+SECRET_KEY=tu_clave_secreta_muy_segura
+
+# Server Configuration  
+PORT=6001
+LOG_LEVEL=INFO
+
+# Fudo POS Integration
+FUDO_API_KEY=tu_api_key_fudo
+FUDO_API_SECRET=tu_api_secret_fudo
+
+# Redis Configuration
+REDIS_HOST=localhost
+REDIS_PORT=6379
+REDIS_DB=0
+
+# Email Configuration (opcional)
+SMTP_HOST=smtp.gmail.com
+SMTP_PORT=587
+SMTP_USER=tu_email@gmail.com
+SMTP_PASSWORD=tu_password_de_app
+```
+
+## Ejecución
+
+### Desarrollo
+```bash
+python main.py
+```
+
+### Producción
+```bash
+gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app --bind 0.0.0.0:6001
+```
+
+## Monitoreo y Logs
+
+### Archivos de Log
+- `logs/app.log`: Log principal de la aplicación
+- `logs.csv`: Registro de interacciones del LLM
+
+### Métricas Monitoreadas
+- Tiempo de respuesta del chatbot
+- Errores de impresión
+- Fallos de conexión con Fudo POS
+- Uso del sistema anti-abuso
+
+## Seguridad
+
+### Medidas Implementadas
+- **Tokens anti-abuso**: Prevención de spam y uso excesivo
+- **Validación de entrada**: Sanitización de todos los inputs
+- **Protección CSRF**: Tokens de sesión seguros
+- **Rate limiting**: Control de frecuencia de requests
+- **Logging de seguridad**: Registro de intentos de acceso no autorizado
+
+## Desarrollo y Contribución
+
+### Estructura de Commits
+- `feat:` Nuevas características
+- `fix:` Corrección de bugs  
+- `docs:` Documentación
+- `style:` Formateo de código
+- `refactor:` Refactorización
+- `test:` Pruebas
+
+### Testing
+```bash
+# Ejecutar tests unitarios
+python -m pytest tests/
+
+# Tests de integración
+python -m pytest tests/integration/
+```
+
+## Roadmap Futuro
+
+### Próximas Características
+- [ ] Sistema de reservas de mesas
+- [ ] Programa de fidelización
+- [ ] Integración con redes sociales
+- [ ] App móvil nativa
+- [ ] Dashboard de analytics
+- [ ] Sistema de reviews y ratings
+
+### Mejoras Técnicas
+- [ ] Migración a PostgreSQL
+- [ ] Implementación de microservicios
+- [ ] Containerización con Docker
+- [ ] CI/CD con GitHub Actions
+- [ ] Monitoreo con Prometheus
+
+## Soporte y Contacto
+
+Para soporte técnico o consultas sobre el proyecto, contactar al equipo de desarrollo.
+
+---
+
+**Versión**: 1.0.0  
+**Última actualización**: Julio 2025  
+**Licencia**: Privada - Biergarten Klein

+ 49 - 0
app.py

@@ -0,0 +1,49 @@
+from time import struct_time
+from fastapi import FastAPI
+from starlette.middleware.sessions import SessionMiddleware
+from config.settings import SECRET_KEY, validate_config
+
+
+def create_app() -> FastAPI:
+    """Create and configure FastAPI application"""
+    app = FastAPI(title="Web Pedidos Klein - FastAPI Backend",
+                  description="Backend for the Web Pedidos Klein application using FastAPI",)
+    
+    # Add SessionMiddleware
+    app.add_middleware(
+        SessionMiddleware,
+        secret_key=SECRET_KEY,
+        max_age=60 * 60  # max_age in seconds for Starlette
+    )
+    
+    return app
+
+
+def setup_routes(app: FastAPI):
+    """Setup all application routes"""
+    from routes import chat, users, products, orders, static
+    from fastapi import Depends
+    from auth.security import get_current_user
+    
+    # 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"])
+
+    # 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)])
+    
+    # Static routes
+    from fastapi.responses import HTMLResponse
+    app.add_api_route("/express", 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)
+
+    # Mount static files
+    static.mount_main_static_files(app)
+    static.mount_register_static_files(app)

+ 1 - 0
auth/__init__.py

@@ -0,0 +1 @@
+# Authentication module

+ 86 - 0
auth/security.py

@@ -0,0 +1,86 @@
+from datetime import datetime, timedelta
+from typing import Union
+from venv import logger
+from fastapi import Depends, HTTPException
+from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
+from typing import Annotated
+from logging import getLogger
+
+from pydantic import BaseModel
+from config.settings import SECRET_KEY
+from jose import jwt, JWTError
+from passlib.context import CryptContext
+
+from services.data_service import UserDataService
+
+
+logger = getLogger(__name__)
+
+ALGORITHM = "HS256"
+ACCESS_TOKEN_EXPIRE_DAYS = 60 * 24 * 7
+security = HTTPBearer()
+pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+user_data_service = UserDataService()
+
+class TokenData(BaseModel):
+    email: str
+
+
+def hash_password(password: str) -> str:
+    """Hash a password using bcrypt."""
+    return pwd_context.hash(password)
+
+def generate_token(email: str):
+    """Generate a JWT token for user authentication."""
+    data = {"sub": email}
+    expires_delta = timedelta(days=ACCESS_TOKEN_EXPIRE_DAYS)
+    token = create_access_token(data=data, expires_delta=expires_delta)
+    logger.debug(f"Generated token for email {email}: {token}")
+    return token
+
+def create_access_token(data: dict, expires_delta: timedelta) -> str:
+    """Create a JWT access token."""
+    to_encode = data.copy()
+    if expires_delta:
+        expire = datetime.utcnow() + expires_delta
+    else:
+        expire = datetime.utcnow() + timedelta(days=1)
+        
+    to_encode.update({"exp": expire})
+    logger.debug(f"Creating access token with data: {to_encode}")
+    logger.debug(f"Token expiration time: {expire}")
+    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
+
+def authenticate_user(email: str, password: str) -> bool:
+    """Authenticate a user by email and password."""
+    user = user_data_service.login(email, password)
+    if not user:
+        return False
+    return True
+
+async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
+    credentials_exception = HTTPException(
+        status_code=401,
+        detail="No se pudieron validar las credenciales",
+        headers={"WWW-Authenticate": "Bearer"},
+    )
+    
+    try:
+        token = credentials.credentials
+        logger.debug(f"Decoding token: {token}")
+        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
+        email = payload.get("sub")
+        if email is None:
+            logger.error("Token does not contain email")
+            raise credentials_exception
+        token_data = TokenData(email=email)
+    except JWTError:
+        logger.error("JWTError: Invalid token")
+        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}")
+        raise credentials_exception
+    return user
+

+ 1 - 0
config/__init__.py

@@ -0,0 +1 @@
+# Configuration module

+ 262 - 0
config/mails.py

@@ -0,0 +1,262 @@
+
+
+REGISTER_MAIL = {
+    "subject": "Welcome to Pedidos Express",
+    "body": """
+<html>
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>¡Bienvenido a KlowApp!</title>
+    <style>
+        body {{
+            margin: 0;
+            padding: 0;
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
+            background-color: #f3f4f6;
+        }}
+        
+        .email-container {{
+            max-width: 600px;
+            margin: 0 auto;
+            background-color: #ffffff;
+            border-radius: 12px;
+            box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
+            overflow: hidden;
+        }}
+        
+        .header {{
+            background-color: #101419;
+            color: white;
+            padding: 32px;
+            text-align: center;
+        }}
+        
+        .header h1 {{
+            margin: 0;
+            font-size: 28px;
+            font-weight: bold;
+        }}
+        
+        .header p {{
+            margin: 8px 0 0 0;
+            font-size: 16px;
+            opacity: 0.9;
+        }}
+        
+        .content {{
+            padding: 32px;
+        }}
+        
+        .welcome-message {{
+            text-align: center;
+            margin-bottom: 32px;
+        }}
+        
+        .welcome-message h2 {{
+            color: #101419;
+            font-size: 24px;
+            margin: 0 0 12px 0;
+            font-weight: bold;
+        }}
+        
+        .welcome-message p {{
+            color: #6b7280;
+            font-size: 16px;
+            line-height: 1.6;
+            margin: 0;
+        }}
+        
+        .benefits-section {{
+            background-color: #f9fafb;
+            border-radius: 8px;
+            padding: 24px;
+            margin: 24px 0;
+        }}
+        
+        .benefits-title {{
+            color: #101419;
+            font-size: 18px;
+            font-weight: 600;
+            margin: 0 0 16px 0;
+            text-align: center;
+        }}
+        
+        .benefits-list {{
+            list-style: none;
+            padding: 0;
+            margin: 0;
+        }}
+        
+        .benefits-list li {{
+            color: #374151;
+            font-size: 14px;
+            line-height: 1.5;
+            margin-bottom: 8px;
+            padding-left: 20px;
+            position: relative;
+        }}
+        
+        .benefits-list li:before {{
+            content: "✓";
+            color: #10b981;
+            font-weight: bold;
+            position: absolute;
+            left: 0;
+        }}
+        
+        .promo-section {{
+            background: linear-gradient(135deg, #101419 0%, #37404a 100%);
+            border-radius: 12px;
+            padding: 32px;
+            text-align: center;
+            margin: 32px 0;
+            color: white;
+        }}
+        
+        .promo-amount {{
+            font-size: 48px;
+            font-weight: bold;
+            margin: 16px 0;
+            border: 3px solid white;
+            border-radius: 8px;
+            padding: 16px;
+            display: inline-block;
+            min-width: 120px;
+        }}
+        
+        .promo-description {{
+            font-size: 18px;
+            margin: 16px 0;
+            opacity: 0.95;
+        }}
+        
+        .cta-button {{
+            background-color: white;
+            color: #101419;
+            padding: 16px 32px;
+            border-radius: 8px;
+            text-decoration: none;
+            font-weight: 600;
+            font-size: 16px;
+            display: inline-block;
+            margin-top: 16px;
+            transition: all 0.2s ease;
+        }}
+        
+        .cta-button:hover {{
+            background-color: #f3f4f6;
+            transform: translateY(-1px);
+        }}
+        
+        .website-section {{
+            text-align: center;
+            margin: 32px 0;
+            padding: 24px;
+            background-color: #f9fafb;
+            border-radius: 8px;
+        }}
+        
+        .website-url {{
+            color: #101419;
+            font-size: 20px;
+            font-weight: 600;
+            text-decoration: none;
+            border-bottom: 2px solid #101419;
+            padding-bottom: 4px;
+        }}
+        
+        .footer {{
+            background-color: #f9fafb;
+            padding: 24px 32px;
+            text-align: center;
+            border-top: 1px solid #e5e7eb;
+        }}
+        
+        .footer p {{
+            color: #6b7280;
+            font-size: 14px;
+            margin: 0;
+            line-height: 1.5;
+        }}
+        
+        @media (max-width: 600px) {{
+            .email-container {{
+                margin: 0;
+                border-radius: 0;
+            }}
+            
+            .header, .content {{
+                padding: 24px 16px;
+            }}
+            
+            .promo-amount {{
+                font-size: 36px;
+                padding: 12px;
+            }}
+        }}
+    </style>
+</head>
+<body>
+    <div style="padding: 20px;">
+        <div class="email-container">
+            <!-- Header -->
+            <div class="header">
+                <h1>¡Hola {name}</h1>
+                <p>Bienvenido a {app_name}</p>
+            </div>
+            
+            <!-- Content -->
+            <div class="content">
+                <!-- Welcome Message -->
+                <div class="welcome-message">
+                    <h2>Te damos la bienvenida a {app_name}</h2>
+                    <p>Tu registro ha sido exitoso. <br>Estamos emocionados de tenerte con nosotros. Esperamos que puedas disfrutar de nuestros beneficios.</p>
+                </div>
+                
+                <!-- Benefits Section -->
+                <div class="benefits-section">
+                    <h3 class="benefits-title">¿Qué puedes hacer con {app_name}?</h3>
+                    <ul class="benefits-list">
+                        <li>Realizar pedidos de forma rápida y sencilla</li>
+                        <li>Acceder a promociones exclusivas</li>
+                        <li>Disfrutar de una experiencia personalizada</li>
+                        <li>Usar nuestra inteligencia artificial</li>
+                    </ul>
+                </div>
+                
+                <!-- Promo Section -->
+                <div class="promo-section">
+                    <div class="promo-description">Este es tu pin de inicio de sesion</div>
+                    <div class="promo-amount">{pin}</div>
+                    <div class="promo-description">Guardalo muy bien</div>
+                    <a href="https://www.expressklein.com" class="cta-button">Y disfruta de {app_name}</a>
+                </div>
+                
+                <!-- Website Section -->
+                <div class="website-section">
+                    <p style="color: #6b7280; margin-bottom: 12px;">Visita nuestra plataforma:</p>
+                    <a href="https://www.expressklein.com" class="website-url">www.expressklein.com</a>
+                </div>
+            </div>
+            
+            <!-- Footer -->
+            <div class="footer">
+                <p>
+                    <strong>Express Klein</strong> - Tu aplicación de pedidos favorita<br>
+                    Si tienes alguna pregunta, no dudes en contactarnos.<br>
+                    © 2025 Express Klein. Todos los derechos reservados.
+                </p>
+            </div>
+        </div>
+    </div>
+</body>
+</html>
+    """
+}
+
+PRINTER_DISCONNECTED_MAIL = {
+    "subject": "Printer Disconnected",
+    "body": """
+"""
+}

+ 51 - 0
config/settings.py

@@ -0,0 +1,51 @@
+import os
+from dotenv import load_dotenv
+import logging
+
+from httpx import get
+
+# Load environment variables from .env file
+load_dotenv()
+
+
+APPNAME = "Pedidos Express"
+LOGS_FOLDER = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'logs')
+if not os.path.exists(LOGS_FOLDER):
+    os.makedirs(LOGS_FOLDER)
+LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
+# Configure logging
+logging.basicConfig(
+    level=getattr(logging, LOG_LEVEL),
+    format='%(asctime)s - %(name)s:%(lineno)d - %(levelname)s - %(message)s',
+    handlers=[
+        logging.FileHandler(os.path.join(LOGS_FOLDER, 'app.log')),
+        logging.StreamHandler()
+    ]
+)
+
+# Configuration
+FEEDBACK_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data', 'feedback.json')
+OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
+PORT = int(os.getenv("PORT", 6001))
+PIN_KEY = os.getenv("PIN_KEY", "-1")
+if PIN_KEY == "-1":
+    logging.warning("Using default PIN_KEY. Please set a strong PIN_KEY in your .env file for production.")
+# SECRET_KEY is crucial for signing session cookies.
+# Fallback to a default if not set, but warn that this is insecure for production.
+SECRET_KEY = os.getenv("SECRET_KEY", "your_very_very_secret_key_for_signing_cookies_python_v2")
+
+# Data paths
+BG_DATA_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)),'data', 'llm_data.json')
+PRODUCT_DATA_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)),'data', 'products.json')
+DB_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)),'data', 'data.db')
+
+def validate_config():
+    logger = logging.getLogger(__name__)
+    """Validate configuration and show warnings"""
+    if SECRET_KEY == "your_very_very_secret_key_for_signing_cookies_python_v2":
+        logger.warning("Using default SECRET_KEY. Please set a strong SECRET_KEY in your .env file for production.")
+
+    if not OPENAI_API_KEY:
+        logger.critical("CRITICAL ERROR: OPENAI_API_KEY environment variable not set. The application will not work correctly.")
+        return False
+    return True


+ 7 - 0
data/feedback.json

@@ -0,0 +1,7 @@
+[
+    {
+        "name": "Erwin Jacimino",
+        "email": "erwinjacimino2003@gmail.com",
+        "message": "El cliente mencionó que la aplicación es muy clara, pero le causó problemas de visión."
+    }
+]

+ 0 - 0
data.json → data/llm_data.json


+ 17 - 3
products.json → data/products.json

@@ -1,51 +1,65 @@
 [
+  
   {
     "id": 6,
+    "status":1,
     "name": "Burlesque",
     "type": "Cerveza",
     "description": "Cerveza Ale ámbar, 5.0º - IBU 12",
     "price": 5000,
     "image": "https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/6"
   },
+  
   {
     "id": 15,
+    "status":1,
     "name": "Bendicion Gitana",
     "type": "Cerveza",
     "description": "Pale Ale - 5,0º - IBU 15",
     "price": 5000,
     "image": "https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/15"
-  },{
+  },
+  {
     "id":163,
+    "status":1,
     "name":"Hoppy Mosh",
     "type":"Cerveza",
     "description":"IPA - 6.0º - IBU 38",
     "price": 6500,
     "image":"https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/163"
   },
+  
   {
     "id": 12,
+    "status":1,
     "name": "Black Mamba",
     "type": "Cerveza",
     "description": "Porter - 6.0º - IBU 15",
     "price": 5000,
     "image": "https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/12"
-  },{
+  },
+  {
     "id": 665,
+    "status":1,
     "name": "Marzen",
     "type": "Cerveza",
     "description": " Estilo Märzenbier, 5.0º - IBU 22",
     "price": 5000,
     "image": "https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/665"
-  },{
+  },
+  {
     "id": 1,
+    "status":1,
     "name": "24k Gold",
     "type": "Cerveza",
     "description": "Golden Ale - 4,5º - IBU 20",
     "price": 5000,
     "image": "https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/1"
   },
+  
   {
     "id": 655,
+    "status":1,
     "name": "🌟 Summer Klein",
     "type": "Coctel",
     "description": "Gin Juno, jugo de naranja, maracuya, limon y Ginger Beer",

+ 98 - 114
fudo/fudo.py

@@ -2,27 +2,58 @@ import math
 from time import time
 import requests
 import os
+import redis
+from logging import getLogger
+
+logger = getLogger(__name__)
+
 api_token = os.getenv('FUDO_API_KEY')
 api_secret = os.getenv('FUDO_API_SECRET')
 
+token = None
+token_exp = None
+
+# Configuración de Redis
+redis_client = redis.Redis(
+    host=os.getenv('REDIS_HOST', 'localhost'),
+    port=int(os.getenv('REDIS_PORT', 6379)),
+    db=int(os.getenv('REDIS_DB', 0)),
+    decode_responses=True
+)
+
+REDIS_TOKEN_KEY = 'fudo_api_token'
+
 def get_token():
     """
-    revisa el archivo dksdabjhvjhSADhsbjksf.txt para ver si el token ya fue guardado,
-    este contiene 2 lineas
-    la primera es el token y la segunda es la fecha de expiracion del token.
-    si el token no existe o ya expiro, se genera uno nuevo y se guarda en el archivo.
-    si el token existe y no ha expirado, se devuelve el token.
+    Obtiene el token de autenticación de Fudo API.
+    Primero verifica si existe un token válido en Redis.
+    Si no existe o ha expirado, solicita uno nuevo y lo guarda en Redis con expiración automática.
     """
+    global token, token_exp
+    # Intento de obtener el token desde la RAM
+    if token and token_exp and time() < token_exp:
+        logger.info("Token obtenido desde variable global")
+        return token
+    try:
+        # Intentar obtener el token desde Redis
+        cached_token = redis_client.get(REDIS_TOKEN_KEY)
+
+        if cached_token:
+            logger.info("Token obtenido desde Redis cache")
 
-    if os.path.exists("dksdabjhvjhSADhsbjksf.txt"):
-        with open("dksdabjhvjhSADhsbjksf.txt", "r") as f:
-            lines = f.readlines()
-            if len(lines) == 2:
-                token = lines[0].strip()
-                expiration = lines[1].strip()
-                if int(expiration) > time():
-                    print("Token desde cache")
-                    return token
+            token = cached_token
+
+            ttl = redis_client.ttl(REDIS_TOKEN_KEY)
+            if ttl is None or int(str(ttl)) < 0:
+                token_exp = None
+            else:
+                token_exp = int(str(ttl)) + int(time())
+            return str(cached_token)
+    except Exception as e:
+        logger.error(f"Error al conectar con Redis: {e}")
+        logger.info("Fallback: obteniendo token sin cache")
+
+    # Si no hay token en cache, solicitar uno nuevo
     url = 'https://auth.fu.do/api'
     data = {
         "apiKey": api_token,
@@ -30,42 +61,27 @@ def get_token():
     }
     
     r = requests.post(url, data=data)
-    with open("dksdabjhvjhSADhsbjksf.txt", "w") as f:
-        f.write(r.json()['token'] + "\n")
-        f.write(str(r.json()['exp']) + "\n")
-    print("Token nuevo")
-    return r.json()['token']
+    response_data = r.json()
+    token = response_data['token']
+    expiration_timestamp = response_data['exp']
     
-def get_categorys():
-    """
-{
-"data": [
-{
-"id": "1",
-"type": "ProductCategory",
-"attributes": {
-"enableOnlineMenu": true,
-"name": "Drinks",
-"preparationTime": 0,
-"position": 50
-},
-"relationships": {
-"kitchen": {
-"data": {
-"id": "1",
-"type": "Kitchen"
-}
-},
-"parentCategory": {
-"data": {
-"id": "1",
-"type": "ProductCategory"
-}
-}
-}
-}
-]
-}"""
+    # Calcular TTL en segundos para Redis
+    current_time = int(time())
+    ttl_seconds = expiration_timestamp - current_time
+    
+    try:
+        # Guardar el token en Redis con expiración automática
+        if ttl_seconds > 0:
+            redis_client.setex(REDIS_TOKEN_KEY, ttl_seconds, token)
+            logger.info(f"Token nuevo guardado en Redis (expira en {ttl_seconds} segundos)")
+        else:
+            logger.warning("Warning: El token ya está expirado")
+    except Exception as e:
+        logger.error(f"Error al guardar en Redis: {e}")
+    
+    return token
+
+def get_categories():
     token = get_token()
     url = 'https://api.fu.do/v1alpha1/product-categories'
     headers = {
@@ -75,64 +91,17 @@ def get_categorys():
     return r.json()
 
 def get_product(id_category:int):
-    """
-        Response Example:
-                [{
-            'type': 'Product',
-            'id': '206',
-            'attributes': {
-                'active': True,
-                'code': None,
-                'cost': 364.0,
-                'description': '',
-                'enableOnlineMenu': None,
-                'enableQrMenu': None,
-                'favourite': False,
-                'imageUrl': None,
-                'name': 'Pollo 70 g',
-                'position': 28800000,
-                'preparationTime': None,
-                'price': 1500.0,
-                'sellAlone': True,
-                'stock': None,
-                'stockControl': False
-            },
-            'relationships': {'kitchen': {'data': {'type': 'Kitchen', 'id': '4'}}, 'productCategory': {'data': {'type': 'ProductCategory', 'id': '28'}}, 'productModifiersGroups': {'data': []}, 'productProportions': {'data': []}}
-        }]
-    """
     url = 'https://api.fu.do/v1alpha1/products/{}'.format(id_category)
     token = get_token()
     headers = {
         'Authorization': 'Bearer ' + token
     }
     r = requests.get(url, headers=headers)
+    if r.status_code != 200:
+        logger.error(f"Error al obtener producto: {r.json()['errors']}")
     return r.json()
 
 def get_products():
-    """
-        Response Example:{
-            'type': 'Product',
-            'id': '206',
-            'attributes': {
-                'active': True,
-                'code': None,
-                'cost': 364.0,
-                'description': '',
-                'enableOnlineMenu': None,
-                'enableQrMenu': None,
-                'favourite': False,
-                'imageUrl': None,
-                'name': 'Pollo 70 g',
-                'position': 28800000,
-                'preparationTime': None,
-                'price': 1500.0,
-                'sellAlone': True,
-                'stock': None,
-                'stockControl': False
-            },
-            'relationships': {'kitchen': {'data': {'type': 'Kitchen', 'id': '4'}}, 'productCategory': {'data': {'type': 'ProductCategory', 'id': '28'}}, 'productModifiersGroups': {'data': []}, 'productProportions': {'data': []}}
-        }
-    """
     url = 'https://api.fu.do/v1alpha1/products'
     token = get_token()
     headers = {
@@ -151,17 +120,16 @@ def get_table(number:int):
     }
     r = requests.get(url, headers=headers)
     if r.status_code != 200:
-        print('Error al obtener tablas:' + str(r.json()['errors']))
+        logger.error('Error al obtener tablas:' + str(r.json()['errors']))
         return None
     try:
         return list(filter(lambda x: x['attributes']['number'] == number, r.json()['data']))[0]
     except:
-        print('Error al obtener tabla')
-        print(r.json())
+        logger.error('Error al obtener tabla')
+        logger.error(r.json())
         return None
 
 def get_sale(sale_id:int):
-
     url = 'https://api.fu.do/v1alpha1/sales/{}'.format(sale_id)
     token = get_token()
     headers = {
@@ -169,7 +137,7 @@ def get_sale(sale_id:int):
     }
     r = requests.get(url, headers=headers)
     if r.status_code != 200:
-        print('Error al obtener tablas:' + str(r.json()['errors']))
+        logger.error('Error al obtener tablas:' + str(r.json()['errors']))
         return None
     return r.json()
 
@@ -205,7 +173,7 @@ def create_sale(table_id:int):
     }
     r = requests.post(url, headers=headers, json=data)
     if r.status_code != 201:
-        print('Error al crear la venta:', r.json())
+        logger.error('Error al crear la venta:', r.json())
         return None
     return r.json()["data"]
 
@@ -242,7 +210,7 @@ def create_item(product_id:int, quantity:int, sale_id:int, comment = None):
     }
     r = requests.post(url, headers=headers, json=data)
     if r.status_code != 201:
-        print(r.json())
+        logger.error(r.json())
         return None
     return r.json()["data"]
 
@@ -252,28 +220,44 @@ def get_active_sale(table):
         return None
     return data[0]
 
+def clear_token():
+    """
+    Elimina el token cached de Redis.
+    Útil cuando el token es inválido o se necesita forzar una renovación.
+    """
+    try:
+        redis_client.delete(REDIS_TOKEN_KEY)
+        logger.info("Token eliminado del cache")
+    except Exception as e:
+        logger.error(f"Error al eliminar token de Redis: {e}")
+
 if __name__ == "__main__":
     table = get_table(107)
     if table is None:
-        print('No se pudo obtener la mesa')
+        logger.error('No se pudo obtener la mesa')
         exit()
     activeSale = get_active_sale(table)
     if not activeSale:
-        print('No hay una venta activa para la mesa')
+        logger.error('No hay una venta activa para la mesa')
         activeSale = create_sale(table['id'])
         if activeSale is None:
-            print('No se pudo crear la venta')
+            logger.error('No se pudo crear la venta')
             exit()
     else:
         activeSale = activeSale[0]
-    print('Venta activa:', activeSale['id'])
+    logger.info('Venta activa: %s', activeSale['id'])
 
 
 """
-Intrucciones para hacer un pedido:
+Instrucciones para hacer un pedido:
+
+1. Obtener el token de autenticación con `get_token()` (ahora usa Redis cache).
+2. Obtener la mesa con `get_table(numero_de_mesa)`.
+3. Ver si tiene una activeSale, en caso contrario crear una con `create_sale(id_mesa)`.
+4. Agregar los items a la venta con `create_item(id_producto, cantidad, id_venta, comentario)`.
 
-1. Obtener el token de autenticación con `get_token()`.
-2. obtener la mesa con `get_table(numero_de_mesa)`.
-3. ver si tiene una activeSale, en caso contrario crear una con `create_sale(id_mesa)`.
-4. agregar los items a la venta con `create_item(id_producto, cantidad, id_venta, comentario)`.
+Configuración de Redis:
+- Host: REDIS_HOST (default: localhost)
+- Puerto: REDIS_PORT (default: 6379)
+- Base de datos: REDIS_DB (default: 0)
 """

+ 27 - 1
impresora/printer.py

@@ -1,10 +1,14 @@
-import tabulate
+import logging
+from venv import logger
 from escpos.printer.win32raw import Win32Raw
 from escpos.printer.usb import Usb
 from escpos.printer.network import Network
 from escpos.printer import Dummy
 from escpos.escpos import Escpos
 from impresora.order import Order
+import usb.core
+
+logger = logging.getLogger(__name__)
 
 
 class BasePrinter:
@@ -15,6 +19,10 @@ class BasePrinter:
         self.doubled_size = False
         self.work = Dummy()
         self.printer:Escpos
+    
+    def is_connected(self):
+        return self.printer.is_online()
+
     def change_font(self):
         self.font = "b" if self.font == "a" else "a"
         self.work.set(font=self.font)
@@ -70,6 +78,24 @@ class PrinterUSB(BasePrinter):
     def __init__(self, vendor_id, product_id):
         super().__init__()
         self.printer = Usb(vendor_id, product_id,in_ep=0x81,out_ep=0x03)
+        self.vendor_id = vendor_id
+        self.product_id = product_id
+    @staticmethod
+    def check_usb_port(vendor_id, product_id):
+        """Check if the USB printer is connected."""
+        try:
+            element = usb.core.find(idVendor=vendor_id, idProduct=product_id)
+            if element is None:
+                logger.error(f"Printer is not connected on USB port {vendor_id}:{product_id}.")
+                return False
+            return True
+        except Exception as e:
+            logger.error(f"Error checking USB printer: {e}")
+            return False
+    
+    def is_connected(self):
+        """Check if the USB printer is connected."""
+        return self.check_usb_port(self.vendor_id, self.product_id)
 
 class PrinterNetwork(BasePrinter):
     def __init__(self, host, port):

Різницю між файлами не показано, бо вона завелика
+ 731 - 0
logs/app.log


+ 39 - 368
main.py

@@ -1,376 +1,47 @@
-import csv
+import asyncio
 import os
-import json
-import secrets
-from typing import List, Dict, Union, Annotated
-
-from fastapi import FastAPI, Request, HTTPException, Header, Depends
-from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
-from fastapi.staticfiles import StaticFiles
-from pydantic import BaseModel
-from openai import OpenAI
-from dotenv import load_dotenv
-from starlette.middleware.sessions import SessionMiddleware
-
-from impresora.printer import PrinterUSB
-from impresora.order import *
-
-import smtplib
-from email.message import EmailMessage
-# Load environment variables from .env file
-load_dotenv()
-import fudo.fudo as fd
-
-# Configuration
-OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
-PORT = int(os.getenv("PORT", 6001))
-
-EXCLUDED_BEER_IDS = [14, 12, 11];
-# SECRET_KEY is crucial for signing session cookies.
-# Fallback to a default if not set, but warn that this is insecure for production.
-SECRET_KEY = os.getenv("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":
-    print("WARNING: Using default SECRET_KEY. Please set a strong SECRET_KEY in your .env file for production.")
-
-if not OPENAI_API_KEY:
-    print("CRITICAL ERROR: OPENAI_API_KEY environment variable not set. The applicaton will not work correctly.")
-    # Potentially exit or prevent app startup if critical env var is missing
-    # raise ValueError("OPENAI_API_KEY is not set, cannot start application.")
-
-# --- FastAPI App Initialization ---
-app = FastAPI(title="Web Pedidos Klein - FastAPI Backend")
-
-# Add SessionMiddleware
-# This middleware adds session support using signed cookies.
-# Original Express maxAge was 1 hour (60 * 60 * 1000 ms)
-app.add_middleware(
-    SessionMiddleware,
-    secret_key=SECRET_KEY,
-    max_age=60 * 60 # max_age in seconds for Starlette
-)
-
-# --- Data Loading ---
-# Assumes data.json is in the same directory as main.py
-# The original path was web_pedidos/src/data.json
-# For the Python version, copy src/data.json to be alongside main.py
-BG_DATA_PATH = os.path.join(os.path.dirname(__file__), 'data.json')
-PRODUCTS_PATH = os.path.join(os.path.dirname(__file__), 'products.json')
-
-def add_product_to_fudo(product_id: int, quantity: int, table_number:int, comment = None):
-    table = fd.get_table(table_number)
-    if not table:
-        print(f"Error: Table {table_number} not found.")
-        return None
-    activeSale = fd.get_active_sale(table)
-    if not activeSale:
-        activeSale = fd.create_sale(table['id'])
-        if not activeSale:
-            print(f"Error: Could not create sale for table {table_number}.")
-            return None
-    item = fd.create_item(product_id, quantity, activeSale['id'], comment)
-    if not item:
-        print(f"Error: Could not create item for product {product_id}.")
-        return None
-    return item
-
-def send_email():
-    # Datos del remitente
-    EMAIL_ORIGEN = 'expresspedidos211@gmail.com'
-    EMAIL_DESTINO = ['erwinjacimino2003@gmail.com', "mompyn@gmail.com"]
-    CONTRASENA = 'drkassszdtgapufg' 
-
-
-    # Crear el correo
-    msg = EmailMessage()
-    msg['Subject'] = 'Impresora Desconectada weon :('
-    msg['From'] = EMAIL_ORIGEN
-    msg['To'] = ", ".join(EMAIL_DESTINO)
-    msg.set_content('Este correo tiene contenido HTML.')
-    msg.add_alternative("""
-    <html>
-    <body style="margin:0; padding:0; background-color:#5a67d8; font-family: Arial, sans-serif;">
-        <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="padding: 40px 0;">
-        <tr>
-            <td align="center">
-            <table border="0" cellpadding="0" cellspacing="0" width="500" style="background-color: #e3e3e3; border-radius: 25px; padding: 40px; text-align: center;">
-                <tr>
-                <td>
-                    <div style="font-size: 60px; background-color: #ff6b6b; width: 80px; height: 80px; line-height: 80px; border-radius: 15px; margin: 0 auto 20px; color: white;">
-                    🖨️
-                    </div>
-                </td>
-                </tr>
-                <tr>
-                <td>
-                    <img src="https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZGlraDhpb2tkeHEweDZ2eWdnZDZlNXFvODhmNzZieWN6OXp0b3ZqNCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3hetVnNSl0IBa/giphy.gif" alt="Gatito peleando con la impresora" width="250" style="border-radius: 12px; margin-bottom: 20px;" />
-                </td>
-                </tr>
-                <tr>
-                <td>
-                    <h1 style="font-size: 24px; color: #ff6b6b; margin-bottom: 10px;">¡Impresora Desconectada!</h1>
-                </td>
-                </tr>
-                <tr>
-                <td>
-                    <p style="font-size: 16px; color: #333333; line-height: 1.5; margin-bottom: 20px;">
-                    No se puede establecer conexión con la impresora.<br>
-                    Por favor, verifica la conexión y vuelve a intentarlo.
-                    </p>
-                </td>
-                </tr>
-                <tr>
-                <td>
-                    <span style="display: inline-block; background: #ff6b6b; color: white; padding: 12px 24px; border-radius: 25px; font-weight: bold; font-size: 16px;">
-                    🔴 Estado: Desconectada
-                    </span>
-                </td>
-                </tr>
-            </table>
-            </td>
-        </tr>
-        </table>
-    </body>
-    </html>
-
-    """, subtype='html')
-
-    # Enviar el correo usando SMTP de Gmail
-    with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp:
-        smtp.login(EMAIL_ORIGEN, CONTRASENA)
-        smtp.send_message(msg)
-
-def load_bg_data() -> List[Dict[str, str]]:
-    try:
-        with open(BG_DATA_PATH, 'r', encoding='utf-8') as f:
-            return json.load(f)
-    except FileNotFoundError:
-        print(f"ERROR: Data file not found at {BG_DATA_PATH}. Serving with empty data.")
-        return []
-    except json.JSONDecodeError:
-        print(f"ERROR: Could not decode JSON from {BG_DATA_PATH}. Serving with empty data.")
-        return []
-
-def load_products() -> List[Dict[str, str]]:
-    try:
-        with open(PRODUCTS_PATH, 'r', encoding='utf-8') as f:
-            return list(filter(lambda product: product['id'] not in EXCLUDED_BEER_IDS, json.load(f)))
-    except FileNotFoundError:
-        print(f"ERROR: Data file not found at {PRODUCTS_PATH}. Serving with empty data.")
-        return []
-    except json.JSONDecodeError:
-        print(f"ERROR: Could not decode JSON from {PRODUCTS_PATH}. Serving with empty data.")
-        return []
-bg_data_loaded = load_bg_data()
-all_products = load_products()
-# region --- Pydantic Models for Request/Response Typing ---
-class Message(BaseModel):
-    role: str
-    content: str
-
-class ChatCompletionRequest(BaseModel):
-    messages: List[Message]
-    user: str
-
-class ItemWeb(BaseModel):
-    id: int
-    name: str
-    quantity: int
-    price: float
-    itemTotal: float
-
-class OrderWeb(BaseModel):
-    customerName: str
-    items: List[ItemWeb]
-    totalAmount: float
-    orderDate: str
-    table: int
-# endregion --- Pydantic Models for Request/Response Typing ---
-
-# region --- OpenAI Service Logic ---
-openai_client = OpenAI(api_key=OPENAI_API_KEY)
-
-async def generate_completion(messages_array: List[Message], session_id: str) -> str:
-    if not OPENAI_API_KEY:
-        print("Error: OpenAI API key is not configured.")
-        raise HTTPException(status_code=500, detail="OpenAI API key not configured on server.")
-
-    print(f"[OpenAI Service Python] Session/Token {session_id} sent: {[msg.model_dump() for msg in messages_array]}")
-
-    data_for_prompt = [
-        f'{{"pregunta": "{item.get("q", "")}", "respuesta": "{item.get("ans", "")}"}}'
-        for item in bg_data_loaded
-    ]
-    data_string = "\n".join(data_for_prompt)
-
-    preprompt = f"""
-Eres un asistente de el bar klein, tu nombre es camilo klein, usas emojis para responder.
-y ser carismatico con el cliente.
-tus responsabilidades son:
-- Responder preguntas sobre el menu de el bar klein
-- Proporcionar información sobre el menú de el bar klein
-- Proporcionar recomendaciones sobre el menú de el bar klein
-- Proporcionar información sobre la comida de el bar klein
-- No puedes tomar pedidos de clientes, solo informar
-- Debes evadir cualquier pregunta que no sea relacionada con el bar klein
-para esto usaras los siguientes datos:
-{data_string}
-    """ #
-
-    processed_messages: List[Dict[str, str]] = [{"role": "system", "content": preprompt}]
-    processed_messages.extend([msg.model_dump() for msg in messages_array])
-
-
-    try:
-        completion = openai_client.chat.completions.create(
-            model="gpt-4o-mini", #
-            messages=processed_messages, # type: ignore (OpenAI lib expects list of specific dicts)
-            temperature=0.3, #
-        )
-        response_content = completion.choices[0].message.content
-        return response_content if response_content else "-1" #
-    except Exception as e:
-        print(f"Error calling OpenAI: {e}")
-        # Avoid exposing detailed error messages to the client unless necessary
-        raise HTTPException(status_code=500, detail="Error al procesar tu solicitud con OpenAI.")
-
-# endregion --- OpenAI Service Logic ---
-# --- Security/Token Dependency ---
-async def get_session_token(request: Request) -> Union[str, None]:
-    return request.session.get("antiAbuseToken")
-
-async def protect_chat_api(
-    request: Request,
-    x_app_token: Annotated[Union[str, None], Header(alias="X-App-Token")] = None,
-    session_token: Annotated[Union[str, None], Depends(get_session_token)] = None
-):
-    # Equivalent to protectChatAPI middleware
-    if not session_token:
-        raise HTTPException(status_code=403, detail="Acceso denegado: Sesión inválida o token no inicializado.")
-
-    if not x_app_token:
-        raise HTTPException(status_code=401, detail="Acceso denegado: Falta el token X-Chat-Token.")
-
-    if x_app_token != session_token:
-        # Log this attempt for security monitoring
-        print(f"WARN: Invalid token attempt. Expected: {session_token}, Received: {x_app_token}")
-        raise HTTPException(status_code=403, detail="Acceso denegado: Token inválido.")
-    return True # Protection passed
-
-@app.get("/api/get_products", summary="Get products")
-async def get_products():
-    return JSONResponse({"products": all_products})
-
-# --- API Endpoints ---
-@app.get("/api/chat/init-chat", summary="Initialize chat and get anti-abuse token")
-async def init_chat(request: Request):
-    current_token = request.session.get("antiAbuseToken")
-    if not current_token:
-        new_token = secrets.token_hex(32)
-        request.session["antiAbuseToken"] = new_token # Store in session
-        print(f"Generated new antiAbuseToken for session: {new_token}")
-        return JSONResponse({"chatToken": new_token})
-    else:
-        # print(f"Using existing antiAbuseToken for session: {current_token}")
-        return JSONResponse({"chatToken": current_token})
-
-class UserCodeRequest(BaseModel):
-    user_code: str
-
-@app.post("/api/existsUser", summary="Check if user exists")
-async def exists_user(request: UserCodeRequest):
-    with open('users.json', 'r') as f:
-        users = json.load(f)
-        for user in users:
-            if user['userCode'] == request.user_code:
-                return JSONResponse({
-                    "success": True,
-                    "userName": user['userName']
-                })
-        return JSONResponse({
-            "success": True,
-            "userName": request.user_code
-        })
+import uvicorn
+from app import create_app, setup_routes
+from config.settings import PORT, OPENAI_API_KEY, BG_DATA_PATH, validate_config
+from logging import getLogger
+from routes.orders import order_thread
+from threading import Thread
+
+from services.data_service import initialize_db
+
+logger = getLogger("main")
+
+def main():
+    """Main application entry point"""
+    # Validate configuration
+    if not validate_config():
+        logger.critical("FATAL: Configuration validation failed.")
+        if not OPENAI_API_KEY:
+            logger.error("Please create a .env file with OPENAI_API_KEY='your_key_here'")
+            with open(".env", "w") as f:
+                f.write("OPENAI_API_KEY='your_key_here'")
+        return
+    
+    # Create and configure app
+    app = create_app()
+    setup_routes(app)
+    initialize_db()
     
-@app.post("/api/printer/order", summary="Printer order", dependencies=[Depends(protect_chat_api)])
-async def printer_order(order: OrderWeb):
-    print("Printer order received")
-    print(order)
-    items = order.items
-    table = order.table
-    if not items or not table:
-        return JSONResponse(status_code=400, content={"message": "Items and table are required."})
-    if not isinstance(table, int):
-        return JSONResponse(status_code=400, content={"message": "Table must be an integer."})
-    product_errors = []
-    for item in items:
-        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}.")
-    if product_errors:
-        return JSONResponse(status_code=424, content={"message": "Error adding products to table.", "errors": product_errors})
-    # en caso de que no alla error, imprimimos el pedido
-    printer = PrinterUSB(0xfe6,0x811e)
-    print_order = Order(order.customerName,[Item(item.name, item.price, item.quantity) for item in items])
-    try:
-        printer.print_order(print_order, table)
-    except:
-        #Si la impresora no esta conectada, enviamos un correo
-        send_email()
-        return JSONResponse(status_code=424, content={"message": "No se pudo imprimir el Pedido, impresora desconectada"})
-    # Logs de pedidos
-    if not os.path.exists('logs.csv'):
-        with open('logs.csv', 'w', newline='') as f:
-            writer = csv.writer(f)
-            writer.writerow(['userName', 'table', 'orderDate', 'items'])
+    # 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:
-        with open('logs.csv', 'a', newline='') as f:
-            writer = csv.writer(f)
-            writer.writerow([order.customerName, order.table, order.orderDate, list(map(lambda item: item.name, items))])
+        logger.info(f"Datos del asistente cargados desde: {os.path.abspath(BG_DATA_PATH)}")
 
-@app.post("/api/chat/completions",
-          summary="Get chat completions from OpenAI",
-          dependencies=[Depends(protect_chat_api)])
-async def chat_completions(request_data: ChatCompletionRequest, request: Request):
-    # Uses session_token (which is the antiAbuseToken) as an identifier for logging
-    session_identifier = request.session.get("antiAbuseToken", "unknown_session")
+    # Run the threads
 
-    try:
-        openai_response = await generate_completion(request_data.messages, session_identifier)
-        if os.path.exists("llm_logs.txt"):
-            with open("llm_logs.txt", "a") as f:
-                f.write(f"{request_data.user}: {openai_response}\n")
-        else:
-            with open("llm_logs.txt", "w") as f:
-                f.write(f"{request_data.user}: {openai_response}\n")
-        return JSONResponse({"response": openai_response})
-    except HTTPException as e: # Re-raise HTTPExceptions from called functions
-        raise e
-    except Exception as e:
-        print(f"Unexpected error in /api/chat/completions: {e}")
-        raise HTTPException(status_code=500, detail="Error interno del servidor al procesar el chat.")
+    thread = Thread(target=order_thread, daemon=True)
+    thread.start()
+    # Start the server
+    #rut without logs
+    uvicorn.run(app, host="0.0.0.0", port=PORT, log_level="info", access_log=False)
 
-@app.get("/", response_class=HTMLResponse, include_in_schema=False)
-async def serve_index_html():
-    index_path = os.path.join("public", "index.html")
-    if not os.path.exists(index_path):
-        raise HTTPException(status_code=404, detail="public/index.html not found.")
-    return FileResponse(index_path)
 
-app.mount("/", StaticFiles(directory="public", html=False), name="public_root_assets")
-
-# --- Main Application Runner ---
 if __name__ == "__main__":
-    if not OPENAI_API_KEY:
-        print("FATAL: OPENAI_API_KEY is not set. OpenAI features will fail.")
-        print("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'")
-    
-    print(f"Servidor corriendo en http://localhost:{PORT}")
-    if not os.path.exists(BG_DATA_PATH):
-         print(f"ADVERTENCIA: {BG_DATA_PATH} no encontrado. El asistente de IA no tendrá datos específicos del menú.")
-    else:
-        print(f"Datos del asistente cargados desde: {os.path.abspath(BG_DATA_PATH)}")
-    
-    import uvicorn
-    uvicorn.run(app, host="0.0.0.0", port=PORT)
+    main()

+ 22 - 0
models/__init__.py

@@ -0,0 +1,22 @@
+# Models module
+
+from .user import User, UserIDRequest, RegisterUserRequest
+from .items import Product, ProductWithQuantity
+from .sells import Sale, SellItem, ItemWeb, OrderWeb
+from .blacklist import Blacklist
+from .chat import Message, ChatCompletionRequest
+
+__all__ = [
+    "User",
+    "UserIDRequest",
+    "RegisterUserRequest",
+    "Product",
+    "ProductWithQuantity",
+    "Sale",
+    "SellItem",
+    "ItemWeb",
+    "OrderWeb", 
+    "Blacklist",
+    "Message",
+    "ChatCompletionRequest"
+]

+ 10 - 0
models/blacklist.py

@@ -0,0 +1,10 @@
+from typing import Optional
+from pydantic import BaseModel
+
+class Blacklist(BaseModel):
+    """Blacklist model matching the database schema"""
+    id: int
+    user_id: int
+    email: Optional[str] = None
+    nombre: Optional[str] = None
+    rut: Optional[str] = None

+ 13 - 0
models/chat.py

@@ -0,0 +1,13 @@
+
+from typing import List
+from pydantic import BaseModel
+
+
+class Message(BaseModel):
+    role: str
+    content: str
+
+
+class ChatCompletionRequest(BaseModel):
+    messages: List[Message]
+    user: str

+ 18 - 0
models/items.py

@@ -0,0 +1,18 @@
+from typing import List, Optional
+from pydantic import BaseModel
+
+class Product(BaseModel):
+    """Product model matching the database schema"""
+    id: int
+    name: str
+    type: Optional[str] = None
+    description: Optional[str] = None
+    price: float
+    image: Optional[str] = None
+    status: int = 1  # 0: Inactive, 1: Active
+
+class ProductWithQuantity(Product):
+    """Product model with quantity field for sales"""
+    cantidad: int = 1
+
+

+ 45 - 0
models/sells.py

@@ -0,0 +1,45 @@
+from typing import List, Optional
+from pydantic import BaseModel
+
+class ItemWeb(BaseModel):
+    id: int
+    quantity: int
+
+class OrderWeb(BaseModel):
+    customerId: int
+    items: List[ItemWeb]
+    totalAmount: float
+    orderDate: str
+    table: int
+
+class Product(BaseModel):
+    """Legacy Product model - use models.items.Product instead"""
+    id: int
+    name: str
+    price: float
+    type: str
+    
+    description: str
+    image: str
+    status: int  # 0: inactive, 1: active
+    quantity: Optional[int] = 1  # Optional quantity for the product
+
+class Sale(BaseModel):
+    """Sale model matching the database schema"""
+    id: int
+    user_id: int
+    total: float
+    fudo_id: str
+    fecha: str
+    table: int
+    user_name: Optional[str] = None
+    user_email: Optional[str] = None
+
+class SellItem(BaseModel):
+    """Legacy model - use Sale instead"""
+    user_id: int
+    products: list[Product]
+    total: float
+    fudo_id: str
+    fecha: str
+    table: int  

+ 25 - 0
models/user.py

@@ -0,0 +1,25 @@
+from typing import Optional
+from pydantic import BaseModel, Field
+
+class UserIDRequest(BaseModel):
+    id: int
+
+class RegisterUserRequest(BaseModel):
+    name: str
+    email: str
+    rut: str
+
+class User(BaseModel):
+    """User model matching the database schema"""
+    id: int
+    email: str
+    nombre: str
+    rut: str
+    pin_hash: str
+    kleincoins: str
+    created_at: str
+
+
+class LoginRequest(BaseModel):
+    email: str
+    pin: str = Field(min_length=4, max_length=4, description="4-digit PIN for user authentication")

+ 0 - 0
public/js/interfaces.js → pin.key


+ 0 - 98
public/js/service.js

@@ -1,98 +0,0 @@
-// public/ts/service.ts
-var chatToken = null;
-
-async function initializeChat() {
-  try {
-    const response = await fetch("/api/chat/init-chat");
-    if (!response.ok) {
-      const errorData = await response.json().catch(() => ({ message: "Error desconocido al inicializar." }));
-      throw new Error(errorData.message || `Error del servidor: ${response.status}`);
-    }
-    const data = await response.json();
-    chatToken = data.chatToken;
-    if (!chatToken) {
-      throw new Error("No se pudo obtener el token de chat.");
-    }
-    return true;
-  } catch (error) {
-    console.error("Error al inicializar el chat:", error);
-  }
-}
-async function sendMessage(message, messageList, userName) {
-  if (!chatToken) {
-    return "not_init";
-    return;
-  }
-  messageList.push({ role: "user", content: message });
-  const cuerpo = { messages: messageList, user: userName }
-  const response = await fetch("/api/chat/completions", {
-    method: "POST",
-    headers: {
-      "Content-Type": "application/json",
-      "X-App-Token": chatToken
-    },
-    body: JSON.stringify(cuerpo)
-  });
-  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();
-  const assistantResponse = data.response;
-  if (assistantResponse) {
-    messageList.push({ role: "assistant", content: assistantResponse });
-    return { messageList, assistantResponse };
-  } else {
-    throw new Error("Respuesta vacía del asistente.");
-  }
-}
-
-async function sendOrder(order) {
-  try {
-    const response = await fetch("/api/printer/order", {
-      method: "POST",
-      headers: {
-        "Content-Type": "application/json",
-        "X-App-Token": chatToken
-      },
-      body: JSON.stringify(order)
-    });
-    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 enviar la orden:", error);
-    throw error;
-  }
-}
-async function getProducts(){
-  const response = await fetch("/api/get_products");
-  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.products;
-}
-async function existsUser(userCode){
-  const response = await fetch("/api/existsUser", {
-    method: "POST",
-    headers: {
-      "Content-Type": "application/json",
-      "X-App-Token": chatToken
-    },
-    body: JSON.stringify({
-      user_code: userCode
-    })
-  });
-  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;
-}
-export { initializeChat, sendMessage, sendOrder, getProducts, existsUser };

+ 0 - 0
public/assets/summer.jpeg → public/main/assets/summer.jpeg


+ 54 - 19
public/index.html → public/main/index.html

@@ -26,8 +26,8 @@
     </script>
   <!-- Markdown -->
   <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
-  <script src="js/app.js" type="module"></script>
-  <link rel="stylesheet" href="styles.css">
+  <script src="/express/js/app.js" type="module"></script>
+  <link rel="stylesheet" href="/express/styles.css">
   <!-- Animaciones -->
   <style>
     @keyframes slideRight {
@@ -207,7 +207,6 @@
         </div>
         <button id="checkoutButton"
                 class="w-full bg-[#101419] hover:bg-[#37404a] disabled:opacity-50 text-white py-2 rounded-md"
-                onclick="processOrder()"
                 disabled>
           Envia tu orden
         </button>
@@ -251,27 +250,63 @@
   
   <!-- === MODAL INICIO DE SESIÓN === -->
 <div id="sessionModal"
-     class="fixed hidden inset-0 bg-black/70 flex items-center justify-center z-50">
-  <div class="bg-white w-full max-w-sm p-6 rounded-lg space-y-4 text-center">
-    <h2 class="text-xl font-bold">¡Bienvenido!</h2>
-    <p class="text-sm text-gray-600">
-      Ingresa tu número de mesa y tu nombre para comenzar.
-    </p>
+     class="fixed hidden inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
+  <form id="loginForm" class="bg-white w-full max-w-md p-8 rounded-xl shadow-xl space-y-6">
+    <div class="text-center">
+      <h2 class="text-2xl font-bold text-gray-900">¡Bienvenido!</h2>
+      <p id="loginMessage" class="text-sm text-gray-600 mt-2">
+        Ingresa tus datos para comenzar tu pedido
+      </p>
+    </div>
 
-    <input id="clientCodeInput"
-           class="w-full border px-3 py-2 rounded-md"
-           placeholder="Ingresa tu nombre" />
-    <input id="tableInput"
-           type="number" min="1"
-           class="w-full border px-3 py-2 rounded-md"
-           placeholder="Ingresa tu numero de mesa" />
+    <div class="space-y-4">
+      <div>
+        <label for="emailInput" class="block text-sm font-medium text-gray-700 mb-2">
+          Correo electrónico
+        </label>
+        <input id="emailInput"
+               name="email"
+               type="email"
+               class="w-full border border-gray-300 px-4 py-3 rounded-lg focus:ring-2 focus:ring-[#101419] focus:border-transparent outline-none transition-all"
+               placeholder="tu@email.com" 
+               required />
+      </div>
 
+      <div>
+        <label for="pinInput" class="block text-sm font-medium text-gray-700 mb-2">
+          PIN de 4 dígitos
+        </label>
+        <input id="pinInput"
+               name="pin"
+               type="password"
+               maxlength="4"
+               pattern="[0-9]{4}"
+               class="w-full border border-gray-300 px-4 py-3 rounded-lg focus:ring-2 focus:ring-[#101419] focus:border-transparent outline-none transition-all"
+               placeholder="••••" 
+               required />
+      </div>
+
+      <div>
+        <label for="tableInput" class="block text-sm font-medium text-gray-700 mb-2">
+          Número de mesa
+        </label>
+        <input id="tableInput"
+               name="table"
+               type="number" 
+               min="1" 
+               max="99"
+               class="w-full border border-gray-300 px-4 py-3 rounded-lg focus:ring-2 focus:ring-[#101419] focus:border-transparent outline-none transition-all"
+               placeholder="Ej: 5" 
+               required />
+      </div>
+    </div>
 
     <button id="sessionAcceptBtn"
-            class="w-full bg-[#101419] hover:bg-[#37404a] text-white py-2 rounded-md">
-      Aceptar
+            type="submit"
+            class="w-full bg-[#101419] hover:bg-[#37404a] text-white py-3 rounded-lg font-medium transition-colors duration-200 focus:ring-2 focus:ring-offset-2 focus:ring-[#101419]">
+      Comenzar pedido
     </button>
-  </div>
+  </form>
 </div>
 
               <!-- ---------- JS: conmutar tabs + toast ---------- -->

+ 173 - 122
public/js/app.js → public/main/js/app.js

@@ -1,8 +1,12 @@
-import { initializeChat as serviceInitializeChat, sendMessage as serviceSendMessage, sendOrder, getProducts, existsUser } from './service.js';
+import { sendMessage as serviceSendMessage } from './service/chat.js';
+import { getProducts, sendOrder } from './service/product.js';
+import { login } from './service/auth.js'
+
 // --- Variables de Usuario ---
-let userName = '';
+let userId = -1;
+let userName = "Cliente";
 let userTable = null;
-
+let userToken = null;
 // --- Datos de Productos y Carrito ---
 let products = [];
 let cart = [];
@@ -13,6 +17,7 @@ let chatHistory = [
     { role: "system", content: "¡Hola! Soy tu asistente en Biergarten Klein. ¿Te gustaría una recomendación de nuestras cervezas artesanales?" }
 ];
 
+
 // --- Elementos del DOM: Productos y Carrito ---
 const productListElement = document.getElementById("productList");
 const cartItemsElement = document.getElementById("cartItems");
@@ -32,6 +37,82 @@ const chatSuggestionsElement = document.getElementById("chatSuggestions");
 // --- Loader Global ---
 let globalLoaderElement = null;
 
+//#region --- Inicialización y Configuracion ---
+async function initializeApp() {
+    
+    showGlobalLoader();
+    await renderProducts();
+    hideGlobalLoader();
+
+    const chatSuggestions = Array.from(chatSuggestionsElement.children);
+
+    chatSuggestions.forEach(suggestion => {
+        suggestion.addEventListener("click", () => {
+            sendSuggestion(suggestion.querySelector(".chat-suggestion").textContent);
+        });
+    });
+}
+
+function initializeLoginModal() {
+    const sessionModal = document.getElementById('sessionModal');
+    const loginForm = document.getElementById('loginForm');
+    sessionModal.classList.remove('hidden');
+    loginForm.addEventListener('submit', async (event) => {
+        event.preventDefault();
+
+        const fd = new FormData(loginForm);
+        const email = fd.get('email').trim();
+        const pin = fd.get('pin').trim();
+        userTable = Number(fd.get('table').trim());
+
+        if (!email || !pin || !userTable) {
+            alert("Por favor, completa todos los campos.");
+            return;
+        }
+        try{
+            const {data} = await login(email, pin)
+            userToken = data.token;
+            userName = data.name;
+            console.log("Usuario autenticado:", data);
+            if (!userToken || data.id === undefined) {
+                alert("Error al iniciar sesión. Por favor, inténtalo de nuevo.");
+                return;
+            }
+
+            sessionModal.classList.add('hidden');
+
+            initializeApp();
+        }catch (error) {
+        }
+
+    });
+}
+
+function initializeChat() {
+    if (!chatForm) return;
+    chatForm.addEventListener("submit", (event) => {
+        event.preventDefault();
+        if (chatInputElement.value.trim() === "") return;
+
+        sendMessageToAI();
+        chatInputElement.addEventListener("input", () => {
+            if (chatInputElement.value.trim() === "") {
+                chatSuggestionsElement.classList.remove("hidden");
+            } else {
+                chatSuggestionsElement.classList.add("hidden");
+            }
+        });
+    });
+}
+
+function setupBasicListeners() {
+    if (!checkoutButton) return;
+    checkoutButton.addEventListener("click", processOrder);
+
+}
+//#endregion
+//#region ===== Utilidad =====
+
 function createGlobalLoader() {
     if (document.getElementById('globalLoader')) return;
     globalLoaderElement = document.createElement('div');
@@ -61,39 +142,9 @@ function hideGlobalLoader() {
 function formatPrice(price) {
     return price.toLocaleString("es-CL", { style: "currency", currency: "CLP" });
 }
+//#endregion
+//#region ===== Productos =====
 
-window.processOrder = async () => {
-    if (cart.length === 0) return;
-    showGlobalLoader();
-    if (checkoutButton) {
-        checkoutButton.disabled = true;
-        checkoutButton.textContent = "Procesando...";
-    }
-
-    try {
-        const orderData = {
-            customerName: userName,
-            table: userTable,
-            items: cart.map(item => ({ id: item.id, name: item.name, quantity: item.quantity, price: item.price, itemTotal: item.price * item.quantity })),
-            totalAmount: cart.reduce((sum, item) => sum + item.price * item.quantity, 0),
-            orderDate: new Date().toLocaleString('sv-SE').replace(' ', 'T')
-        };
-        await sendOrder(orderData);
-        alert("Pedido enviado con éxito.");
-        cart = []
-        updateCartDisplay();
-    } catch (error) {
-        console.error("Error al procesar la orden:", error);
-        alert(`Hubo un problema: ${error.message || "Por favor, inténtalo de nuevo."}`);
-
-
-    } finally {
-        hideGlobalLoader();
-        checkoutButton.disabled = cart.length === 0;
-        checkoutButton.textContent = originalCheckoutButtonText
-
-    }
-}
 
 async function renderProducts() {
     if (!productListElement) return;
@@ -102,7 +153,8 @@ async function renderProducts() {
     if (!template) return;
 
     productListElement.innerHTML = "";
-    products = await getProducts();
+    console.log("Cargando productos...");
+    products = await getProducts(userToken);
 
     products.forEach(product => {
         const clone = template.content.cloneNode(true);
@@ -126,8 +178,11 @@ async function renderProducts() {
         });
     });
 }
+//#endregion
+//#region ===== Carrito =====
+
 
-window.addToCart = async (productId, buttonElement = null) => {
+async function addToCart (productId, buttonElement = null) {
     const product = products.find(p => p.id === productId);
     if (!product) return;
     const cartItem = cart.find(item => item.id === productId);
@@ -147,12 +202,12 @@ window.addToCart = async (productId, buttonElement = null) => {
         }, 300);
     }
     updateCartDisplay();
-    // Dentro de window.addToCart (después de updateCartDisplay())
+    // Dentro de addToCart (después de updateCartDisplay())
     if (typeof showToast === "function") showToast(`${product.name} agregado al carrito`);
 
 };
 
-window.removeFromCart = (productId, removeAll = false) => {
+async function removeFromCart (productId, removeAll = false) {
     const itemIndex = cart.findIndex(item => item.id === productId);
     if (itemIndex > -1) {
         if (removeAll || cart[itemIndex].quantity === 1) {
@@ -164,6 +219,12 @@ window.removeFromCart = (productId, removeAll = false) => {
     updateCartDisplay();
 };
 
+function calculateTotal() {
+    if (!cartTotalElement) return;
+    const total = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
+    cartTotalElement.textContent = formatPrice(total);
+}
+
 function updateCartDisplay() {
     if (!cartItemsElement || !emptyCartTextElement || !checkoutButton || !cartCountElement) return;
     cartItemsElement.innerHTML = "";
@@ -207,27 +268,82 @@ function updateCartDisplay() {
                         <p class="text-sm accent-red">${formatPrice(item.price * item.quantity)}</p>
                     </div>
                     <div class="flex items-center gap-1 sm:gap-2">
-                        <button onclick="addToCart(${item.id})" class="text-green-500 hover:text-green-400 text-lg sm:text-xl font-bold p-1 rounded-full hover:bg-gray-700 transition-colors">+</button>
-                        <button onclick="removeFromCart(${item.id})" class="text-yellow-500 hover:text-yellow-400 text-lg sm:text-xl font-bold p-1 rounded-full hover:bg-gray-700 transition-colors">-</button>
-                        <button onclick="removeFromCart(${item.id}, true)" class="text-red-500 hover:text-red-400 text-base sm:text-lg p-1 rounded-full hover:bg-gray-700 transition-colors">
+                        <button class="plus-button text-green-500 hover:text-green-400 text-lg sm:text-xl font-bold p-1 rounded-full hover:bg-gray-700 transition-colors">+</button>
+                        <button class="minus-button text-yellow-500 hover:text-yellow-400 text-lg sm:text-xl font-bold p-1 rounded-full hover:bg-gray-700 transition-colors">-</button>
+                        <button class="remove-button text-red-500 hover:text-red-400 text-base sm:text-lg p-1 rounded-full hover:bg-gray-700 transition-colors">
                             <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 sm:w-5 sm:h-5 pointer-events-none"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12.56 0c1.153 0 2.24.032 3.22.096M15 5.79V4.5A2.25 2.25 0 0012.75 2.25h-1.5A2.25 2.25 0 009 4.5v1.29m0 0L9 19.5M15 5.79l-1.5-1.5M9 5.79l1.5-1.5" /></svg>
                         </button>
                     </div>
                 </div>
             `;
             cartItemsElement.innerHTML += cartItemHTML;
+            const plusButton = cartItemsElement.querySelectorAll(".plus-button");
+            const minusButton = cartItemsElement.querySelectorAll(".minus-button");
+            const removeButton = cartItemsElement.querySelectorAll(".remove-button");
+            plusButton.forEach((btn, index) => {
+                btn.addEventListener("click", () => {
+                    addToCart(item.id);
+                    btn.classList.add("animate-pulse");
+                    setTimeout(() => btn.classList.remove("animate-pulse"), 300);
+                });
+            });
+            minusButton.forEach((btn, index) => {
+                btn.addEventListener("click", () => {
+                    removeFromCart(item.id);
+                    btn.classList.add("animate-pulse");
+                    setTimeout(() => btn.classList.remove("animate-pulse"), 300);
+                });
+            });
+            removeButton.forEach((btn, index) => {
+                btn.addEventListener("click", () => {
+                    removeFromCart(item.id, true);
+                    btn.classList.add("animate-pulse");
+                    setTimeout(() => btn.classList.remove("animate-pulse"), 300);
+                });
+            });
         });
+        cart
     }
     calculateTotal();
 }
+//#endregion
+//#region ===== Pedidos =====
+
+
+async function processOrder() {
+    if (cart.length === 0) return;
+    showGlobalLoader();
+    if (checkoutButton) {
+        checkoutButton.disabled = true;
+        checkoutButton.textContent = "Procesando...";
+    }
+
+    try {
+        const orderData = {
+            customerId: userId,
+            table: userTable,
+            items: cart.map(item => ({ id: item.id, quantity: item.quantity})),
+            totalAmount: cart.reduce((sum, item) => sum + item.price * item.quantity, 0),
+            orderDate: new Date().toLocaleString('sv-SE').replace(' ', 'T')
+        };
+        await sendOrder(orderData,userToken);
+        alert("Pedido enviado con éxito.");
+        cart = []
+        updateCartDisplay();
+    } catch (error) {
+        console.error("Error al procesar la orden:", error);
+        alert(`Hubo un problema: ${error.message || "Por favor, inténtalo de nuevo."}`);
 
-function calculateTotal() {
-    if (!cartTotalElement) return;
-    const total = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
-    cartTotalElement.textContent = formatPrice(total);
-}
 
-// --- Lógica del Chat ---
+    } finally {
+        hideGlobalLoader();
+        checkoutButton.disabled = cart.length === 0;
+        checkoutButton.textContent = originalCheckoutButtonText
+
+    }
+}
+//#endregion
+//#region ===== Chat =====
 function displayChatMessage(sender, message) {
     if (!chatMessagesElement) return;
     const bubbleClass = sender === "user" ? "chat-bubble-user" : "chat-bubble-ai";
@@ -256,12 +372,12 @@ async function sendMessageToAI() {
     aiLoadingIndicator.classList.remove("hidden");
 
     try {
-        const response = await serviceSendMessage(userInput, chatHistory, userName);
+        const response = await serviceSendMessage(userInput, chatHistory, userName, userToken);
         if (!response) {
             displayChatMessage("ai", "Hubo un problema al conectar con el Chef IA.");
         } else if (response === "not_init") {
-            if (await serviceInitializeChat()) {
-                const response = await serviceSendMessage(userInput, chatHistory, userName);
+            if (await initializeService()) {
+                const response = await serviceSendMessage(userInput, chatHistory, userName, userToken);
                 if (response) {
                     chatHistory = response.messageList;
                     displayChatMessage("ai", response.assistantResponse);
@@ -284,81 +400,16 @@ async function sendMessageToAI() {
     }
 }
 
-async function initializeApp() {
-    await renderProducts();
-    try {
-        if (await serviceInitializeChat()) {
-        } else {
-            console.warn("Chat AI no pudo inicializarse.");
-            displayChatMessage("ai", "No se pudo conectar con el Chef IA en este momento.");
-        }
-    } catch (error) {
-        console.error("Error durante la inicialización del Chat AI:", error);
-        displayChatMessage("ai", "Error al iniciar la IA.");
-    }
-
-    // #region Sugerencias
-    const chatSuggestions = Array.from(chatSuggestionsElement.children);
-
-    chatSuggestions.forEach(suggestion => {
-        suggestion.addEventListener("click", () => {
-            sendSuggestion(suggestion.querySelector(".chat-suggestion").textContent);
-        });
-    });
-}
-
-function popupConfig(){
-    const sessionModal = document.getElementById('sessionModal');
-    const sessionAcceptBtn = document.getElementById('sessionAcceptBtn');
-    const tableInput = document.getElementById('tableInput');
-    const clientCodeInput = document.getElementById('clientCodeInput');
-    sessionModal.classList.remove('hidden');
-    sessionAcceptBtn.addEventListener('click', async () => {
-        const mesa = parseInt(tableInput.value, 10);
-        const codigo = clientCodeInput.value.trim();
+//#endregion
 
-        if (!mesa || !codigo) {
-            alert('Por favor completa ambos campos.');
-            return;
-        }
-        const existUser = await existsUser(codigo);
-        if (!existUser.success) {
-            alert('El código de cliente no existe.');
-            return;
-        }
-        userName = existUser.userName;
-        //destruye el modal
-        sessionModal.remove();
-        userTable = mesa;
-        initializeApp();
-    });
-}
-
-function chatConfig() {
-  if (!chatForm) return;
-    chatForm.addEventListener("submit", (event) => {
-        event.preventDefault();
-        if (chatInputElement.value.trim() === "") return;
-
-        sendMessageToAI();
-        chatInputElement.addEventListener("input", () => {
-            if (chatInputElement.value.trim() === "") {
-                chatSuggestionsElement.classList.remove("hidden");
-            } else {
-                chatSuggestionsElement.classList.add("hidden");
-            }
-        });
-    });
-}
-
-// --- Event Listeners ---
+// --- APP initialization ---
 document.addEventListener("DOMContentLoaded", async () => {
 
     createGlobalLoader();
     updateCartDisplay();
-    chatConfig()
-    popupConfig();
-    // initializeApp();
+    initializeChat()
+    setupBasicListeners();
+    initializeLoginModal();
+    // initializeApp()
 
 });
-

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

@@ -0,0 +1,33 @@
+
+async function login(email,pin){
+  const response = await fetch("/api/users/login", {
+    method: "POST",
+    headers: {
+      "Content-Type": "application/json"
+    },
+    body: JSON.stringify({ email, pin })
+}
+  );
+  if (response.status == 404) {
+    const errorData = await response.json().catch(() => ({ message: "El usuario no fue encontrado." }));
+    throw new Error(errorData.message);
+  }else if (response.status == 401) {
+    const errorData = await response.json().catch(() => ({ message: "Credenciales incorrectas.", attempts: 0 }));
+    alert(errorData.message);
+    throw new Error(errorData.message);
+  }else if (response.status != 200) {
+    console.error(response.status, response.statusText);
+    const errorData = await response.json().catch(() => ({ message: "Error al iniciar sesión." }));
+    alert(errorData.message, `Intentos restantes: ${errorData.attempts_remaining || 0}`);
+    throw new Error(errorData.message);
+  }
+  const data = await response.json();
+  if (!data || !data.data.token) {
+    alert("Error al iniciar sesión.");
+    throw new Error("Error al iniciar sesión.");
+  }
+  return data;
+}
+
+
+export { login };

+ 30 - 0
public/main/js/service/chat.js

@@ -0,0 +1,30 @@
+
+async function sendMessage(message, messageList, userName, token) {
+  if (!token) { 
+    return "not_init";
+  }
+  messageList.push({ role: "user", content: message });
+  const cuerpo = { messages: messageList, user: userName }
+  const response = await fetch("/api/chat/completions", {
+    method: "POST",
+    headers: {
+      "Content-Type": "application/json",
+      "Authorization": `Bearer ${token}`
+    },
+    body: JSON.stringify(cuerpo)
+  });
+  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();
+  const assistantResponse = data.response;
+  if (assistantResponse) {
+    messageList.push({ role: "assistant", content: assistantResponse });
+    return { messageList, assistantResponse };
+  } else {
+    throw new Error("Respuesta vacía del asistente.");
+  }
+}
+
+export { sendMessage };

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

@@ -0,0 +1,39 @@
+
+async function sendOrder(order, token) {
+  console.log("Enviando orden:", order);
+  try {
+    const response = await fetch("/api/orders/send", {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+        "X-App-Token": token
+      },
+      body: JSON.stringify(order)
+    });
+    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 enviar la orden:", error);
+    throw error;
+  }
+}
+async function getProducts(token){
+  const response = await fetch("/api/products/", {
+    headers: {
+      "Content-Type": "application/json",
+      "Authorization": `Bearer ${token}`
+    }
+  });
+  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.products;
+}
+
+export {sendOrder, getProducts}

+ 0 - 0
public/styles.css → public/main/styles.css


+ 62 - 0
public/register/app.js

@@ -0,0 +1,62 @@
+function showRegisterModal() {
+            document.getElementById('registerModal').classList.remove('hidden');
+        }
+
+        function hideRegisterModal() {
+            document.getElementById('registerModal').classList.add('hidden');
+        }
+
+        // Formateo automático del RUT mientras se escribe
+        document.getElementById('rutInput').addEventListener('input', function(e) {
+            let rut = e.target.value.replace(/[^0-9kK]/g, '');
+            
+            if (rut.length > 1) {
+                let body = rut.slice(0, -1);
+                let dv = rut.slice(-1);
+                
+                if (body.length > 0) {
+                    // Agregar puntos cada 3 dígitos desde la derecha
+                    body = body.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
+                    rut = body + '-' + dv;
+                }
+            }
+            
+            e.target.value = rut;
+        });
+
+        // Manejo del envío del formulario
+        document.getElementById('registerForm').addEventListener('submit', function(e) {
+            e.preventDefault();
+            
+            const formData = new FormData(e.target);
+            const data = {
+                name: formData.get('name'),
+                email: formData.get('email'),
+                rut: formData.get('rut')
+            };
+            console.log('Datos del formulario:', data);
+            fetch('/api/users/register', {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json'
+                },
+                body: JSON.stringify(data)
+            })
+            .then(response => {
+                if (!response.ok) {
+                    return response.json().then(errorData => {
+                        throw new Error(errorData.message || 'Error al registrar el usuario.');
+                    });
+                }
+                return response.json();
+            })
+            .then(data => {
+                console.log('Registro exitoso:', data);
+                alert('Registro exitoso! Revisa tu correo electrónico para el PIN.');
+                hideRegisterModal();
+            })
+        });
+
+document.addEventListener('DOMContentLoaded', function() {
+    showRegisterModal();
+});

+ 74 - 0
public/register/index.html

@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<html lang="es">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Modal de Registro</title>
+    <script src="https://cdn.tailwindcss.com"></script>
+    <script src="/register/app.js" defer></script>
+</head>
+<body class="bg-gray-100 p-8">
+
+    <!-- === MODAL DE REGISTRO === -->
+    <div id="registerModal"
+         class="fixed hidden inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
+      <form id="registerForm" class="bg-white w-full max-w-md p-8 rounded-xl shadow-xl space-y-6">
+        <div class="text-center">
+          <h2 class="text-2xl font-bold text-gray-900">¡Regístrate!</h2>
+          <p id="registerMessage" class="text-sm text-gray-600 mt-2">
+            Crea tu cuenta para realizar pedidos
+          </p>
+        </div>
+
+        <div class="space-y-4">
+          <div>
+            <label for="nameInput" class="block text-sm font-medium text-gray-700 mb-2">
+              Nombre completo
+            </label>
+            <input id="nameInput"
+                   name="name"
+                   type="text"
+                    pattern="[A-Za-zÀ-ÿ\s]{2,}"
+                   class="w-full border border-gray-300 px-4 py-3 rounded-lg focus:ring-2 focus:ring-[#101419] focus:border-transparent outline-none transition-all"
+                   placeholder="Juan Pérez" 
+                   required />
+          </div>
+
+          <div>
+            <label for="emailRegisterInput" class="block text-sm font-medium text-gray-700 mb-2">
+              Correo electrónico
+            </label>
+            <input id="emailRegisterInput"
+                   name="email"
+                   type="email"
+                   class="w-full border border-gray-300 px-4 py-3 rounded-lg focus:ring-2 focus:ring-[#101419] focus:border-transparent outline-none transition-all"
+                   placeholder="tu@email.com" 
+                   required />
+          </div>
+
+          <div>
+            <label for="rutInput" class="block text-sm font-medium text-gray-700 mb-2">
+              RUT
+            </label>
+            <input id="rutInput"
+                   name="rut"
+                   type="text"
+                   maxlength="12"
+                   pattern="^\d{1,2}\.\d{3}\.\d{3}-[\dkK]$"
+                   class="w-full border border-gray-300 px-4 py-3 rounded-lg focus:ring-2 focus:ring-[#101419] focus:border-transparent outline-none transition-all"
+                   placeholder="12345678-9" 
+                   required />
+          </div>
+        </div>
+
+        <div class="flex gap-3">
+          <button id="registerAcceptBtn"
+                  type="submit"
+                  class="flex-1 bg-[#101419] hover:bg-[#37404a] text-white py-3 rounded-lg font-medium transition-colors duration-200 focus:ring-2 focus:ring-offset-2 focus:ring-[#101419]">
+            Registrarse
+          </button>
+        </div>
+      </form>
+    </div>
+</body>
+</html>

+ 0 - 0
public/register/styles.css


+ 1 - 0
routes/__init__.py

@@ -0,0 +1 @@
+# Routes module

+ 29 - 0
routes/chat.py

@@ -0,0 +1,29 @@
+from fastapi import Request, HTTPException, Depends
+from fastapi.responses import JSONResponse
+from httpx import get
+from models.chat import ChatCompletionRequest
+from services.openai_service.openai_service import generate_completion
+from services.logging_service import log_llm_response
+from auth.security import get_current_user
+import logging
+from fastapi import APIRouter
+logger = logging.getLogger(__name__)
+
+chat_router = APIRouter()
+
+
+@chat_router.post("/completions")
+async def chat_completions(request_data: ChatCompletionRequest, request: Request, current_user=Depends(get_current_user)):
+    """Get chat completions from OpenAI"""
+    # Uses session_token (which is the antiAbuseToken) as an identifier for logging
+    session_identifier = request.session.get("antiAbuseToken", "unknown_session")
+
+    try:
+        openai_response = await generate_completion(request_data.messages, session_identifier, current_user.nombre, current_user.email)
+        log_llm_response(request_data.user, openai_response)
+        return JSONResponse({"response": openai_response})
+    except HTTPException as e:
+        raise e
+    except Exception as e:
+        logger.error(f"Unexpected error in /api/chat/completions: {e}")
+        raise HTTPException(status_code=500, detail="Error interno del servidor al procesar el chat.")

+ 98 - 0
routes/orders.py

@@ -0,0 +1,98 @@
+from itertools import product
+from math import log
+import time
+from fastapi import HTTPException, APIRouter
+from fastapi.responses import JSONResponse
+from fudo import fudo
+from models.sells import OrderWeb
+from services.fudo_service import add_product_to_fudo
+from services.email_service import send_email
+from services.logging_service import log_order
+from impresora.printer import PrinterUSB
+from impresora.order import Order, Item
+from logging import getLogger
+from threading import Thread
+from services.data_service import ProductDataService, UserDataService
+from config.mails import PRINTER_DISCONNECTED_MAIL
+
+logger = getLogger(__name__)
+user_data_service = UserDataService()
+product_data_service = ProductDataService()
+printer_orders = []
+order_router = APIRouter()
+
+@order_router.post("/send")
+async def printer_order(order: OrderWeb):
+    """Process printer order"""
+    logger.info("Printer order received")
+    logger.info(order)
+
+    if not PrinterUSB.check_usb_port(0xfe6, 0x811e):
+        logger.error("Printer is not connected.")
+        email_thread = Thread(
+            target=send_email,
+            args=(PRINTER_DISCONNECTED_MAIL["subject"], PRINTER_DISCONNECTED_MAIL["body"], ["erwinjacimino2003@gmail.com", "mompyn@gmail.com"]),
+            daemon=True
+        )
+        email_thread.start()
+        logger.error("Email sent to admin about printer issue.")
+        return JSONResponse(status_code=424, content={"message": "Printer is not connected."})
+
+    
+    items = order.items
+    table = order.table
+    
+    if not items or not table:
+        return JSONResponse(status_code=400, content={"message": "Items and table are required."})
+    
+    if not isinstance(table, int):
+        return JSONResponse(status_code=400, content={"message": "Table must be an integer."})
+    
+    # Add products to Fudo
+    product_errors = []
+    for item in items:
+        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}.")
+    
+    if product_errors:
+        return JSONResponse(
+            status_code=424, 
+            content={"message": "Error adding products to table.", "errors": product_errors}
+        )
+    
+    user = user_data_service.get_by_id(order.customerId)
+    if not user:
+        return JSONResponse(status_code=404, content={"message": "User not found."})
+    products = product_data_service.get_products([item.id for item in items])
+    # Print order
+    printer_orders.append(Order(
+        user=user.nombre if user else "Unknown User",
+        items=[Item(product.name, product.price, item.quantity) for product, item in zip(products, items)]
+    ))
+    
+    
+    # Log order
+    log_order(user.nombre, order.table, order_date=order.orderDate, items=[product.name for product in products])
+    
+    return JSONResponse({"message": "Order processed successfully"})
+
+def order_thread():
+    """Thread to process orders"""
+    logger.info("Starting order thread")
+    while True:
+        if printer_orders:
+            order = printer_orders.pop(0)
+            logger.info(f"Processing order: {order}")
+            try:
+                printer = PrinterUSB(0xfe6, 0x811e)
+                if not printer.is_connected():
+                    logger.error("Printer is not connected.")
+                    continue
+                
+                # printer.print_order(order)
+                logger.info(f"Order printed: {order}")
+            except Exception as e:
+                logger.error(f"Error printing order: {e}")
+        time.sleep(1)  # Sleep to avoid busy waiting

+ 19 - 0
routes/products.py

@@ -0,0 +1,19 @@
+from math import prod
+from fastapi.responses import JSONResponse
+from auth.security import get_current_user
+from services.data_service import ProductDataService
+from logging import getLogger
+from fastapi import APIRouter, Depends
+logger = getLogger(__name__)
+
+product_data_service = ProductDataService()
+
+product_router = APIRouter()
+
+@product_router.get("/")
+async def get_products(current_user = Depends(get_current_user)):
+    """Get products"""
+    logger.debug(f"Current user: {current_user.email if current_user else 'Anonymous'}")
+    logger.info("Fetching all products")
+    all_products = list(map(lambda p: p.model_dump(), product_data_service.get_all()))
+    return JSONResponse({"products": all_products})

+ 2 - 0
routes/sells.py

@@ -0,0 +1,2 @@
+from pydantic import BaseModel
+

+ 27 - 0
routes/static.py

@@ -0,0 +1,27 @@
+import os
+from fastapi import HTTPException
+from fastapi.responses import HTMLResponse, FileResponse
+from fastapi.staticfiles import StaticFiles
+
+
+async def serve_app_html():
+    """Serve the main HTML file"""
+    index_path = os.path.join("public","main", "index.html")
+    if not os.path.exists(index_path):
+        raise HTTPException(status_code=404, detail="public/index.html not found.")
+    return FileResponse(index_path)
+
+async def serve_register_html():
+    """Serve the register HTML file"""
+    register_path = os.path.join("public", "register", "index.html")
+    if not os.path.exists(register_path):
+        raise HTTPException(status_code=404, detail="public/register/index.html not found.")
+    return FileResponse(register_path)
+
+def mount_register_static_files(app):
+    """Mount static files for the register page"""
+    app.mount("/register/", StaticFiles(directory="public/register", html=False), name="register_static")
+
+def mount_main_static_files(app):
+    """Mount static files"""
+    app.mount("/express/", StaticFiles(directory="public/main", html=False), name="public_root_assets")

+ 105 - 0
routes/users.py

@@ -0,0 +1,105 @@
+from logging import getLogger
+from fastapi import APIRouter
+from fastapi.responses import JSONResponse
+from httpx import RequestError
+import redis
+import config
+from models import user
+from models.user import  LoginRequest, UserIDRequest, RegisterUserRequest
+from services.data_service import UserDataService
+from cryptography.fernet import Fernet
+from config.settings import PIN_KEY
+from auth.security import generate_token
+from services.email_service import send_email
+from config.mails import REGISTER_MAIL
+from config.settings import APPNAME
+fernet = Fernet(PIN_KEY.encode())
+logger = getLogger(__name__)
+user_data_service = UserDataService()
+
+user_router = APIRouter()
+
+
+def unique_pin_generate():
+    """Generate a unique 4-digit PIN"""
+    import random
+    pin = str(random.randint(1000, 9999))
+    return pin
+
+@user_router.post("/exists")
+async def exists_user(request: UserIDRequest):
+        """Check if user exists"""
+        user = user_data_service.get_by_id(request.id)
+        if user:
+            return JSONResponse(status_code=200, content={"exists": True})
+        else:
+            return JSONResponse(status_code=404, content={"exists": False, "data": {"message": "User does not exist."}})
+
+@user_router.post("/register")
+async def register_user(request: RegisterUserRequest):
+    """Register a new user"""
+    pin = unique_pin_generate()
+    userID = user_data_service.create(request.name, request.email, request.rut, pin)
+    if userID == -1:
+        return JSONResponse(status_code=400, content={"message": "User already exists."})
+    user = user_data_service.get_by_id(userID)
+    if not user:
+        return JSONResponse(status_code=500, content={"message": "Error creating user."})
+    send_email(
+        REGISTER_MAIL["subject"],
+        REGISTER_MAIL["body"],
+        [user.email], name=user.nombre, app_name=APPNAME, pin=pin
+    )
+    return JSONResponse(status_code=201, content={"message": "User created successfully.", "data": {
+        **user.model_dump(exclude={"pin_hash"}),
+        "token": generate_token(user.email)
+    }})
+
+@user_router.post("/login")
+async def login_user(request: LoginRequest):
+    """Login user with email and PIN"""
+    logger.debug(f"Login attempt for email: {request.email}")
+    user = user_data_service.login(request.email, request.pin)
+    if user:
+        # Successful login, return user data and token
+        return JSONResponse(status_code=200, content={"message": "Login successful.", "data": {
+            "id": user.id,
+            "name": user.nombre,
+            "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 = redis.Redis(host='localhost', port=6379, db=0)
+        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
+            return JSONResponse(status_code=429, content={"message": "Too many login attempts. Please try again later."})
+        else:
+            # Set flag for last failed login
+            redis_client.set(f"last_failed_login:{request.email}", "true")
+            redis_client.expire(f"last_failed_login:{request.email}", 3600)
+            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": "Invalid email or PIN.", "attempts_remaining": 5 - attempts if attempts else 5})
+
+@user_router.delete("/delete")
+async def delete_user(request: UserIDRequest):
+    """Delete a user by ID"""
+    user = user_data_service.delete(request.id)
+    if user:
+        return JSONResponse(status_code=200, content={"message": "User deleted successfully.", "data": user})
+    else:
+        return JSONResponse(status_code=404, content={"message": "User not found."})
+
+@user_router.get("/all")
+async def get_all_users():
+    """Get all users"""
+    users = list(map(lambda u: u.model_dump(), user_data_service.get_all()))
+    return JSONResponse(status_code=200, content={"data": users})

+ 1 - 0
services/__init__.py

@@ -0,0 +1 @@
+# Services module

+ 1184 - 0
services/data_service.py

@@ -0,0 +1,1184 @@
+import json
+from math import log
+import os
+import sqlite3
+from typing import List, Dict, Optional, Any, Union
+from abc import ABC, abstractmethod
+from config.settings import BG_DATA_PATH, DB_PATH, PRODUCT_DATA_PATH
+from logging import getLogger
+from datetime import datetime
+import uuid
+from cryptography.fernet import Fernet
+from config.settings import PIN_KEY
+
+# Import models
+from models.user import User
+from models.items import Product, ProductWithQuantity
+from models.sells import Sale
+from models.blacklist import Blacklist
+
+fernet = Fernet(PIN_KEY.encode())
+
+logger = getLogger(__name__)
+
+"""
+ESQUEMA DE BASE DE DATOS SQLITE (data.db)
+
+1. Tabla: users
+-----------------------------------
+- id           INTEGER PRIMARY KEY AUTOINCREMENT
+- email       TEXT UNIQUE NOT NULL
+- nombre       TEXT NOT NULL
+- rut          TEXT UNIQUE NOT NULL
+- pin_hash     TEXT NOT NULL (encriptado)
+- kleincoins   TEXT NOT NULL (encriptado, valor por defecto "0")
+- created_at   TEXT NOT NULL (fecha de creación en formato ISO 8601)
+(Guarda la información del usuario con su pin hasheado y kleincoins encriptadas)
+
+2. Tabla: productos
+-----------------------------------
+- id           INTEGER PRIMARY KEY
+- name         TEXT NOT NULL
+- type         TEXT
+- description  TEXT
+- price        REAL NOT NULL
+- image        TEXT (URL de la imagen)
+- status       INTEGER DEFAULT 1 NOT NULL CHECK (status IN (0, 1)) -- 0: Inactivo, 1: Activo
+(Guarda los productos disponibles para venta con su estado activo/inactivo)
+
+3. Tabla: ventas
+-----------------------------------
+- id           INTEGER PRIMARY KEY AUTOINCREMENT
+- user_id      INTEGER NOT NULL (relación a users.id)
+- total        REAL NOT NULL (precio total de la venta)
+- fudo_id      TEXT UNIQUE NOT NULL (ID string único por venta)
+- fecha        TEXT NOT NULL (fecha y hora en formato ISO 8601)
+- table        INTEGER NOT NULL (número de mesa)
+(Guarda cada venta, asociada a un usuario y mesa)
+
+4. Tabla: venta_productos
+-----------------------------------
+- venta_id     INTEGER NOT NULL (relación a ventas.id)
+- producto_id  INTEGER NOT NULL (relación a productos.id)
+- cantidad     INTEGER NOT NULL DEFAULT 1 (cantidad del producto)
+(Relación muchos a muchos entre ventas y productos con cantidad)
+
+5. Tabla: blacklist
+-----------------------------------
+- id           INTEGER PRIMARY KEY AUTOINCREMENT
+- user_id      INTEGER NOT NULL (relación a users.id)
+(Usuarios bloqueados o no autorizados para ciertas acciones)
+
+RELACIONES:
+-----------------------------------
+- users puede tener muchas ventas
+- ventas puede tener muchos productos (y viceversa), por eso se usa una tabla intermedia (venta_productos)
+- productos pueden repetirse en múltiples ventas
+"""
+
+# Base abstract class for data access
+class BaseDataService(ABC):
+    """Abstract base class for data services"""
+    
+    def __init__(self, db_path: str = DB_PATH):
+        self.db_path = db_path
+    
+    def _get_connection(self) -> sqlite3.Connection:
+        """Get database connection"""
+        return sqlite3.connect(self.db_path)
+    
+    @abstractmethod
+    def get_all(self) -> List[Any]:
+        """Get all records"""
+        pass
+    
+    @abstractmethod
+    def get_by_id(self, id: int) -> Optional[Any]:
+        """Get record by ID"""
+        pass
+    
+    @abstractmethod
+    def create(self, **kwargs) -> int:
+        """Create new record"""
+        pass
+    
+    @abstractmethod
+    def update(self, id: int, **kwargs) -> bool:
+        """Update record"""
+        pass
+    
+    @abstractmethod
+    def delete(self, id: int) -> bool:
+        """Delete record"""
+        pass
+
+
+# User Data Service
+class UserDataService(BaseDataService):
+    """Service for managing user data"""
+    #region Create
+    def create(self, nombre: str, email: str, rut: str, pin_hash: str) -> int:
+        """Add a new user to the database"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        try:
+            cursor.execute(
+                "INSERT INTO users (nombre, email, rut, pin_hash, kleincoins, created_at) VALUES (?, ?, ?, ?, ?, ?)",
+                (nombre, email, rut, fernet.encrypt(pin_hash.encode()).decode(), fernet.encrypt(b"0").decode(), datetime.now().isoformat())
+            )
+            conn.commit()
+            user_id = cursor.lastrowid
+            if user_id:
+                logger.info(f"User added with ID: {user_id}")
+                return user_id
+            else:
+                logger.error("Failed to add user.")
+                return -1
+        except sqlite3.IntegrityError as e:
+            logger.error(f"Failed to add user: {e}")
+            return -1
+        finally:
+            conn.close()
+    #endregion
+    #region Read
+    def get_all(self) -> List[User]:
+        """Get all users from the database"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute("SELECT * FROM users")
+        users = cursor.fetchall()
+        conn.close()
+        return [
+            User(
+                id=user[0],
+                email=user[1],
+                nombre=user[2],
+                rut=user[3],
+                pin_hash=fernet.decrypt(user[4].encode()).decode(),
+                kleincoins=fernet.decrypt(user[5].encode()).decode(),
+                created_at=user[6]
+            ) for user in users
+        ]
+    
+    def get_by_id(self, user_id: int) -> Optional[User]:
+        """Get user data from the database"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
+        user = cursor.fetchone()
+        conn.close()
+        if user:
+            return User(
+                id=user[0],
+                email=user[1],
+                nombre=user[2],
+                rut=user[3],
+                pin_hash=fernet.decrypt(user[4].encode()).decode(),
+                kleincoins=fernet.decrypt(user[5].encode()).decode(),
+                created_at=user[6]
+            )
+        return None
+    
+    def get_by_email(self, email: str) -> Optional[User]:
+        """Get user by email"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute("SELECT * FROM users WHERE email = ?", (email,))
+        user = cursor.fetchone()
+        conn.close()
+        logger.debug(f"get_by_email: {email}, user: {user}")
+        if user:
+            return User(
+                id=user[0],
+                email=user[1],
+                nombre=user[2],
+                rut=user[3],
+                pin_hash=user[4],
+                kleincoins=fernet.decrypt(user[5].encode()).decode(),
+                created_at=user[6]
+            )
+        return None
+    
+    def login(self, email: str, pin_hashed: str) -> Optional[User]:
+        """Login user by email and pin hash"""
+        user = self.get_by_email(email)
+        if user and fernet.decrypt(user.pin_hash.encode()).decode() == pin_hashed:
+            logger.info(f"User {user.email} logged in successfully.")
+            return user
+        else:
+            logger.error("Login failed: Invalid email or pin.")
+            return None
+
+
+    def get_by_rut(self, rut: str) -> Optional[User]:
+        """Get user by RUT"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute("SELECT * FROM users WHERE rut = ?", (rut,))
+        user = cursor.fetchone()
+        conn.close()
+        if user:
+            return User(
+                id=user[0],
+                email=user[1],
+                nombre=user[2],
+                rut=user[3],
+                pin_hash=fernet.decrypt(user[4].encode()).decode(),
+                kleincoins=fernet.decrypt(user[5].encode()).decode(),
+                created_at=user[6]
+            )
+        return None
+    #endregion
+    #region Update
+    def update(self, user_id: int, email=None, nombre=None, rut=None, pin_hash=None, kleincoins=None) -> bool:
+        """Update user information in the database"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        updates = []
+        params = []
+        if email:
+            updates.append("email = ?")
+            params.append(email)
+        if nombre:
+            updates.append("nombre = ?")
+            params.append(nombre)
+        if rut:
+            updates.append("rut = ?")
+            params.append(rut)
+        if pin_hash:
+            updates.append("pin_hash = ?")
+            params.append(fernet.encrypt(pin_hash.encode()).decode())
+        if kleincoins is not None:
+            updates.append("kleincoins = ?")
+            params.append(fernet.encrypt(str(kleincoins).encode()).decode())
+        if not updates:
+            conn.close()
+            return False
+        try:
+            cursor.execute(f"UPDATE users SET {', '.join(updates)} WHERE id = ?", (*params, user_id))
+            conn.commit()
+            success = cursor.rowcount > 0
+            if success:
+                logger.info(f"User with ID {user_id} updated.")
+            return success
+        except sqlite3.IntegrityError as e:
+            logger.error(f"Failed to update user: {e}")
+            return False
+        finally:
+            conn.close()
+        #endregion
+    #region Delete
+    def delete(self, user_id: int) -> bool:
+        """Delete a user from the database"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute("DELETE FROM users WHERE id = ?", (user_id,))
+        conn.commit()
+        conn.close()
+        if cursor.rowcount > 0:
+            logger.info(f"User with ID {user_id} deleted.")
+            return True
+        else:
+            logger.error(f"Failed to delete user with ID {user_id}.")
+            return False
+    
+    def update_kleincoins(self, user_id: int, kleincoins: int) -> bool:
+        """Update user's kleincoins"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        try:
+            cursor.execute(
+                "UPDATE users SET kleincoins = ? WHERE id = ?", 
+                (fernet.encrypt(str(kleincoins).encode()).decode(), user_id)
+            )
+            conn.commit()
+            success = cursor.rowcount > 0
+            if success:
+                logger.info(f"Kleincoins updated for user {user_id}: {kleincoins}")
+            return success
+        except sqlite3.IntegrityError as e:
+            logger.error(f"Failed to update kleincoins: {e}")
+            return False
+        finally:
+            conn.close()
+    
+    def get_kleincoins(self, user_id: int) -> Optional[int]:
+        """Get user's kleincoins"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute("SELECT kleincoins FROM users WHERE id = ?", (user_id,))
+        result = cursor.fetchone()
+        conn.close()
+        if result:
+            try:
+                return int(fernet.decrypt(result[0].encode()).decode())
+            except Exception as e:
+                logger.error(f"Failed to decrypt kleincoins for user {user_id}: {e}")
+                return None
+        return None
+    #endregion
+
+# Blacklist Data Service
+class BlacklistDataService(BaseDataService):
+    """Service for managing blacklisted users"""
+    #region Create
+    def create(self, user_id: int) -> int:
+        """Add a user to the blacklist"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        try:
+            cursor.execute("INSERT INTO blacklist (user_id) VALUES (?)", (user_id,))
+            conn.commit()
+            blacklist_id = cursor.lastrowid
+            if blacklist_id:
+                logger.info(f"User with ID {user_id} added to blacklist.")
+                return blacklist_id
+            else:
+                logger.error(f"Failed to add user with ID {user_id} to blacklist.")
+                return -1
+        except sqlite3.IntegrityError as e:
+            logger.error(f"Failed to add user to blacklist: {e}")
+            return -1
+        finally:
+            conn.close()
+    #endregion
+    #region Read
+    def get_all(self) -> List[Blacklist]:
+        """Get all blacklisted users"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute("""
+            SELECT b.id, b.user_id, u.email, u.nombre, u.rut 
+            FROM blacklist b 
+            LEFT JOIN users u ON b.user_id = u.id
+        """)
+        blacklisted = cursor.fetchall()
+        conn.close()
+        return [
+            Blacklist(
+                id=row[0],
+                user_id=row[1],
+                email=row[2],
+                nombre=row[3],
+                rut=row[4]
+            ) for row in blacklisted
+        ]
+    
+    def get_by_id(self, id: int) -> Optional[Blacklist]:
+        """Get blacklist entry by ID"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute("""
+            SELECT b.id, b.user_id, u.email, u.nombre, u.rut 
+            FROM blacklist b 
+            LEFT JOIN users u ON b.user_id = u.id 
+            WHERE b.id = ?
+        """, (id,))
+        row = cursor.fetchone()
+        conn.close()
+        if row:
+            return Blacklist(
+                id=row[0],
+                user_id=row[1],
+                email=row[2],
+                nombre=row[3],
+                rut=row[4]
+            )
+        return None
+    
+    def get_blacklisted_user_ids(self) -> List[int]:
+        """Get a list of blacklisted user IDs"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute("SELECT user_id FROM blacklist")
+        blacklisted_users = [row[0] for row in cursor.fetchall()]
+        conn.close()
+        return blacklisted_users
+    
+    def is_user_blacklisted(self, user_id: int) -> bool:
+        """Check if a user is blacklisted"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute("SELECT * FROM blacklist WHERE user_id = ?", (user_id,))
+        blacklisted = cursor.fetchone() is not None
+        conn.close()
+        return blacklisted
+    #endregion
+    #region Update
+    def update(self, id: int, **kwargs) -> bool:
+        """Update blacklist entry (not commonly used)"""
+        # Blacklist entries typically don't need updates
+        return False
+    #endregion
+    #region Delete
+    def delete(self, id: int) -> bool:
+        """Remove a blacklist entry by ID"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute("DELETE FROM blacklist WHERE id = ?", (id,))
+        conn.commit()
+        success = cursor.rowcount > 0
+        conn.close()
+        if success:
+            logger.info(f"Blacklist entry with ID {id} removed.")
+        else:
+            logger.error(f"Failed to remove blacklist entry with ID {id}.")
+        return success
+    
+    def remove_user_from_blacklist(self, user_id: int) -> bool:
+        """Remove a user from the blacklist"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute("DELETE FROM blacklist WHERE user_id = ?", (user_id,))
+        conn.commit()
+        success = cursor.rowcount > 0
+        conn.close()
+        if success:
+            logger.info(f"User with ID {user_id} removed from blacklist.")
+        else:
+            logger.error(f"Failed to remove user with ID {user_id} from blacklist.")
+        return success
+    #endregion
+
+# Product Data Service
+class ProductDataService(BaseDataService):
+    """Service for managing products"""
+    #region Create
+    def create(self, id: int, name: str, price: float, type: Optional[str] = None, description: Optional[str] = None, image: Optional[str] = None, status: int = 1) -> int:
+        """Add a new product to the database"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        try:
+            cursor.execute(
+                "INSERT INTO productos (id, name, type, description, price, image, status) VALUES (?, ?, ?, ?, ?, ?, ?)",
+                (id, name, type, description, price, image, status)
+            )
+            conn.commit()
+            product_id = cursor.lastrowid
+            if product_id:
+                logger.info(f"Product added with ID: {product_id}")
+                return product_id
+            else:
+                logger.error("Failed to add product.")
+                return -1
+        except sqlite3.Error as e:
+            logger.error(f"Failed to add product: {e}")
+            return -1
+        finally:
+            conn.close()
+    def load_data(self, products: List[Dict[str, str]]) -> None:
+        """Load multiple products from a list of dictionaries"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        for product in products:
+            try:
+                self.create(
+                    id=int(product['id']),
+                    name=product['name'],
+                    price=int(product['price']),
+                    type=product.get('type'),
+                    description=product.get('description'),
+                    image=product.get('image'),
+                    status=int(product.get('status', 1))  # Default to active (1)
+                )
+            except Exception as e:
+                logger.error(f"Failed to load product {product['name']}: {e}")
+        conn.close()
+    #endregion
+    #region Read
+    def get_all(self) -> List[Product]:
+        """Get all products from the database"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute("SELECT * FROM productos")
+        products = cursor.fetchall()
+        conn.close()
+        return [
+            Product(
+                id=product[0],
+                name=product[1],
+                type=product[2],
+                description=product[3],
+                price=product[4],
+                image=product[5],
+                status=product[6]
+            ) for product in products
+        ]
+    
+    def get_by_id(self, product_id: int) -> Optional[Product]:
+        """Get product by ID"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute("SELECT * FROM productos WHERE id = ?", (product_id,))
+        product = cursor.fetchone()
+        conn.close()
+        if product:
+            return Product(
+                id=product[0],
+                name=product[1],
+                type=product[2],
+                description=product[3],
+                price=product[4],
+                image=product[5],
+                status=product[6]
+            )
+        return None
+    
+    def get_by_type(self, product_type: str) -> List[Product]:
+        """Get products by type"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute("SELECT * FROM productos WHERE type = ?", (product_type,))
+        products = cursor.fetchall()
+        conn.close()
+        return [
+            Product(
+                id=product[0],
+                name=product[1],
+                type=product[2],
+                description=product[3],
+                price=product[4],
+                image=product[5],
+                status=product[6]
+            ) for product in products
+        ]
+    
+    def search_by_name(self, name: str) -> List[Product]:
+        """Search products by name"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute("SELECT * FROM productos WHERE name LIKE ?", (f"%{name}%",))
+        products = cursor.fetchall()
+        conn.close()
+        return [
+            Product(
+                id=product[0],
+                name=product[1],
+                type=product[2],
+                description=product[3],
+                price=product[4],
+                image=product[5],
+                status=product[6]
+            ) for product in products
+        ]
+    
+    def get_products(self, product_ids: List[int]) -> List[Product]:
+        """Get multiple products by their IDs"""
+        if not product_ids:
+            return []
+        placeholders = ', '.join('?' for _ in product_ids)
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute(f"SELECT * FROM productos WHERE id IN ({placeholders})", product_ids)
+        products = cursor.fetchall()
+        conn.close()
+        return [
+            Product(
+                id=product[0],
+                name=product[1],
+                type=product[2],
+                description=product[3],
+                price=product[4],
+                image=product[5],
+                status=product[6]
+            ) for product in products
+        ]
+    #endregion
+    #region Update
+    def update(self, product_id: int, name=None, type=None, description=None, price=None, image=None, status=None) -> bool:
+        """Update product information"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        updates = []
+        params = []
+        if name is not None:
+            updates.append("name = ?")
+            params.append(name)
+        if type is not None:
+            updates.append("type = ?")
+            params.append(type)
+        if description is not None:
+            updates.append("description = ?")
+            params.append(description)
+        if price is not None:
+            updates.append("price = ?")
+            params.append(price)
+        if image is not None:
+            updates.append("image = ?")
+            params.append(image)
+        if status is not None:
+            updates.append("status = ?")
+            params.append(status)
+        if not updates:
+            conn.close()
+            return False
+        try:
+            cursor.execute(f"UPDATE productos SET {', '.join(updates)} WHERE id = ?", (*params, product_id))
+            conn.commit()
+            success = cursor.rowcount > 0
+            if success:
+                logger.info(f"Product with ID {product_id} updated.")
+            return success
+        except sqlite3.Error as e:
+            logger.error(f"Failed to update product: {e}")
+            return False
+        finally:
+            conn.close()
+    
+    def get_active_products(self) -> List[Product]:
+        """Get only active products (status = 1)"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute("SELECT * FROM productos WHERE status = 1")
+        products = cursor.fetchall()
+        conn.close()
+        return [
+            Product(
+                id=product[0],
+                name=product[1],
+                type=product[2],
+                description=product[3],
+                price=product[4],
+                image=product[5],
+                status=product[6]
+            ) for product in products
+        ]
+    
+    def get_inactive_products(self) -> List[Product]:
+        """Get only inactive products (status = 0)"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute("SELECT * FROM productos WHERE status = 0")
+        products = cursor.fetchall()
+        conn.close()
+        return [
+            Product(
+                id=product[0],
+                name=product[1],
+                type=product[2],
+                description=product[3],
+                price=product[4],
+                image=product[5],
+                status=product[6]
+            ) for product in products
+        ]
+    
+    def activate_product(self, product_id: int) -> bool:
+        """Activate a product (set status to 1)"""
+        return self.update(product_id, status=1)
+    
+    def deactivate_product(self, product_id: int) -> bool:
+        """Deactivate a product (set status to 0)"""
+        return self.update(product_id, status=0)
+    
+    def is_product_active(self, product_id: int) -> Optional[bool]:
+        """Check if a product is active"""
+        product = self.get_by_id(product_id)
+        if product:
+            return product.status == 1
+        return None
+        #endregion
+    #region Delete
+    def delete(self, product_id: int) -> bool:
+        """Delete a product from the database"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute("DELETE FROM productos WHERE id = ?", (product_id,))
+        conn.commit()
+        success = cursor.rowcount > 0
+        conn.close()
+        if success:
+            logger.info(f"Product with ID {product_id} deleted.")
+        else:
+            logger.error(f"Failed to delete product with ID {product_id}.")
+        return success
+    #endregion
+
+# Sales Data Service
+class SalesDataService(BaseDataService):
+    """Service for managing sales"""
+    #region C
+    def create(self, user_id: int,fudo_id:str, total: float, table: int, product_ids: List[int], quantities: Optional[List[int]] = None) -> int:
+        """Create a new sale with products and quantities"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        try:
+            fecha = datetime.now().isoformat()
+            
+            # Insert sale
+            cursor.execute(
+                "INSERT INTO ventas (user_id, total, fudo_id, fecha, table) VALUES (?, ?, ?, ?, ?)",
+                (user_id, total, fudo_id, fecha, table)
+            )
+            sale_id = cursor.lastrowid
+            
+            if sale_id and product_ids:
+                # Insert sale-product relationships with quantities
+                if quantities is None:
+                    quantities = [1] * len(product_ids)  # Default quantity 1
+                
+                for i, product_id in enumerate(product_ids):
+                    quantity = quantities[i] if i < len(quantities) else 1
+                    cursor.execute(
+                        "INSERT INTO venta_productos (venta_id, producto_id, cantidad) VALUES (?, ?, ?)",
+                        (sale_id, product_id, quantity)
+                    )
+            
+            conn.commit()
+            if sale_id:
+                logger.info(f"Sale created with ID: {sale_id}, fudo_id: {fudo_id}")
+                return sale_id
+            else:
+                logger.error("Failed to create sale.")
+                return -1
+        except sqlite3.Error as e:
+            logger.error(f"Failed to create sale: {e}")
+            conn.rollback()
+            return -1
+        finally:
+            conn.close()
+    
+
+    def add_product_to_sale(self, sale_id: int, product_id: int, quantity: int = 1) -> bool:
+        """Add a product to an existing sale with quantity"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        try:
+            cursor.execute(
+                "INSERT INTO venta_productos (venta_id, producto_id, cantidad) VALUES (?, ?, ?)",
+                (sale_id, product_id, quantity)
+            )
+            conn.commit()
+            success = cursor.rowcount > 0
+            if success:
+                logger.info(f"Product {product_id} (qty: {quantity}) added to sale {sale_id}.")
+            return success
+        except sqlite3.IntegrityError as e:
+            logger.error(f"Failed to add product to sale: {e}")
+            return False
+        finally:
+            conn.close()
+    
+    #endregion
+    #region R
+    def get_all(self) -> List[Sale]:
+        """Get all sales from the database"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute("""
+            SELECT v.id, v.user_id, v.total, v.fudo_id, v.fecha, v.table, u.nombre, u.email
+            FROM ventas v
+            LEFT JOIN users u ON v.user_id = u.id
+            ORDER BY v.fecha DESC
+        """)
+        sales = cursor.fetchall()
+        conn.close()
+        return [
+            Sale(
+                id=sale[0],
+                user_id=sale[1],
+                total=sale[2],
+                fudo_id=sale[3],
+                fecha=sale[4],
+                table=sale[5],
+                user_name=sale[6],
+                user_email=sale[7]
+            ) for sale in sales
+        ]
+    
+    def get_by_id(self, sale_id: int) -> Optional[Sale]:
+        """Get sale by ID"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute("""
+            SELECT v.id, v.user_id, v.total, v.fudo_id, v.fecha, v.table, u.nombre, u.email
+            FROM ventas v
+            LEFT JOIN users u ON v.user_id = u.id
+            WHERE v.id = ?
+        """, (sale_id,))
+        sale = cursor.fetchone()
+        conn.close()
+        if sale:
+            return Sale(
+                id=sale[0],
+                user_id=sale[1],
+                total=sale[2],
+                fudo_id=sale[3],
+                fecha=sale[4],
+                table=sale[5],
+                user_name=sale[6],
+                user_email=sale[7]
+            )
+        return None
+    
+    def get_by_fudo_id(self, fudo_id: str) -> Optional[Sale]:
+        """Get sale by fudo_id"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute("""
+            SELECT v.id, v.user_id, v.total, v.fudo_id, v.fecha, v.table, u.nombre, u.email
+            FROM ventas v
+            LEFT JOIN users u ON v.user_id = u.id
+            WHERE v.fudo_id = ?
+        """, (fudo_id,))
+        sale = cursor.fetchone()
+        conn.close()
+        if sale:
+            return Sale(
+                id=sale[0],
+                user_id=sale[1],
+                total=sale[2],
+                fudo_id=sale[3],
+                fecha=sale[4],
+                table=sale[5],
+                user_name=sale[6],
+                user_email=sale[7]
+            )
+        return None
+    
+    def get_by_user(self, user_id: int) -> List[Sale]:
+        """Get sales by user ID"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute("""
+            SELECT v.id, v.user_id, v.total, v.fudo_id, v.fecha, v.table, u.nombre, u.email
+            FROM ventas v
+            LEFT JOIN users u ON v.user_id = u.id
+            WHERE v.user_id = ?
+            ORDER BY v.fecha DESC
+        """, (user_id,))
+        sales = cursor.fetchall()
+        conn.close()
+        return [
+            Sale(
+                id=sale[0],
+                user_id=sale[1],
+                total=sale[2],
+                fudo_id=sale[3],
+                fecha=sale[4],
+                table=sale[5],
+                user_name=sale[6],
+                user_email=sale[7]
+            ) for sale in sales
+        ]
+    
+    def get_sale_products(self, sale_id: int) -> List[ProductWithQuantity]:
+        """Get products for a specific sale with quantities"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute("""
+            SELECT p.id, p.name, p.type, p.description, p.price, p.image, p.status, vp.cantidad
+            FROM venta_productos vp
+            JOIN productos p ON vp.producto_id = p.id
+            WHERE vp.venta_id = ?
+        """, (sale_id,))
+        products = cursor.fetchall()
+        conn.close()
+        return [
+            ProductWithQuantity(
+                id=product[0],
+                name=product[1],
+                type=product[2],
+                description=product[3],
+                price=product[4],
+                image=product[5],
+                status=product[6],
+                cantidad=product[7]
+            ) for product in products
+        ]
+    
+    def get_by_table(self, table: int) -> List[Sale]:
+        """Get sales by table number"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute("""
+            SELECT v.id, v.user_id, v.total, v.fudo_id, v.fecha, v.table, u.nombre, u.email
+            FROM ventas v
+            LEFT JOIN users u ON v.user_id = u.id
+            WHERE v.table = ?
+            ORDER BY v.fecha DESC
+        """, (table,))
+        sales = cursor.fetchall()
+        conn.close()
+        return [
+            Sale(
+                id=sale[0],
+                user_id=sale[1],
+                total=sale[2],
+                fudo_id=sale[3],
+                fecha=sale[4],
+                table=sale[5],
+                user_name=sale[6],
+                user_email=sale[7]
+            ) for sale in sales
+        ]
+    
+    def update_product_quantity(self, sale_id: int, product_id: int, new_quantity: int) -> bool:
+        """Update quantity of a product in a sale"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        try:
+            cursor.execute(
+                "UPDATE venta_productos SET cantidad = ? WHERE venta_id = ? AND producto_id = ?",
+                (new_quantity, sale_id, product_id)
+            )
+            conn.commit()
+            success = cursor.rowcount > 0
+            if success:
+                logger.info(f"Updated quantity to {new_quantity} for product {product_id} in sale {sale_id}.")
+            return success
+        except sqlite3.Error as e:
+            logger.error(f"Failed to update product quantity: {e}")
+            return False
+        finally:
+            conn.close()
+    
+    def get_product_quantity_in_sale(self, sale_id: int, product_id: int) -> Optional[int]:
+        """Get quantity of a specific product in a sale"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute(
+            "SELECT cantidad FROM venta_productos WHERE venta_id = ? AND producto_id = ?",
+            (sale_id, product_id)
+        )
+        result = cursor.fetchone()
+        conn.close()
+        return result[0] if result else None
+    #endregion
+    #region U
+    def update(self, sale_id: int, user_id=None, total=None, table=None) -> bool:
+        """Update sale information (products should be updated via specific methods)"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        updates = []
+        params = []
+        if user_id is not None:
+            updates.append("user_id = ?")
+            params.append(user_id)
+        if total is not None:
+            updates.append("total = ?")
+            params.append(total)
+        if table is not None:
+            updates.append("table = ?")
+            params.append(table)
+        if not updates:
+            conn.close()
+            return False
+        try:
+            cursor.execute(f"UPDATE ventas SET {', '.join(updates)} WHERE id = ?", (*params, sale_id))
+            conn.commit()
+            success = cursor.rowcount > 0
+            if success:
+                logger.info(f"Sale with ID {sale_id} updated.")
+            return success
+        except sqlite3.Error as e:
+            logger.error(f"Failed to update sale: {e}")
+            return False
+        finally:
+            conn.close()
+        #endregion
+    #region D
+    def delete(self, sale_id: int) -> bool:
+        """Delete a sale and its product relationships"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        try:
+            # Delete sale-product relationships first
+            cursor.execute("DELETE FROM venta_productos WHERE venta_id = ?", (sale_id,))
+            # Delete the sale
+            cursor.execute("DELETE FROM ventas WHERE id = ?", (sale_id,))
+            conn.commit()
+            success = cursor.rowcount > 0
+            if success:
+                logger.info(f"Sale with ID {sale_id} deleted.")
+            else:
+                logger.error(f"Failed to delete sale with ID {sale_id}.")
+            return success
+        except sqlite3.Error as e:
+            logger.error(f"Failed to delete sale: {e}")
+            return False
+        finally:
+            conn.close()
+    
+    def remove_product_from_sale(self, sale_id: int, product_id: int) -> bool:
+        """Remove a product from a sale (removes all quantity)"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute(
+            "DELETE FROM venta_productos WHERE venta_id = ? AND producto_id = ?",
+            (sale_id, product_id)
+        )
+        conn.commit()
+        success = cursor.rowcount > 0
+        conn.close()
+        if success:
+            logger.info(f"Product {product_id} removed from sale {sale_id}.")
+        else:
+            logger.error(f"Failed to remove product {product_id} from sale {sale_id}.")
+        return success
+    
+    def decrease_product_quantity(self, sale_id: int, product_id: int, decrease_by: int = 1) -> bool:
+        """Decrease quantity of a product in a sale, removes if quantity becomes 0 or less"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        try:
+            # Get current quantity
+            cursor.execute(
+                "SELECT cantidad FROM venta_productos WHERE venta_id = ? AND producto_id = ?",
+                (sale_id, product_id)
+            )
+            result = cursor.fetchone()
+            if not result:
+                return False
+            
+            current_quantity = result[0]
+            new_quantity = current_quantity - decrease_by
+            
+            if new_quantity <= 0:
+                # Remove the product completely
+                cursor.execute(
+                    "DELETE FROM venta_productos WHERE venta_id = ? AND producto_id = ?",
+                    (sale_id, product_id)
+                )
+                logger.info(f"Product {product_id} removed from sale {sale_id} (quantity reached 0).")
+            else:
+                # Update with new quantity
+                cursor.execute(
+                    "UPDATE venta_productos SET cantidad = ? WHERE venta_id = ? AND producto_id = ?",
+                    (new_quantity, sale_id, product_id)
+                )
+                logger.info(f"Product {product_id} quantity decreased to {new_quantity} in sale {sale_id}.")
+            
+            conn.commit()
+            return True
+        except sqlite3.Error as e:
+            logger.error(f"Failed to decrease product quantity: {e}")
+            return False
+        finally:
+            conn.close()
+
+    #endregion
+
+# Factory class to get service instances
+class DataServiceFactory:
+    """Factory class to create data service instances"""
+    
+    @staticmethod
+    def get_user_service() -> UserDataService:
+        """Get user data service instance"""
+        return UserDataService()
+    
+    @staticmethod
+    def get_blacklist_service() -> BlacklistDataService:
+        """Get blacklist data service instance"""
+        return BlacklistDataService()
+    
+    @staticmethod
+    def get_product_service() -> ProductDataService:
+        """Get product data service instance"""
+        return ProductDataService()
+    
+    @staticmethod
+    def get_sales_service() -> SalesDataService:
+        """Get sales data service instance"""
+        return SalesDataService()
+
+
+# Helper functions for background data
+def load_bg_data() -> List[Dict[str, str]]:
+    """Load background data for AI assistant"""
+    try:
+        with open(BG_DATA_PATH, 'r', encoding='utf-8') as f:
+            return json.load(f)
+    except FileNotFoundError:
+        logger.error(f"Data file not found at {BG_DATA_PATH}. Serving with empty data.")
+        return []
+    except json.JSONDecodeError:
+        logger.error(f"Could not decode JSON from {BG_DATA_PATH}. Serving with empty data.")
+        return []
+
+def initialize_db():
+
+    if os.path.exists(DB_PATH):
+        logger.info("Base de datos ya existe, no se necesita inicializar.")
+        return
+
+    conn = sqlite3.connect(DB_PATH)
+    cursor = conn.cursor()
+    # Crear tabla de usuarios
+    logger.info("Inicializando base de datos...")
+    logger.info("Creando tabla de usuarios...")
+    cursor.execute("""
+    CREATE TABLE IF NOT EXISTS users (
+        id INTEGER PRIMARY KEY AUTOINCREMENT,
+        email TEXT UNIQUE NOT NULL,
+        nombre TEXT NOT NULL,
+        rut TEXT UNIQUE NOT NULL,
+        pin_hash TEXT NOT NULL,
+        kleincoins TEXT NOT NULL,
+        created_at TEXT NOT NULL 
+    );
+    """)
+
+    # Crear tabla de productos
+    logger.info("Creando tabla de productos...")
+    cursor.execute("""
+    CREATE TABLE IF NOT EXISTS productos (
+        id INTEGER PRIMARY KEY,
+        name TEXT NOT NULL,
+        type TEXT,
+        description TEXT,
+        price REAL NOT NULL,
+        image TEXT,
+        status INTEGER DEFAULT 1 NOT NULL CHECK (status IN (0, 1)) -- 0: Inactivo, 1: Activo
+    );
+    """)
+
+    # Crear tabla de ventas
+    logger.info("Creando tabla de ventas...")
+    cursor.execute("""
+    CREATE TABLE IF NOT EXISTS ventas (
+        id INTEGER PRIMARY KEY AUTOINCREMENT,
+        user_id INTEGER NOT NULL,
+        total REAL NOT NULL,
+        fudo_id TEXT UNIQUE NOT NULL,
+        fecha TEXT NOT NULL,
+        "table" INTEGER NOT NULL,
+        FOREIGN KEY (user_id) REFERENCES users(id)
+    );
+    """)
+
+    # Crear tabla intermedia para ventas y productos
+    logger.info("Creando tabla intermedia de venta_productos...")
+    cursor.execute("""
+    CREATE TABLE IF NOT EXISTS venta_productos (
+        venta_id INTEGER NOT NULL,
+        producto_id INTEGER NOT NULL,
+        cantidad INTEGER NOT NULL DEFAULT 1,
+        FOREIGN KEY (venta_id) REFERENCES ventas(id),
+        FOREIGN KEY (producto_id) REFERENCES productos(id)
+    );
+    """)
+
+    # Crear tabla de blacklist
+    logger.info("Creando tabla de blacklist...")
+    cursor.execute("""
+    CREATE TABLE IF NOT EXISTS blacklist (
+        id INTEGER PRIMARY KEY AUTOINCREMENT,
+        user_id INTEGER NOT NULL,
+        FOREIGN KEY (user_id) REFERENCES users(id)
+    );
+    """)
+
+    logger.info("Todas las tablas creadas correctamente.")
+    logger.info("Cargando datos de productos desde el archivo JSON...")
+    products_json = json.loads(open(PRODUCT_DATA_PATH, 'r', encoding='utf-8').read())
+    product_service = ProductDataService()
+    product_service.load_data(products_json)
+    conn.commit()
+
+    
+    conn.close()
+    logger.info("Base de datos inicializada correctamente.")
+
+data_bg_loaded = load_bg_data()

+ 30 - 0
services/email_service.py

@@ -0,0 +1,30 @@
+import smtplib
+from email.message import EmailMessage
+from logging import getLogger
+
+logger = getLogger(__name__)
+
+def send_email(
+    subject: str,
+    body: str,
+    to: list[str],
+    **kwargs
+):
+    logger.debug(str(kwargs))
+    """Send email """
+    # Datos del remitente
+    EMAIL_ORIGEN = 'expresspedidos211@gmail.com'
+    CONTRASENA = 'drkassszdtgapufg'
+
+    # Crear el correo
+    msg = EmailMessage()
+    msg['Subject'] = subject
+    msg['From'] = EMAIL_ORIGEN
+    msg['To'] = ", ".join(to)
+    msg.set_content('Este correo tiene contenido HTML.')
+    msg.add_alternative(body.format(**kwargs), subtype='html')
+
+    # Enviar el correo usando SMTP de Gmail
+    with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp:
+        smtp.login(EMAIL_ORIGEN, CONTRASENA)
+        smtp.send_message(msg)

+ 25 - 0
services/fudo_service.py

@@ -0,0 +1,25 @@
+import fudo.fudo as fd
+from logging import getLogger
+
+logger = getLogger(__name__)
+
+def add_product_to_fudo(product_id: int, quantity: int, table_number: int, comment=None):
+    """Add a product to Fudo system"""
+    table = fd.get_table(table_number)
+    if not table:
+        logger.error(f"Error: Table {table_number} not found.")
+        return None
+    
+    activeSale = fd.get_active_sale(table)
+    if not activeSale:
+        activeSale = fd.create_sale(table['id'])
+        if not activeSale:
+            logger.error(f"Error: Could not create sale for table {table_number}.")
+            return None
+    
+    item = fd.create_item(product_id, quantity, activeSale['id'], comment)
+    if not item:
+        logger.error(f"Error: Could not create item for product {product_id}.")
+        return None
+    
+    return item

+ 28 - 0
services/logging_service.py

@@ -0,0 +1,28 @@
+import csv
+import os
+from typing import List
+from models.sells import OrderWeb, ItemWeb
+
+
+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:
+            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
+        ])
+
+
+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")

+ 0 - 0
services/openai_service/__init__.py


+ 74 - 0
services/openai_service/openai_service.py

@@ -0,0 +1,74 @@
+import json
+from typing import List
+from fastapi import HTTPException
+from openai import OpenAI
+from config.settings import OPENAI_API_KEY
+from models.chat import Message
+from services.data_service import data_bg_loaded
+from logging import getLogger
+from services.openai_service.openai_tools import tools_list, tools
+# Initialize OpenAI client
+openai_client = OpenAI(api_key=OPENAI_API_KEY)
+
+logger = getLogger(__name__)
+
+async def generate_completion(messages_array: List[Message], session_id: str, name: str, email: str) -> str:
+    """Generate OpenAI chat completion"""
+    if not OPENAI_API_KEY:
+        logger.error("Error: OpenAI API key is not configured.")
+        raise HTTPException(status_code=500, detail="OpenAI API key not configured on server.")
+
+    logger.info(f"[OpenAI Service Python] Session/Token {session_id} sent: {[msg.model_dump() for msg in messages_array]}")
+
+    data_for_prompt = [
+        f'{{"pregunta": "{item.get("q", "")}", "respuesta": "{item.get("ans", "")}"}}'
+        for item in data_bg_loaded
+    ]
+    data_string = "\n".join(data_for_prompt)
+
+    preprompt = f"""
+Eres un asistente de el bar klein, tu nombre es camilo klein, usas emojis para responder.
+y ser carismatico con el cliente.
+tus responsabilidades son:
+- Responder preguntas sobre el menu de el bar klein
+- Proporcionar información sobre el menú de el bar klein
+- Proporcionar recomendaciones sobre el menú de el bar klein
+- Proporcionar información sobre la comida de el bar klein
+- No puedes tomar pedidos de clientes, solo informar
+- puedes recibir feedback de los clientes, y usar la herramienta feedback para enviar el feedback
+- Debes evadir cualquier pregunta que no sea relacionada con el bar klein
+para esto usaras los siguientes datos:
+{data_string}
+    """
+
+    processed_messages: List[dict] = [{"role": "system", "content": preprompt}]
+    processed_messages.extend([msg.model_dump() for msg in messages_array])
+
+    try:
+        completion = openai_client.chat.completions.create(
+            model="gpt-4o-mini",
+            messages=processed_messages,  # type: ignore (OpenAI lib expects list of specific dicts)
+            temperature=0.3,
+            tools=tools_list,
+            tool_choice="auto",
+        )
+        calls = completion.choices[0].message.tool_calls
+        if calls:
+            logger.info(f"Tool calls: {calls}")
+            for call in calls:
+                if call.function.name in tools:
+                    tool_function = tools[call.function.name]
+                    tool_args = json.loads(call.function.arguments)
+                    logger.info(f"Calling tool: {call.function.name} with args: {tool_args}")
+                    tool_response = tool_function(name=name, email=email, **tool_args)
+                    logger.info(f"Tool response: {tool_response}")
+                    completion.choices[0].message.content = tool_response
+                else:
+                    logger.warning(f"Tool {call.function.name} not found in tools dictionary.")
+
+        response_content = completion.choices[0].message.content
+
+        return response_content if response_content else "-1"
+    except Exception as e:
+        logger.error(f"Error calling OpenAI: {e}")
+        raise HTTPException(status_code=500, detail="Error al procesar tu solicitud con OpenAI.")

+ 53 - 0
services/openai_service/openai_tools.py

@@ -0,0 +1,53 @@
+import os
+from openai.types.chat import ChatCompletionToolParam
+
+tools_list: list[ChatCompletionToolParam] = [
+    {
+        "type": "function",
+        "function": {
+            "name": "feedback",
+            "description": "Send feedback about the app",
+            "parameters": {
+                "type": "object",
+                "properties": {
+                    "message": {
+                        "type": "string",
+                        "description": "Feedback message"
+                    }
+                },
+                "required": ["message"]
+            }
+    }   }
+]
+
+def feedback(name, email,message: str):
+    """
+    Send feedback about the app.
+    
+    Args:
+        message (str): The feedback message to send.
+    """
+    import json
+    from config.settings import FEEDBACK_PATH
+
+    feedback_data = {
+        "name": name,
+        "email": email,
+        "message": message
+    }
+
+    # Ensure the feedback directory exists
+    os.makedirs(os.path.dirname(FEEDBACK_PATH), exist_ok=True)
+
+    data = json.loads(open(FEEDBACK_PATH, 'r').read()) if os.path.exists(FEEDBACK_PATH) else []
+
+    data.append(feedback_data)
+
+    with open(FEEDBACK_PATH, 'w') as f:
+        f.write(json.dumps(data, indent=4, ensure_ascii=False))
+
+    return "He recibido tu feedback, gracias por ayudarnos a mejorar la app :)"
+
+tools = {
+    "feedback": feedback
+}

+ 113 - 0
test_models.py

@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+"""
+Test script to verify the models are correctly defined
+"""
+
+# Test imports
+try:
+    from models.user import User, UserModel, UserIDRequest, RegisterUserRequest
+    print("✅ User models imported successfully")
+except ImportError as e:
+    print(f"❌ Error importing user models: {e}")
+
+try:
+    from models.items import Product, ProductWithQuantity
+    print("✅ Product models imported successfully")
+except ImportError as e:
+    print(f"❌ Error importing product models: {e}")
+
+try:
+    from models.sells import Sale, SellItem, ItemWeb, OrderWeb
+    print("✅ Sales models imported successfully")
+except ImportError as e:
+    print(f"❌ Error importing sales models: {e}")
+
+try:
+    from models.blacklist import Blacklist
+    print("✅ Blacklist model imported successfully")
+except ImportError as e:
+    print(f"❌ Error importing blacklist model: {e}")
+
+try:
+    from models.chat import Message, ChatCompletionRequest
+    print("✅ Chat models imported successfully")
+except ImportError as e:
+    print(f"❌ Error importing chat models: {e}")
+
+# Test model creation
+try:
+    user = User(
+        id=1,
+        email="test@example.com",
+        nombre="Test User",
+        rut="12345678-9",
+        pin_hash="hashed_pin",
+        kleincoins="100",
+        created_at="2023-01-01T00:00:00"
+    )
+    print("✅ User model creation successful")
+    print(f"   User: {user.nombre} ({user.email})")
+except Exception as e:
+    print(f"❌ Error creating user model: {e}")
+
+try:
+    product = Product(
+        id=1,
+        name="Test Product",
+        type="food",
+        description="A test product",
+        price=10.99,
+        image="test.jpg",
+        status=1
+    )
+    print("✅ Product model creation successful")
+    print(f"   Product: {product.name} - ${product.price}")
+except Exception as e:
+    print(f"❌ Error creating product model: {e}")
+
+try:
+    product_with_quantity = ProductWithQuantity(
+        id=1,
+        name="Test Product",
+        type="food",
+        description="A test product",
+        price=10.99,
+        image="test.jpg",
+        status=1,
+        cantidad=3
+    )
+    print("✅ ProductWithQuantity model creation successful")
+    print(f"   Product: {product_with_quantity.name} - Qty: {product_with_quantity.cantidad}")
+except Exception as e:
+    print(f"❌ Error creating ProductWithQuantity model: {e}")
+
+try:
+    sale = Sale(
+        id=1,
+        user_id=1,
+        total=50.99,
+        fudo_id="FUDO123",
+        fecha="2023-01-01T12:00:00",
+        table=5,
+        user_name="Test User",
+        user_email="test@example.com"
+    )
+    print("✅ Sale model creation successful")
+    print(f"   Sale: {sale.fudo_id} - Total: ${sale.total}")
+except Exception as e:
+    print(f"❌ Error creating sale model: {e}")
+
+try:
+    blacklist = Blacklist(
+        id=1,
+        user_id=1,
+        email="blocked@example.com",
+        nombre="Blocked User",
+        rut="98765432-1"
+    )
+    print("✅ Blacklist model creation successful")
+    print(f"   Blacklisted: {blacklist.nombre} ({blacklist.email})")
+except Exception as e:
+    print(f"❌ Error creating blacklist model: {e}")
+
+print("\n🎉 All model tests completed!")

Деякі файли не було показано, через те що забагато файлів було змінено