Jelajahi Sumber

megarefactor and redis incoporation

latapp 10 bulan lalu
induk
melakukan
7f4d9225f4

+ 94 - 0
README.md

@@ -0,0 +1,94 @@
+# Web Pedidos Klein - Backend
+
+## Estructura del Proyecto
+
+El backend ha sido reorganizado para mejorar la mantenibilidad y seguir buenas prácticas de desarrollo. La nueva estructura es:
+
+```
+pedidos_express/
+├── main.py                    # Punto de entrada principal
+├── app.py                     # Configuración de la aplicación FastAPI
+├── requirements.txt           # Dependencias
+├── .env                       # Variables de entorno
+├── config/
+│   ├── __init__.py
+│   └── settings.py           # Configuración y variables de entorno
+├── models/
+│   ├── __init__.py
+│   └── schemas.py            # Modelos Pydantic
+├── auth/
+│   ├── __init__.py
+│   └── security.py           # Autenticación y seguridad
+├── services/
+│   ├── __init__.py
+│   ├── data_service.py       # Manejo de datos (productos, usuarios)
+│   ├── openai_service.py     # Servicio de OpenAI/ChatGPT
+│   ├── email_service.py      # Servicio de envío de emails
+│   ├── fudo_service.py       # Integración con Fudo
+│   └── logging_service.py    # Logging de pedidos y respuestas
+├── routes/
+│   ├── __init__.py
+│   ├── chat.py              # Endpoints del chat
+│   ├── users.py             # Endpoints de usuarios
+│   ├── products.py          # Endpoints de productos
+│   ├── orders.py            # Endpoints de pedidos
+│   └── static.py            # Archivos estáticos
+├── impresora/               # Módulo de impresión (existente)
+├── fudo/                    # Módulo Fudo (existente)
+└── public/                  # Archivos estáticos frontend
+```
+
+## Módulos
+
+### config/settings.py
+- Configuración de la aplicación
+- Variables de entorno
+- Validación de configuración
+
+### models/schemas.py
+- Modelos Pydantic para request/response
+- Validación de datos
+
+### auth/security.py
+- Middleware de autenticación
+- Generación de tokens anti-abuse
+- Protección de endpoints
+
+### services/
+- **data_service.py**: Carga y manejo de datos (productos, usuarios, datos del menú)
+- **openai_service.py**: Integración con OpenAI para el chatbot
+- **email_service.py**: Envío de notificaciones por email
+- **fudo_service.py**: Integración con el sistema Fudo
+- **logging_service.py**: Logging de pedidos y respuestas del LLM
+
+### routes/
+- **chat.py**: Endpoints relacionados con el chat (/api/chat/*)
+- **users.py**: Endpoints de usuarios (/api/existsUser)
+- **products.py**: Endpoints de productos (/api/get_products)
+- **orders.py**: Endpoints de pedidos (/api/printer/order)
+- **static.py**: Servir archivos estáticos
+
+## Ventajas de la Nueva Estructura
+
+1. **Separación de responsabilidades**: Cada módulo tiene una función específica
+2. **Mantenibilidad**: Código más fácil de mantener y modificar
+3. **Testabilidad**: Cada módulo puede ser probado independientemente
+4. **Escalabilidad**: Fácil agregar nuevas funcionalidades
+5. **Legibilidad**: Código más organizado y fácil de entender
+
+## Cómo Ejecutar
+
+```bash
+python main.py
+```
+
+## Variables de Entorno Requeridas
+
+```env
+OPENAI_API_KEY=tu_api_key_aqui
+SECRET_KEY=tu_secret_key_para_sessions
+PORT=6001
+
+FUDO_API_KEY=tu_api_key_fudo
+FUDO_API_SECRET=tu_api_secret_fudo
+```

+ 47 - 0
app.py

@@ -0,0 +1,47 @@
+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")
+    
+    # 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 protect_chat_api
+    
+    # Chat routes
+    app.add_api_route("/api/chat/init-chat", chat.init_chat, methods=["GET"], summary="Initialize chat and get anti-abuse token")
+    app.add_api_route("/api/chat/completions", chat.chat_completions, methods=["POST"], 
+                     summary="Get chat completions from OpenAI", dependencies=[Depends(protect_chat_api)])
+    
+    # User routes
+    app.add_api_route("/api/existsUser", users.exists_user, methods=["POST"], summary="Check if user exists")
+    
+    # Product routes
+    app.add_api_route("/api/get_products", products.get_products, methods=["GET"], summary="Get products")
+    
+    # Order routes
+    app.add_api_route("/api/printer/order", orders.printer_order, methods=["POST"], 
+                     summary="Printer order", dependencies=[Depends(protect_chat_api)])
+    
+    # Static routes
+    from fastapi.responses import HTMLResponse
+    app.add_api_route("/", static.serve_index_html, methods=["GET"], 
+                     response_class=HTMLResponse, include_in_schema=False)
+    
+    # Mount static files
+    static.mount_static_files(app)

+ 1 - 0
auth/__init__.py

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

+ 46 - 0
auth/security.py

@@ -0,0 +1,46 @@
+from typing import Union
+from venv import logger
+from fastapi import Request, HTTPException, Header, Depends
+from typing import Annotated
+import secrets
+from logging import getLogger
+
+logger = getLogger(__name__)
+
+async def get_session_token(request: Request) -> Union[str, None]:
+    """Get the anti-abuse token from the session"""
+    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
+):
+    """Protect chat API endpoints with token validation"""
+    # Equivalent to protectChatAPI middleware
+    if not session_token:
+        if request.client:
+            logger.error(f"Session token is not initialized or invalid. IP: {request.client.host}")
+        else:
+            logger.error("Session token is not initialized or invalid.")
+        logger.error("Session token is not initialized or invalid.")
+        raise HTTPException(status_code=403, detail="Acceso denegado: Sesión inválida o token no inicializado.")
+
+    if not x_app_token:
+        if request.client:
+            logger.error(f"X-App-Token is missing. IP: {request.client.host}")
+        else:
+            logger.error("X-App-Token is missing.")
+        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
+        logger.warning(f"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
+
+
+def generate_anti_abuse_token() -> str:
+    """Generate a new anti-abuse token"""
+    return secrets.token_hex(32)

+ 1 - 0
config/__init__.py

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

+ 46 - 0
config/settings.py

@@ -0,0 +1,46 @@
+import os
+from dotenv import load_dotenv
+import logging
+
+from httpx import get
+
+# Load environment variables from .env file
+load_dotenv()
+
+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
+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")
+
+# Data paths
+BG_DATA_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)),'data', 'llm_data.json')
+PRODUCTS_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)),'data', 'products.json')
+
+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

+ 0 - 0
data/data.db


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


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


+ 98 - 114
fudo/fudo.py

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

+ 27 - 1
impresora/printer.py

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

+ 76 - 0
logs/app.log

@@ -0,0 +1,76 @@
+2025-07-24 16:55:26,259 - __main__:28 - INFO - Servidor corriendo en http://localhost:6001
+2025-07-24 16:55:26,259 - __main__:32 - INFO - Datos del asistente cargados desde: /home/superti/workspace/pedidos_express/data/llm_data.json
+2025-07-24 16:55:26,260 - routes.orders:67 - INFO - Starting order thread
+2025-07-24 16:55:44,638 - routes.orders:21 - INFO - Printer order received
+2025-07-24 16:55:44,638 - routes.orders:22 - INFO - customerName='' items=[ItemWeb(id=163, name='Hoppy Mosh', quantity=1, price=6500.0, itemTotal=6500.0), ItemWeb(id=15, name='Bendicion Gitana', quantity=1, price=5000.0, itemTotal=5000.0)] totalAmount=11500.0 orderDate='2025-07-24T16:55:44' table=12
+2025-07-24 16:55:44,649 - routes.orders:25 - ERROR - Printer is not connected.
+2025-07-24 16:56:42,340 - __main__:28 - INFO - Servidor corriendo en http://localhost:6001
+2025-07-24 16:56:42,340 - __main__:32 - INFO - Datos del asistente cargados desde: /home/superti/workspace/pedidos_express/data/llm_data.json
+2025-07-24 16:56:42,341 - routes.orders:67 - INFO - Starting order thread
+2025-07-24 16:56:53,097 - routes.products:9 - INFO - Fetching all products
+2025-07-24 16:56:53,112 - routes.chat:15 - INFO - App initialized from client: 127.0.0.1
+2025-07-24 16:56:57,804 - routes.products:9 - INFO - Fetching all products
+2025-07-24 16:56:57,808 - routes.chat:15 - INFO - App initialized from client: 127.0.0.1
+2025-07-24 16:57:03,006 - routes.orders:21 - INFO - Printer order received
+2025-07-24 16:57:03,006 - routes.orders:22 - INFO - customerName='' items=[ItemWeb(id=15, name='Bendicion Gitana', quantity=1, price=5000.0, itemTotal=5000.0)] totalAmount=5000.0 orderDate='2025-07-24T16:57:03' table=12
+2025-07-24 16:57:03,020 - routes.orders:25 - ERROR - Printer is not connected.
+2025-07-24 16:58:58,279 - main:28 - INFO - Servidor corriendo en http://localhost:6001
+2025-07-24 16:58:58,279 - main:32 - INFO - Datos del asistente cargados desde: /home/superti/workspace/pedidos_express/data/llm_data.json
+2025-07-24 16:58:58,279 - routes.orders:67 - INFO - Starting order thread
+2025-07-24 16:59:01,287 - routes.orders:21 - INFO - Printer order received
+2025-07-24 16:59:01,287 - routes.orders:22 - INFO - customerName='' items=[ItemWeb(id=15, name='Bendicion Gitana', quantity=1, price=5000.0, itemTotal=5000.0)] totalAmount=5000.0 orderDate='2025-07-24T16:59:01' table=12
+2025-07-24 16:59:01,290 - fudo.fudo:42 - INFO - Token obtenido desde Redis cache
+2025-07-24 16:59:02,284 - routes.orders:71 - INFO - Processing order: Orden de 24/07/2025
+[<impresora.order.Item object at 0x7f8060411f40>]
+Total: 5000.0
+2025-07-24 16:59:02,293 - routes.orders:81 - ERROR - Error printing order: Device not found (Unable to open USB printer on (4070, 33054):
+USB device not found (Device (4070, 33054) not found or cable not plugged in.))
+2025-07-24 16:59:44,706 - main:28 - INFO - Servidor corriendo en http://localhost:6001
+2025-07-24 16:59:44,707 - main:32 - INFO - Datos del asistente cargados desde: /home/superti/workspace/pedidos_express/data/llm_data.json
+2025-07-24 16:59:44,707 - routes.orders:67 - INFO - Starting order thread
+2025-07-24 16:59:54,367 - routes.orders:21 - INFO - Printer order received
+2025-07-24 16:59:54,367 - routes.orders:22 - INFO - customerName='' items=[ItemWeb(id=163, name='Hoppy Mosh', quantity=1, price=6500.0, itemTotal=6500.0), ItemWeb(id=15, name='Bendicion Gitana', quantity=1, price=5000.0, itemTotal=5000.0)] totalAmount=11500.0 orderDate='2025-07-24T16:59:54' table=12
+2025-07-24 16:59:54,368 - fudo.fudo:42 - INFO - Token obtenido desde Redis cache
+2025-07-24 16:59:54,368 - fudo.fudo:35 - INFO - Token obtenido desde variable global
+2025-07-24 16:59:54,717 - routes.orders:71 - INFO - Processing order: Orden de 24/07/2025
+[<impresora.order.Item object at 0x7f446437ff40>, <impresora.order.Item object at 0x7f446437fd00>]
+Total: 11500.0
+2025-07-24 16:59:54,726 - routes.orders:81 - ERROR - Error printing order: Device not found (Unable to open USB printer on (4070, 33054):
+USB device not found (Device (4070, 33054) not found or cable not plugged in.))
+2025-07-24 17:04:35,588 - main:28 - INFO - Servidor corriendo en http://localhost:6001
+2025-07-24 17:04:35,588 - main:32 - INFO - Datos del asistente cargados desde: /home/superti/workspace/pedidos_express/data/llm_data.json
+2025-07-24 17:04:35,588 - routes.orders:67 - INFO - Starting order thread
+2025-07-24 17:04:41,659 - routes.products:9 - INFO - Fetching all products
+2025-07-24 17:04:41,684 - routes.chat:15 - INFO - App initialized from client: 127.0.0.1
+2025-07-24 17:04:48,549 - routes.products:9 - INFO - Fetching all products
+2025-07-24 17:04:48,562 - routes.chat:15 - INFO - App initialized from client: 127.0.0.1
+2025-07-24 17:04:53,199 - routes.orders:21 - INFO - Printer order received
+2025-07-24 17:04:53,199 - routes.orders:22 - INFO - customerName='' items=[ItemWeb(id=163, name='Hoppy Mosh', quantity=1, price=6500.0, itemTotal=6500.0)] totalAmount=6500.0 orderDate='2025-07-24T17:04:53' table=12
+2025-07-24 17:04:53,214 - impresora.printer:87 - ERROR - Printer is not connected on USB port 4070:33054.
+2025-07-24 17:04:53,214 - routes.orders:25 - ERROR - Printer is not connected.
+2025-07-24 17:05:46,295 - main:28 - INFO - Servidor corriendo en http://localhost:6001
+2025-07-24 17:05:46,295 - main:32 - INFO - Datos del asistente cargados desde: /home/superti/workspace/pedidos_express/data/llm_data.json
+2025-07-24 17:05:46,295 - routes.orders:67 - INFO - Starting order thread
+2025-07-24 17:05:51,230 - routes.orders:21 - INFO - Printer order received
+2025-07-24 17:05:51,230 - routes.orders:22 - INFO - customerName='' items=[ItemWeb(id=163, name='Hoppy Mosh', quantity=1, price=6500.0, itemTotal=6500.0)] totalAmount=6500.0 orderDate='2025-07-24T17:05:51' table=12
+2025-07-24 17:05:51,241 - impresora.printer:87 - ERROR - Printer is not connected on USB port 4070:33054.
+2025-07-24 17:05:51,241 - routes.orders:25 - ERROR - Printer is not connected.
+2025-07-24 17:09:38,842 - main:28 - INFO - Servidor corriendo en http://localhost:6001
+2025-07-24 17:09:38,842 - main:32 - INFO - Datos del asistente cargados desde: /home/superti/workspace/pedidos_express/data/llm_data.json
+2025-07-24 17:09:38,842 - routes.orders:71 - INFO - Starting order thread
+2025-07-24 17:09:55,095 - routes.orders:22 - INFO - Printer order received
+2025-07-24 17:09:55,095 - routes.orders:23 - INFO - customerName='' items=[ItemWeb(id=163, name='Hoppy Mosh', quantity=1, price=6500.0, itemTotal=6500.0)] totalAmount=6500.0 orderDate='2025-07-24T17:09:55' table=12
+2025-07-24 17:09:55,109 - impresora.printer:89 - ERROR - Printer is not connected on USB port 4070:33054.
+2025-07-24 17:09:55,109 - routes.orders:26 - ERROR - Printer is not connected.
+2025-07-24 17:09:55,110 - routes.orders:30 - ERROR - Email sent to admin about printer issue.
+2025-07-24 17:17:15,952 - main:28 - INFO - Servidor corriendo en http://localhost:6001
+2025-07-24 17:17:15,952 - main:32 - INFO - Datos del asistente cargados desde: /home/superti/workspace/pedidos_express/data/llm_data.json
+2025-07-24 17:17:15,952 - routes.orders:71 - INFO - Starting order thread
+2025-07-24 17:17:15,953 - routes.orders:73 - INFO - Current printer orders: []
+2025-07-24 17:17:16,954 - routes.orders:73 - INFO - Current printer orders: []
+2025-07-24 17:17:17,956 - routes.orders:73 - INFO - Current printer orders: []
+2025-07-24 17:17:18,957 - routes.orders:73 - INFO - Current printer orders: []
+2025-07-24 17:17:19,958 - routes.orders:73 - INFO - Current printer orders: []
+2025-07-24 17:17:20,959 - routes.orders:73 - INFO - Current printer orders: []
+2025-07-24 17:17:21,960 - routes.orders:73 - INFO - Current printer orders: []
+2025-07-24 17:17:22,961 - routes.orders:73 - INFO - Current printer orders: []

+ 35 - 368
main.py

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

+ 1 - 0
models/__init__.py

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

+ 32 - 0
models/schemas.py

@@ -0,0 +1,32 @@
+from typing import List
+from pydantic import BaseModel
+
+
+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
+
+
+class UserCodeRequest(BaseModel):
+    user_code: str

+ 52 - 17
public/index.html

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

+ 161 - 117
public/js/app.js

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

+ 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 };

+ 21 - 0
public/js/service/auth.js

@@ -0,0 +1,21 @@
+
+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 { existsUser};

+ 47 - 0
public/js/service/chat.js

@@ -0,0 +1,47 @@
+async function initializeService() {
+  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();
+    const token = data.chatToken;
+    if (!token) {
+      throw new Error("No se pudo obtener el token de chat.");
+    }
+    return [true, token];
+  } 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.");
+  }
+}
+
+export { initializeService , sendMessage };

+ 34 - 0
public/js/service/product.js

@@ -0,0 +1,34 @@
+
+async function sendOrder(order, token) {
+  console.log("Enviando orden:", order);
+  try {
+    const response = await fetch("/api/printer/order", {
+      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(){
+  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;
+}
+
+export {sendOrder, getProducts}

+ 1 - 0
routes/__init__.py

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

+ 40 - 0
routes/chat.py

@@ -0,0 +1,40 @@
+from math import log
+from fastapi import Request, HTTPException, Depends
+from fastapi.responses import JSONResponse
+from models.schemas import ChatCompletionRequest
+from services.openai_service import generate_completion
+from services.logging_service import log_llm_response
+from auth.security import protect_chat_api, generate_anti_abuse_token
+import logging
+
+logger = logging.getLogger(__name__)
+
+async def init_chat(request: Request):
+    """Initialize chat and get anti-abuse token"""
+    if request.client:
+        logger.info(f"App initialized from client: {request.client.host}")
+
+    current_token = request.session.get("antiAbuseToken")
+    if not current_token:
+        new_token = generate_anti_abuse_token()
+        request.session["antiAbuseToken"] = new_token
+        logger.info(f"Generated new antiAbuseToken for session: {new_token}")
+        return JSONResponse({"chatToken": new_token})
+    else:
+        return JSONResponse({"chatToken": current_token})
+
+
+async def chat_completions(request_data: ChatCompletionRequest, request: Request):
+    """Get chat completions from OpenAI"""
+    # Uses session_token (which is the antiAbuseToken) as an identifier for logging
+    session_identifier = request.session.get("antiAbuseToken", "unknown_session")
+
+    try:
+        openai_response = await generate_completion(request_data.messages, session_identifier)
+        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.")

+ 86 - 0
routes/orders.py

@@ -0,0 +1,86 @@
+import asyncio
+from math import log
+import time
+from fastapi import HTTPException
+from fastapi.responses import JSONResponse
+from fudo import fudo
+from models.schemas 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
+logger = getLogger(__name__)
+
+printer_orders = []
+
+
+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, 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}
+        )
+    
+    # Print order
+    printer_orders.append(Order(
+        order.customerName, 
+        [Item(item.name, item.price, item.quantity) for item in items]
+    ))
+    
+    
+    # Log order
+    log_order(order, items)
+    
+    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

+ 10 - 0
routes/products.py

@@ -0,0 +1,10 @@
+from fastapi.responses import JSONResponse
+from services.data_service import all_products
+from logging import getLogger
+
+logger = getLogger(__name__)
+
+async def get_products():
+    """Get products"""
+    logger.info("Fetching all products")
+    return JSONResponse({"products": all_products})

+ 17 - 0
routes/static.py

@@ -0,0 +1,17 @@
+import os
+from fastapi import HTTPException
+from fastapi.responses import HTMLResponse, FileResponse
+from fastapi.staticfiles import StaticFiles
+
+
+async def serve_index_html():
+    """Serve the main HTML file"""
+    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)
+
+
+def mount_static_files(app):
+    """Mount static files"""
+    app.mount("/", StaticFiles(directory="public", html=False), name="public_root_assets")

+ 18 - 0
routes/users.py

@@ -0,0 +1,18 @@
+from fastapi.responses import JSONResponse
+from models.schemas import UserCodeRequest
+from services.data_service import load_users
+
+
+async def exists_user(request: UserCodeRequest):
+    """Check if user exists"""
+    users = load_users()
+    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
+    })

+ 1 - 0
services/__init__.py

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

+ 52 - 0
services/data_service.py

@@ -0,0 +1,52 @@
+import json
+import os
+from typing import List, Dict
+from venv import logger
+from config.settings import BG_DATA_PATH, PRODUCTS_PATH, EXCLUDED_BEER_IDS
+from logging import getLogger
+
+logger = getLogger(__name__)
+
+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 load_products() -> List[Dict[str, str]]:
+    """Load products data excluding beer IDs"""
+    try:
+        with open(PRODUCTS_PATH, 'r', encoding='utf-8') as f:
+            products = json.load(f)
+            return list(filter(lambda product: product['id'] not in EXCLUDED_BEER_IDS, products))
+    except FileNotFoundError:
+        logger.error(f"Data file not found at {PRODUCTS_PATH}. Serving with empty data.")
+        return []
+    except json.JSONDecodeError:
+        logger.error(f"Could not decode JSON from {PRODUCTS_PATH}. Serving with empty data.")
+        return []
+
+
+def load_users() -> List[Dict[str, str]]:
+    """Load users data"""
+    try:
+        with open('users.json', 'r') as f:
+            return json.load(f)
+    except FileNotFoundError:
+        logger.error("ERROR: users.json file not found.")
+        return []
+    except json.JSONDecodeError:
+        logger.error("ERROR: Could not decode JSON from users.json.")
+        return []
+
+
+# Load data at module level
+bg_data_loaded = load_bg_data()
+all_products = load_products()

+ 69 - 0
services/email_service.py

@@ -0,0 +1,69 @@
+import smtplib
+from email.message import EmailMessage
+
+
+def send_email():
+    """Send email notification when printer is disconnected"""
+    # 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)

+ 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.schemas import OrderWeb, ItemWeb
+
+
+def log_order(order: OrderWeb, items: List[ItemWeb]):
+    """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([
+            order.customerName, 
+            order.table, 
+            order.orderDate, 
+            list(map(lambda item: item.name, 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")

+ 54 - 0
services/openai_service.py

@@ -0,0 +1,54 @@
+from typing import List
+from fastapi import HTTPException
+from openai import OpenAI
+from config.settings import OPENAI_API_KEY
+from models.schemas import Message
+from services.data_service import bg_data_loaded
+from logging import getLogger
+# 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) -> 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 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] = [{"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:
+        logger.error(f"Error calling OpenAI: {e}")
+        raise HTTPException(status_code=500, detail="Error al procesar tu solicitud con OpenAI.")