Просмотр исходного кода

Merge pull request #3 from latapp/login_implement

Login implement
Erwin Jacimino 9 месяцев назад
Родитель
Сommit
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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
 OPENAI_API_KEY = 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
 NODE_ENV = development
 NODE_ENV = development
 FUDO_API_KEY=NzZAMTEzMzc4
 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
 logs.csv
 llm_logs.*
 llm_logs.*
 *.pyc
 *.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,
     "id": 6,
+    "status":1,
     "name": "Burlesque",
     "name": "Burlesque",
     "type": "Cerveza",
     "type": "Cerveza",
     "description": "Cerveza Ale ámbar, 5.0º - IBU 12",
     "description": "Cerveza Ale ámbar, 5.0º - IBU 12",
     "price": 5000,
     "price": 5000,
     "image": "https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/6"
     "image": "https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/6"
   },
   },
+  
   {
   {
     "id": 15,
     "id": 15,
+    "status":1,
     "name": "Bendicion Gitana",
     "name": "Bendicion Gitana",
     "type": "Cerveza",
     "type": "Cerveza",
     "description": "Pale Ale - 5,0º - IBU 15",
     "description": "Pale Ale - 5,0º - IBU 15",
     "price": 5000,
     "price": 5000,
     "image": "https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/15"
     "image": "https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/15"
-  },{
+  },
+  {
     "id":163,
     "id":163,
+    "status":1,
     "name":"Hoppy Mosh",
     "name":"Hoppy Mosh",
     "type":"Cerveza",
     "type":"Cerveza",
     "description":"IPA - 6.0º - IBU 38",
     "description":"IPA - 6.0º - IBU 38",
     "price": 6500,
     "price": 6500,
     "image":"https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/163"
     "image":"https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/163"
   },
   },
+  
   {
   {
     "id": 12,
     "id": 12,
+    "status":1,
     "name": "Black Mamba",
     "name": "Black Mamba",
     "type": "Cerveza",
     "type": "Cerveza",
     "description": "Porter - 6.0º - IBU 15",
     "description": "Porter - 6.0º - IBU 15",
     "price": 5000,
     "price": 5000,
     "image": "https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/12"
     "image": "https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/12"
-  },{
+  },
+  {
     "id": 665,
     "id": 665,
+    "status":1,
     "name": "Marzen",
     "name": "Marzen",
     "type": "Cerveza",
     "type": "Cerveza",
     "description": " Estilo Märzenbier, 5.0º - IBU 22",
     "description": " Estilo Märzenbier, 5.0º - IBU 22",
     "price": 5000,
     "price": 5000,
     "image": "https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/665"
     "image": "https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/665"
-  },{
+  },
+  {
     "id": 1,
     "id": 1,
+    "status":1,
     "name": "24k Gold",
     "name": "24k Gold",
     "type": "Cerveza",
     "type": "Cerveza",
     "description": "Golden Ale - 4,5º - IBU 20",
     "description": "Golden Ale - 4,5º - IBU 20",
     "price": 5000,
     "price": 5000,
     "image": "https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/1"
     "image": "https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/1"
   },
   },
+  
   {
   {
     "id": 655,
     "id": 655,
+    "status":1,
     "name": "🌟 Summer Klein",
     "name": "🌟 Summer Klein",
     "type": "Coctel",
     "type": "Coctel",
     "description": "Gin Juno, jugo de naranja, maracuya, limon y Ginger Beer",
     "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
 from time import time
 import requests
 import requests
 import os
 import os
+import redis
+from logging import getLogger
+
+logger = getLogger(__name__)
+
 api_token = os.getenv('FUDO_API_KEY')
 api_token = os.getenv('FUDO_API_KEY')
 api_secret = os.getenv('FUDO_API_SECRET')
 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():
 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'
     url = 'https://auth.fu.do/api'
     data = {
     data = {
         "apiKey": api_token,
         "apiKey": api_token,
@@ -30,42 +61,27 @@ def get_token():
     }
     }
     
     
     r = requests.post(url, data=data)
     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()
     token = get_token()
     url = 'https://api.fu.do/v1alpha1/product-categories'
     url = 'https://api.fu.do/v1alpha1/product-categories'
     headers = {
     headers = {
@@ -75,64 +91,17 @@ def get_categorys():
     return r.json()
     return r.json()
 
 
 def get_product(id_category:int):
 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)
     url = 'https://api.fu.do/v1alpha1/products/{}'.format(id_category)
     token = get_token()
     token = get_token()
     headers = {
     headers = {
         'Authorization': 'Bearer ' + token
         'Authorization': 'Bearer ' + token
     }
     }
     r = requests.get(url, headers=headers)
     r = requests.get(url, headers=headers)
+    if r.status_code != 200:
+        logger.error(f"Error al obtener producto: {r.json()['errors']}")
     return r.json()
     return r.json()
 
 
 def get_products():
 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'
     url = 'https://api.fu.do/v1alpha1/products'
     token = get_token()
     token = get_token()
     headers = {
     headers = {
@@ -151,17 +120,16 @@ def get_table(number:int):
     }
     }
     r = requests.get(url, headers=headers)
     r = requests.get(url, headers=headers)
     if r.status_code != 200:
     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 None
     try:
     try:
         return list(filter(lambda x: x['attributes']['number'] == number, r.json()['data']))[0]
         return list(filter(lambda x: x['attributes']['number'] == number, r.json()['data']))[0]
     except:
     except:
-        print('Error al obtener tabla')
-        print(r.json())
+        logger.error('Error al obtener tabla')
+        logger.error(r.json())
         return None
         return None
 
 
 def get_sale(sale_id:int):
 def get_sale(sale_id:int):
-
     url = 'https://api.fu.do/v1alpha1/sales/{}'.format(sale_id)
     url = 'https://api.fu.do/v1alpha1/sales/{}'.format(sale_id)
     token = get_token()
     token = get_token()
     headers = {
     headers = {
@@ -169,7 +137,7 @@ def get_sale(sale_id:int):
     }
     }
     r = requests.get(url, headers=headers)
     r = requests.get(url, headers=headers)
     if r.status_code != 200:
     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 None
     return r.json()
     return r.json()
 
 
@@ -205,7 +173,7 @@ def create_sale(table_id:int):
     }
     }
     r = requests.post(url, headers=headers, json=data)
     r = requests.post(url, headers=headers, json=data)
     if r.status_code != 201:
     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 None
     return r.json()["data"]
     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)
     r = requests.post(url, headers=headers, json=data)
     if r.status_code != 201:
     if r.status_code != 201:
-        print(r.json())
+        logger.error(r.json())
         return None
         return None
     return r.json()["data"]
     return r.json()["data"]
 
 
@@ -252,28 +220,44 @@ def get_active_sale(table):
         return None
         return None
     return data[0]
     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__":
 if __name__ == "__main__":
     table = get_table(107)
     table = get_table(107)
     if table is None:
     if table is None:
-        print('No se pudo obtener la mesa')
+        logger.error('No se pudo obtener la mesa')
         exit()
         exit()
     activeSale = get_active_sale(table)
     activeSale = get_active_sale(table)
     if not activeSale:
     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'])
         activeSale = create_sale(table['id'])
         if activeSale is None:
         if activeSale is None:
-            print('No se pudo crear la venta')
+            logger.error('No se pudo crear la venta')
             exit()
             exit()
     else:
     else:
         activeSale = activeSale[0]
         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.win32raw import Win32Raw
 from escpos.printer.usb import Usb
 from escpos.printer.usb import Usb
 from escpos.printer.network import Network
 from escpos.printer.network import Network
 from escpos.printer import Dummy
 from escpos.printer import Dummy
 from escpos.escpos import Escpos
 from escpos.escpos import Escpos
 from impresora.order import Order
 from impresora.order import Order
+import usb.core
+
+logger = logging.getLogger(__name__)
 
 
 
 
 class BasePrinter:
 class BasePrinter:
@@ -15,6 +19,10 @@ class BasePrinter:
         self.doubled_size = False
         self.doubled_size = False
         self.work = Dummy()
         self.work = Dummy()
         self.printer:Escpos
         self.printer:Escpos
+    
+    def is_connected(self):
+        return self.printer.is_online()
+
     def change_font(self):
     def change_font(self):
         self.font = "b" if self.font == "a" else "a"
         self.font = "b" if self.font == "a" else "a"
         self.work.set(font=self.font)
         self.work.set(font=self.font)
@@ -70,6 +78,24 @@ class PrinterUSB(BasePrinter):
     def __init__(self, vendor_id, product_id):
     def __init__(self, vendor_id, product_id):
         super().__init__()
         super().__init__()
         self.printer = Usb(vendor_id, product_id,in_ep=0x81,out_ep=0x03)
         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):
 class PrinterNetwork(BasePrinter):
     def __init__(self, host, port):
     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 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:
     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 __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>
     </script>
   <!-- Markdown -->
   <!-- Markdown -->
   <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
   <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 -->
   <!-- Animaciones -->
   <style>
   <style>
     @keyframes slideRight {
     @keyframes slideRight {
@@ -207,7 +207,6 @@
         </div>
         </div>
         <button id="checkoutButton"
         <button id="checkoutButton"
                 class="w-full bg-[#101419] hover:bg-[#37404a] disabled:opacity-50 text-white py-2 rounded-md"
                 class="w-full bg-[#101419] hover:bg-[#37404a] disabled:opacity-50 text-white py-2 rounded-md"
-                onclick="processOrder()"
                 disabled>
                 disabled>
           Envia tu orden
           Envia tu orden
         </button>
         </button>
@@ -251,27 +250,63 @@
   
   
   <!-- === MODAL INICIO DE SESIÓN === -->
   <!-- === MODAL INICIO DE SESIÓN === -->
 <div id="sessionModal"
 <div id="sessionModal"
-     class="fixed hidden inset-0 bg-black/70 flex items-center justify-center z-50">
-  <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"
     <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>
     </button>
-  </div>
+  </form>
 </div>
 </div>
 
 
               <!-- ---------- JS: conmutar tabs + toast ---------- -->
               <!-- ---------- 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 ---
 // --- Variables de Usuario ---
-let userName = '';
+let userId = -1;
+let userName = "Cliente";
 let userTable = null;
 let userTable = null;
-
+let userToken = null;
 // --- Datos de Productos y Carrito ---
 // --- Datos de Productos y Carrito ---
 let products = [];
 let products = [];
 let cart = [];
 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?" }
     { 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 ---
 // --- Elementos del DOM: Productos y Carrito ---
 const productListElement = document.getElementById("productList");
 const productListElement = document.getElementById("productList");
 const cartItemsElement = document.getElementById("cartItems");
 const cartItemsElement = document.getElementById("cartItems");
@@ -32,6 +37,82 @@ const chatSuggestionsElement = document.getElementById("chatSuggestions");
 // --- Loader Global ---
 // --- Loader Global ---
 let globalLoaderElement = null;
 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() {
 function createGlobalLoader() {
     if (document.getElementById('globalLoader')) return;
     if (document.getElementById('globalLoader')) return;
     globalLoaderElement = document.createElement('div');
     globalLoaderElement = document.createElement('div');
@@ -61,39 +142,9 @@ function hideGlobalLoader() {
 function formatPrice(price) {
 function formatPrice(price) {
     return price.toLocaleString("es-CL", { style: "currency", currency: "CLP" });
     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() {
 async function renderProducts() {
     if (!productListElement) return;
     if (!productListElement) return;
@@ -102,7 +153,8 @@ async function renderProducts() {
     if (!template) return;
     if (!template) return;
 
 
     productListElement.innerHTML = "";
     productListElement.innerHTML = "";
-    products = await getProducts();
+    console.log("Cargando productos...");
+    products = await getProducts(userToken);
 
 
     products.forEach(product => {
     products.forEach(product => {
         const clone = template.content.cloneNode(true);
         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);
     const product = products.find(p => p.id === productId);
     if (!product) return;
     if (!product) return;
     const cartItem = cart.find(item => item.id === productId);
     const cartItem = cart.find(item => item.id === productId);
@@ -147,12 +202,12 @@ window.addToCart = async (productId, buttonElement = null) => {
         }, 300);
         }, 300);
     }
     }
     updateCartDisplay();
     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`);
     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);
     const itemIndex = cart.findIndex(item => item.id === productId);
     if (itemIndex > -1) {
     if (itemIndex > -1) {
         if (removeAll || cart[itemIndex].quantity === 1) {
         if (removeAll || cart[itemIndex].quantity === 1) {
@@ -164,6 +219,12 @@ window.removeFromCart = (productId, removeAll = false) => {
     updateCartDisplay();
     updateCartDisplay();
 };
 };
 
 
+function calculateTotal() {
+    if (!cartTotalElement) return;
+    const total = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
+    cartTotalElement.textContent = formatPrice(total);
+}
+
 function updateCartDisplay() {
 function updateCartDisplay() {
     if (!cartItemsElement || !emptyCartTextElement || !checkoutButton || !cartCountElement) return;
     if (!cartItemsElement || !emptyCartTextElement || !checkoutButton || !cartCountElement) return;
     cartItemsElement.innerHTML = "";
     cartItemsElement.innerHTML = "";
@@ -207,27 +268,82 @@ function updateCartDisplay() {
                         <p class="text-sm accent-red">${formatPrice(item.price * item.quantity)}</p>
                         <p class="text-sm accent-red">${formatPrice(item.price * item.quantity)}</p>
                     </div>
                     </div>
                     <div class="flex items-center gap-1 sm:gap-2">
                     <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>
                             <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>
                         </button>
                     </div>
                     </div>
                 </div>
                 </div>
             `;
             `;
             cartItemsElement.innerHTML += cartItemHTML;
             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();
     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) {
 function displayChatMessage(sender, message) {
     if (!chatMessagesElement) return;
     if (!chatMessagesElement) return;
     const bubbleClass = sender === "user" ? "chat-bubble-user" : "chat-bubble-ai";
     const bubbleClass = sender === "user" ? "chat-bubble-user" : "chat-bubble-ai";
@@ -256,12 +372,12 @@ async function sendMessageToAI() {
     aiLoadingIndicator.classList.remove("hidden");
     aiLoadingIndicator.classList.remove("hidden");
 
 
     try {
     try {
-        const response = await serviceSendMessage(userInput, chatHistory, userName);
+        const response = await serviceSendMessage(userInput, chatHistory, userName, userToken);
         if (!response) {
         if (!response) {
             displayChatMessage("ai", "Hubo un problema al conectar con el Chef IA.");
             displayChatMessage("ai", "Hubo un problema al conectar con el Chef IA.");
         } else if (response === "not_init") {
         } 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) {
                 if (response) {
                     chatHistory = response.messageList;
                     chatHistory = response.messageList;
                     displayChatMessage("ai", response.assistantResponse);
                     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 () => {
 document.addEventListener("DOMContentLoaded", async () => {
 
 
     createGlobalLoader();
     createGlobalLoader();
     updateCartDisplay();
     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!")

Некоторые файлы не были показаны из-за большого количества измененных файлов