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
 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):

+ 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 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:
     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 __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>
         </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 ---------- -->

+ 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 ---
 // --- Variables de Usuario ---
 let userName = '';
 let userName = '';
 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 +15,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 +35,78 @@ const chatSuggestionsElement = document.getElementById("chatSuggestions");
 // --- Loader Global ---
 // --- Loader Global ---
 let globalLoaderElement = null;
 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() {
 function createGlobalLoader() {
     if (document.getElementById('globalLoader')) return;
     if (document.getElementById('globalLoader')) return;
     globalLoaderElement = document.createElement('div');
     globalLoaderElement = document.createElement('div');
@@ -61,39 +136,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;
@@ -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);
     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 +195,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 +212,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 +261,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 =====
 
 
-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) {
 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";
@@ -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 () => {
 document.addEventListener("DOMContentLoaded", async () => {
 
 
     createGlobalLoader();
     createGlobalLoader();
     updateCartDisplay();
     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.")