Przeglądaj źródła

Add lazy loading for images, utility CSS, and response handling in FastAPI

- Implemented an Intersection Observer in `observer.js` for lazy loading images, improving performance by loading images only when they are in the viewport.
- Added a comprehensive CSS file for styling using Tailwind CSS, ensuring a responsive and modern design.
- Created utility functions in `responses.py` for standardized success and error JSON responses in FastAPI, enhancing API response consistency.
Erwin Jacimino 6 miesięcy temu
rodzic
commit
b2f6d7f376

+ 5 - 1
.gitignore

@@ -15,4 +15,8 @@ mrda.txt
 public/images/
 /local/
 deploy.sh
-./load_products.py
+./load_products.py
+api_test.py
+node_modules
+package.json
+package-lock.json

+ 0 - 205
LOGGING_IMPROVEMENTS.md

@@ -1,205 +0,0 @@
-# Mejoras en el Sistema de Logging - Pedidos Express
-
-## Resumen de Mejoras Implementadas
-
-Este documento describe las mejoras implementadas en el sistema de logging del proyecto Pedidos Express.
-
-## 1. Nuevo Sistema de Logging Estructurado
-
-### Archivo: `services/logging_service.py`
-
-Se implementó un sistema de logging estructurado con las siguientes características:
-
-- **Logging por categorías**: ORDER, USER, PAYMENT, SECURITY, API, DATABASE, EMAIL, PRINT, CHAT, SYSTEM
-- **Niveles de log**: DEBUG, INFO, WARNING, ERROR, CRITICAL
-- **Logs estructurados en JSON**: Fáciles de procesar y analizar
-- **Archivos de log específicos**: Un archivo por categoría para mejor organización
-- **Metadatos enriquecidos**: Incluye user_id, user_email, timestamp, datos contextuales
-
-### Funciones principales:
-- `log_order_event()`: Para eventos relacionados con pedidos
-- `log_user_event()`: Para eventos de usuarios
-- `log_security_event()`: Para eventos de seguridad
-- `log_api_event()`: Para eventos de API
-- `log_database_event()`: Para eventos de base de datos
-- `log_email_event()`: Para eventos de email
-- `log_print_event()`: Para eventos de impresión
-- `log_chat_event()`: Para eventos de chat/LLM
-- `log_system_event()`: Para eventos del sistema
-
-## 2. Mejoras por Archivo
-
-### `services/print_service.py`
-- ✅ Logging detallado de operaciones de impresión
-- ✅ Manejo robusto de errores con contexto
-- ✅ Logging de status de impresora
-- ✅ Timeouts en requests para evitar bloqueos
-- ✅ Logging estructurado para troubleshooting
-
-### `auth/security.py`
-- ✅ Logging de eventos de autenticación
-- ✅ Logging de generación de tokens
-- ✅ Logging de validación de tokens
-- ✅ Logging de errores de JWT
-- ✅ Logging de intentos de acceso no autorizado
-
-### `services/email_service.py`
-- ✅ Logging de conexiones SMTP
-- ✅ Logging de autenticación SMTP
-- ✅ Logging de envío de emails
-- ✅ Logging de reintentos automáticos
-- ✅ Logging de errores de conexión
-
-### `routes/orders.py`
-- ✅ Logging completo del flujo de pedidos
-- ✅ Logging de validaciones de datos
-- ✅ Logging de integración con Fudo
-- ✅ Logging de creación de ventas
-- ✅ Logging de actualización de recompensas
-- ✅ Logging de impresión de pedidos
-- ✅ Manejo robusto de errores con contexto
-
-### `routes/users.py`
-- ✅ Logging de registro de usuarios
-- ✅ Logging de intentos de login
-- ✅ Logging de bloqueos por intentos fallidos
-- ✅ Logging de validaciones RUT
-- ✅ Logging de verificaciones por email
-- ✅ Logging de eventos de seguridad
-
-### `routes/chat.py`
-- ✅ Logging de requests de chat
-- ✅ Logging de respuestas de OpenAI
-- ✅ Logging de errores de procesamiento
-- ✅ Logging de métricas de uso
-
-### `main.py`
-- ✅ Logging de inicio de aplicación
-- ✅ Logging de validación de configuración
-- ✅ Logging de inicialización de componentes
-- ✅ Logging de errores críticos de startup
-
-### `app.py`
-- ✅ Logging de creación de app FastAPI
-- ✅ Logging de configuración de middleware
-- ✅ Logging de setup de rutas
-- ✅ Logging de montaje de archivos estáticos
-
-## 3. Beneficios del Nuevo Sistema
-
-### Troubleshooting Mejorado
-- **Logs estructurados** facilitan el análisis automático
-- **Contexto enriquecido** con user_id, emails, datos de request
-- **Categorización** permite filtrar por tipo de evento
-- **Timestamps precisos** para correlación temporal
-
-### Seguridad
-- **Logging de eventos de seguridad** (login failures, admin access attempts)
-- **Tracking de intentos de acceso** no autorizado
-- **Logging de generación y validación de tokens**
-
-### Monitoreo Operacional
-- **Status de servicios externos** (printer, SMTP)
-- **Métricas de rendimiento** y tiempo de respuesta
-- **Tracking de errores** con stack traces contextuales
-
-### Cumplimiento y Auditoría
-- **Trazabilidad completa** de acciones de usuario
-- **Logs de transacciones** de pedidos y pagos
-- **Registro de accesos administrativos**
-
-## 4. Estructura de Archivos de Log
-
-```
-logs/
-├── app.log              # Log principal de la aplicación
-├── order.log            # Eventos de pedidos
-├── user.log             # Eventos de usuarios
-├── security.log         # Eventos de seguridad
-├── api.log              # Eventos de API
-├── database.log         # Eventos de base de datos
-├── email.log            # Eventos de email
-├── print.log            # Eventos de impresión
-├── chat.log             # Eventos de chat/LLM
-├── system.log           # Eventos del sistema
-├── orders.csv           # Log legacy de pedidos (CSV)
-└── llm_responses.txt    # Log legacy de respuestas LLM
-```
-
-## 5. Ejemplo de Log Estructurado
-
-```json
-{
-  "timestamp": "2025-08-07T10:30:15.123456",
-  "category": "ORDER",
-  "level": "INFO",
-  "message": "Order completed successfully for table 5",
-  "user_id": 123,
-  "user_email": "user@example.com",
-  "data": {
-    "table": 5,
-    "customer_id": 123,
-    "total_amount": 25000,
-    "items": [
-      {"name": "Cerveza", "quantity": 2, "price": 5000},
-      {"name": "Hamburguesa", "quantity": 1, "price": 15000}
-    ],
-    "sale_id": 456,
-    "new_reward_progress": 85,
-    "beers_for_promo": 2
-  }
-}
-```
-
-## 6. Compatibilidad con Sistema Anterior
-
-- ✅ **Funciones legacy mantenidas** para evitar breaking changes
-- ✅ **Archivos CSV y TXT** se siguen generando para compatibilidad
-- ✅ **Migración gradual** sin afectar funcionalidad existente
-
-## 7. Recomendaciones de Uso
-
-### Para Desarrollo
-- Utilizar nivel `DEBUG` para troubleshooting detallado
-- Revisar `logs/security.log` para eventos de autenticación
-- Monitorear `logs/print.log` para problemas de impresión
-
-### Para Producción
-- Configurar nivel `INFO` o `WARNING` para logs principales
-- Implementar rotación de logs para gestión de espacio
-- Configurar alertas para eventos `ERROR` y `CRITICAL`
-- Monitorear `logs/system.log` para salud de la aplicación
-
-### Para Análisis
-- Procesar logs JSON con herramientas como `jq`, ELK stack, o Datadog
-- Correlacionar eventos por `user_id` para tracking de usuario
-- Analizar métricas de performance y errores
-
-## 8. Próximos Pasos Recomendados
-
-1. **Implementar rotación de logs** con `logrotate` o similar
-2. **Configurar alertas** para eventos críticos
-3. **Integrar con sistema de monitoreo** (Grafana, Datadog, etc.)
-4. **Implementar dashboards** para métricas operacionales
-5. **Configurar backup** de logs importantes
-6. **Documentar alertas** y procedimientos de respuesta
-
----
-
-## Comandos Útiles para Análisis de Logs
-
-```bash
-# Ver logs de errores en tiempo real
-tail -f logs/app.log | grep ERROR
-
-# Analizar logs de seguridad
-jq '. | select(.category == "SECURITY")' logs/security.log
-
-# Contar pedidos por usuario
-jq '. | select(.category == "ORDER") | .user_email' logs/order.log | sort | uniq -c
-
-# Ver errores de los últimos 30 minutos
-jq '. | select(.level == "ERROR" and (.timestamp | fromdateiso8601) > (now - 1800))' logs/app.log
-```
-
-Esta implementación proporciona una base sólida para el monitoreo, troubleshooting y análisis operacional del sistema Pedidos Express.

+ 0 - 82
NO_CACHE_SETUP.md

@@ -1,82 +0,0 @@
-# Configuración para Evitar Cache del Navegador
-
-Este documento explica las implementaciones realizadas para evitar que las páginas de la carpeta `public` guarden cache en el navegador.
-
-## Cambios Realizados
-
-### 1. Archivos HTML Modificados
-
-Se agregaron meta tags de no-cache en las secciones `<head>` de todos los archivos HTML:
-
-- `public/main/index.html`
-- `public/register/index.html` 
-- `public/verify.html`
-
-**Meta tags agregados:**
-```html
-<!-- Meta tags para evitar cache -->
-<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
-<meta http-equiv="Pragma" content="no-cache">
-<meta http-equiv="Expires" content="0">
-```
-
-### 2. Servidor FastAPI - Headers HTTP
-
-#### a) Clase NoCacheStaticFiles
-
-Se creó una clase personalizada `NoCacheStaticFiles` en `routes/static.py` que extiende `StaticFiles` y agrega automáticamente headers de no-cache a todos los archivos estáticos.
-
-#### b) Funciones de Servicio HTML
-
-Se modificaron las funciones que sirven archivos HTML para incluir headers de no-cache:
-- `serve_app_html()`
-- `serve_register_html()`
-- `serve_image()`
-
-**Headers agregados:**
-```python
-headers = {
-    "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
-    "Pragma": "no-cache",
-    "Expires": "0"
-}
-```
-
-### 3. Middleware Global
-
-Se creó un middleware global `NoCacheMiddleware` que asegura que todas las rutas relacionadas con archivos públicos tengan headers de no-cache.
-
-**Rutas cubiertas:**
-- `/` (página principal)
-- `/register` (página de registro)
-- `/verify` (página de verificación)
-- `/express/` (archivos estáticos principales)
-- `/register/` (archivos estáticos de registro)
-- `/images/` (imágenes)
-
-## Efectos de los Cambios
-
-1. **Cache del navegador:** Los archivos HTML, CSS, JS e imágenes no se guardarán en cache
-2. **Actualizaciones inmediatas:** Los cambios en archivos se reflejarán inmediatamente sin necesidad de refrescar con Ctrl+F5
-3. **Compatibilidad:** Funciona con todos los navegadores modernos
-
-## Verificación
-
-Para verificar que funciona correctamente:
-
-1. Abre las herramientas de desarrollador (F12)
-2. Ve a la pestaña "Network" 
-3. Recarga la página
-4. Verifica que en los headers de respuesta aparezcan:
-   - `Cache-Control: no-cache, no-store, must-revalidate, max-age=0`
-   - `Pragma: no-cache`
-   - `Expires: 0`
-
-## Nota Importante
-
-Estos cambios evitarán completamente el cache, lo que puede resultar en:
-- ✅ Actualizaciones inmediatas
-- ❌ Mayor uso de ancho de banda
-- ❌ Tiempos de carga ligeramente mayores
-
-Para producción, considera implementar cache selectivo solo en archivos que cambien frecuentemente.

+ 182 - 347
README.md

@@ -1,348 +1,183 @@
-# Biergarten Klein - Sistema de Pedidos Express
-
-## Descripción del Proyecto
-
-**Biergarten Klein** es una aplicación web completa para la gestión de pedidos en un bar cervecero artesanal. El sistema permite a los clientes interactuar con un asistente de IA llamado "Camilo Klein" para consultar el menú, recibir recomendaciones de cervezas artesanales, realizar pedidos y gestionar su experiencia en el establecimiento.
-
-## Características Principales
-
-### 🤖 Asistente de IA Personalizado
-- **Camilo Klein**: Chatbot especializado en el menú del bar
-- Integración con OpenAI GPT-4o-mini
-- Respuestas carismáticas con emojis
-- Base de conocimientos específica sobre cervezas artesanales
-- Sistema anti-abuso con tokens de sesión
-
-### 🍺 Gestión de Productos
-- Catálogo completo de cervezas artesanales (Burlesque, Queen Burlesque, Hoppy Mosh, Black Mamba, etc.)
-- Sistema de carrito de compras interactivo
-- Información detallada de cada cerveza (IBU, SRM, notas de sabor)
-- Precios y disponibilidad en tiempo real
-
-### 🧾 Sistema de Pedidos
-- Procesamiento de pedidos con validación
-- Integración con impresora térmica USB
-- Sincronización con sistema Fudo POS
-- Notificaciones por email en caso de errores
-- Logging completo de transacciones
-
-### 🔐 Seguridad y Autenticación
-- Sistema de tokens anti-abuso
-- Validación de usuarios
-- Protección de endpoints sensibles
-- Middleware de sesiones seguras
-
-## Estructura del Proyecto
-
-```
-pedidos_express/
-├── main.py                    # Punto de entrada principal
-├── app.py                     # Configuración FastAPI y rutas
-├── requirements.txt           # Dependencias del proyecto
-├── tailwind.config.js         # Configuración de Tailwind CSS
-├── config/
-│   ├── __init__.py
-│   └── settings.py           # Variables de entorno y logging
-├── models/
-│   ├── __init__.py
-│   └── schemas.py            # Modelos Pydantic (Message, Order, User)
-├── auth/
-│   ├── __init__.py
-│   └── security.py           # Autenticación y tokens anti-abuso
-├── services/
-│   ├── __init__.py
-│   ├── data_service.py       # Gestión de datos y productos
-│   ├── openai_service.py     # Integración con OpenAI
-│   ├── email_service.py      # Notificaciones por email
-│   ├── fudo_service.py       # Integración con Fudo POS
-│   └── logging_service.py    # Sistema de logs
-├── routes/
-│   ├── __init__.py
-│   ├── chat.py              # Endpoints del chatbot
-│   ├── users.py             # Gestión de usuarios
-│   ├── products.py          # Catálogo de productos
-│   ├── orders.py            # Procesamiento de pedidos
-│   └── static.py            # Archivos estáticos
-├── impresora/               # Módulo de impresión térmica
-│   ├── __init__.py
-│   ├── order.py             # Modelos para órdenes de impresión
-│   └── printer.py           # Control de impresora USB
-├── fudo/                    # Integración con Fudo POS
-│   └── fudo.py              # API y gestión de tokens
-├── data/                    # Almacenamiento de datos
-│   ├── data.db              # Base de datos SQLite
-│   ├── llm_data.json        # Base de conocimientos para IA
-│   └── products.json        # Catálogo de productos
-├── public/                  # Frontend web
-│   ├── index.html           # Página principal
-│   ├── styles.css           # Estilos personalizados
-│   ├── assets/              # Recursos multimedia
-│   └── js/
-│       ├── app.js           # Lógica principal del frontend
-│       ├── interfaces.js    # Interfaces de usuario
-│       └── service/         # Servicios del frontend
-│           ├── auth.js      # Autenticación cliente
-│           ├── chat.js      # Comunicación con chatbot
-│           └── product.js   # Gestión de productos
-└── logs/                    # Archivos de log
-    └── app.log              # Logs de la aplicación
-```
-
-## Tecnologías Utilizadas
-
-### Backend
-- **FastAPI**: Framework web moderno y rápido
-- **Python 3.9+**: Lenguaje de programación principal
-- **OpenAI API**: Integración con GPT-4o-mini para el chatbot
-- **SQLite**: Base de datos local
-- **Redis**: Cache para tokens de autenticación
-- **Uvicorn**: Servidor ASGI para producción
-
-### Frontend
-- **HTML5/CSS3/JavaScript**: Tecnologías web estándar
-- **Tailwind CSS**: Framework de estilos utilitarios
-- **Vanilla JavaScript**: Sin frameworks adicionales para máximo rendimiento
-
-### Integraciones
-- **Fudo POS**: Sistema de punto de venta
-- **Impresora Térmica USB**: Para tickets de pedidos
-- **SMTP**: Envío de notificaciones por email
-
-## Módulos Principales
-
-### config/settings.py
-- Configuración centralizada de la aplicación
-- Gestión de variables de entorno
-- Sistema de logging configurado
-- Validación de configuración crítica
-
-### models/schemas.py
-- **Message**: Estructura para mensajes del chat
-- **ChatCompletionRequest**: Requests para el chatbot
-- **OrderWeb**: Modelo completo de pedidos
-- **ItemWeb**: Elementos individuales del pedido
-- **UserCodeRequest**: Validación de códigos de usuario
-
-### auth/security.py
-- **Sistema de tokens anti-abuso**: Protección contra uso excesivo
-- **Validación de sesiones**: Control de acceso a endpoints
-- **Middleware de seguridad**: Protección de rutas sensibles
-- **Generación segura de tokens**: Utilizando secrets
-
-### services/
-- **data_service.py**: Carga y gestión de productos, usuarios y datos del menú
-- **openai_service.py**: Integración completa con OpenAI para el asistente Camilo Klein
-- **email_service.py**: Sistema de notificaciones por correo electrónico
-- **fudo_service.py**: Integración bidireccional con el sistema Fudo POS
-- **logging_service.py**: Registro detallado de pedidos y respuestas del LLM
-
-### routes/
-- **chat.py**: 
-  - `/api/chat/init-chat` - Inicialización del chat y generación de tokens
-  - `/api/chat/completions` - Procesamiento de mensajes del chatbot
-- **users.py**: 
-  - `/api/existsUser` - Validación de códigos de usuario
-- **products.py**: 
-  - `/api/get_products` - Obtención del catálogo completo
-- **orders.py**: 
-  - `/api/printer/order` - Procesamiento e impresión de pedidos
-- **static.py**: 
-  - Servir archivos estáticos y página principal
-
-## API Endpoints
-
-### Chat y Asistente IA
-```
-GET  /api/chat/init-chat       # Inicializar sesión de chat
-POST /api/chat/completions     # Enviar mensaje al asistente
-```
-
-### Gestión de Productos
-```
-GET  /api/get_products         # Obtener catálogo de productos
-```
-
-### Gestión de Pedidos
-```
-POST /api/printer/order        # Procesar y imprimir pedido
-```
-
-### Usuarios
-```
-POST /api/existsUser          # Validar código de usuario
-```
-
-## Flujo de la Aplicación
-
-1. **Inicialización**: El usuario accede a la página principal
-2. **Autenticación**: Se genera un token anti-abuso para la sesión
-3. **Exploración**: El usuario puede consultar productos y chatear con Camilo Klein
-4. **Pedido**: Selección de productos y agregado al carrito
-5. **Procesamiento**: Validación, impresión y sincronización con Fudo POS
-6. **Confirmación**: Notificación al usuario y registro en logs
-
-## Base de Conocimientos del Asistente
-
-El asistente Camilo Klein utiliza una base de datos especializada (`llm_data.json`) que incluye:
-
-- **Información del establecimiento**: Historia y concepto del Biergarten Klein
-- **Catálogo completo de cervezas**: 15+ variedades artesanales con detalles técnicos
-- **Recomendaciones especializadas**: Sugerencias basadas en preferencias
-- **Información nutricional y técnica**: IBU, SRM, graduación alcohólica
-- **Horarios y ubicación**: Datos prácticos para los clientes
-
-## Características Técnicas Avanzadas
-
-### Sistema de Logging
-- Registro detallado de todas las interacciones
-- Logs estructurados con timestamps
-- Separación por niveles (INFO, WARNING, ERROR)
-- Rotación automática de archivos de log
-
-### Integración con Hardware
-- **Impresora térmica USB**: Detección automática y manejo de errores
-- **Códigos de barras**: Generación para tracking de pedidos
-- **Notificaciones automáticas**: Email en caso de fallos de hardware
-
-### Gestión de Estado
-- **Redis para cache**: Tokens de autenticación y sesiones
-- **SQLite para persistencia**: Datos permanentes y históricos
-- **JSON para configuración**: Datos del menú y configuraciones
-
-## Ventajas de la Arquitectura
-
-1. **Modularidad**: Cada componente tiene responsabilidades específicas
-2. **Escalabilidad**: Fácil agregar nuevas funcionalidades
-3. **Mantenibilidad**: Código organizado y bien documentado
-4. **Testabilidad**: Componentes aislados para pruebas unitarias
-5. **Seguridad**: Múltiples capas de protección
-6. **Performance**: Optimizado para respuestas rápidas
-
-## Instalación y Configuración
-
-### Prerequisitos
-```bash
-Python 3.9+
-Redis Server
-Impresora térmica USB (opcional)
-```
-
-### Instalación
-```bash
-# Clonar el repositorio
-git clone [repository-url]
-cd pedidos_express
-
-# Instalar dependencias
-pip install -r requirements.txt
-
-# Configurar variables de entorno
-cp .env.example .env
-```
-
-### Variables de Entorno Requeridas
-```env
-# OpenAI Configuration
-OPENAI_API_KEY=tu_api_key_de_openai
-
-# Security
-SECRET_KEY=tu_clave_secreta_muy_segura
-
-# Server Configuration  
-PORT=6001
-LOG_LEVEL=INFO
-
-# Fudo POS Integration
-FUDO_API_KEY=tu_api_key_fudo
-FUDO_API_SECRET=tu_api_secret_fudo
-
-# Redis Configuration
-REDIS_HOST=localhost
-REDIS_PORT=6379
-REDIS_DB=0
-
-# Email Configuration (opcional)
-SMTP_HOST=smtp.gmail.com
-SMTP_PORT=587
-SMTP_USER=tu_email@gmail.com
-SMTP_PASSWORD=tu_password_de_app
-```
-
-## Ejecución
-
-### Desarrollo
-```bash
+# **Biergarten Klein \- Sistema de Pedidos Express**
+
+Este proyecto es el backend de una aplicación web de gestión de pedidos para el bar cervecero artesanal **Biergarten Klein**. Está construido con **FastAPI** y proporciona una API RESTful para gestionar productos, pedidos, usuarios y un asistente de IA ("Camilo Klein") basado en OpenAI.
+
+## **🚀 Puesta en Marcha (Getting Started)**
+
+Sigue estos pasos para levantar el entorno de desarrollo local.
+
+### **1\. Prerrequisitos**
+
+* **Python 3.9+**  
+* **Servidor Redis** (para la gestión de tokens y sesiones)  
+* (Opcional) Una impresora térmica USB configurada en el sistema.
+
+### **2\. Instalación**
+
+1. Clona el repositorio:  
+   git clone \[repository-url\]  
+   cd pedidos\_express\_server
+
+2. Crea y activa un entorno virtual (recomendado):  
+   python \-m venv venv  
+   source venv/bin/activate  \# En Windows: venv\\Scripts\\activate
+
+3. Instala las dependencias:  
+   pip install \-r requirements.txt
+
+4. Configura las variables de entorno. Crea un archivo .env en la raíz del proyecto basándote en el archivo .env.example (o el .env proporcionado si ya lo tienes).
+
+### **3\. Variables de Entorno (.env)**
+
+Asegúrate de que tu archivo .env contenga las siguientes claves:
+
+\# \--- Configuración del Servidor \---  
+PORT=6001  
+LOG\_LEVEL=INFO
+
+\# \--- Seguridad (¡Genera claves seguras\!) \---  
+SECRET\_KEY=tu\_clave\_secreta\_muy\_segura
+
+\# \--- OpenAI API \---  
+OPENAI\_API\_KEY=tu\_api\_key\_de\_openai
+
+\# \--- Redis \---  
+REDIS\_HOST=localhost  
+REDIS\_PORT=6379  
+REDIS\_DB=0
+
+\# \--- Fudo POS \---  
+FUDO\_API\_KEY=tu\_api\_key\_fudo  
+FUDO\_API\_SECRET=tu\_api\_secret\_fudo
+
+\# \--- Email (para notificaciones de error) \---  
+SMTP\_HOST=smtp.gmail.com  
+SMTP\_PORT=587  
+SMTP\_USER=tu\_email@gmail.com  
+SMTP\_PASSWORD=tu\_password\_de\_app
+
+### **4\. Ejecución**
+
+#### **Modo Desarrollo**
+
+Ejecuta el servidor con Uvicorn para recarga automática (como se define en main.py):
+
 python main.py
-```
-
-### Producción
-```bash
-gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app --bind 0.0.0.0:6001
-```
-
-## Monitoreo y Logs
-
-### Archivos de Log
-- `logs/app.log`: Log principal de la aplicación
-- `logs.csv`: Registro de interacciones del LLM
-
-### Métricas Monitoreadas
-- Tiempo de respuesta del chatbot
-- Errores de impresión
-- Fallos de conexión con Fudo POS
-- Uso del sistema anti-abuso
-
-## Seguridad
-
-### Medidas Implementadas
-- **Tokens anti-abuso**: Prevención de spam y uso excesivo
-- **Validación de entrada**: Sanitización de todos los inputs
-- **Protección CSRF**: Tokens de sesión seguros
-- **Rate limiting**: Control de frecuencia de requests
-- **Logging de seguridad**: Registro de intentos de acceso no autorizado
-
-## Desarrollo y Contribución
-
-### Estructura de Commits
-- `feat:` Nuevas características
-- `fix:` Corrección de bugs  
-- `docs:` Documentación
-- `style:` Formateo de código
-- `refactor:` Refactorización
-- `test:` Pruebas
-
-### Testing
-```bash
-# Ejecutar tests unitarios
-python -m pytest tests/
-
-# Tests de integración
-python -m pytest tests/integration/
-```
-
-## Roadmap Futuro
-
-### Próximas Características
-- [ ] Sistema de reservas de mesas
-- [ ] Programa de fidelización
-- [ ] Integración con redes sociales
-- [ ] App móvil nativa
-- [ ] Dashboard de analytics
-- [ ] Sistema de reviews y ratings
-
-### Mejoras Técnicas
-- [ ] Migración a PostgreSQL
-- [ ] Implementación de microservicios
-- [ ] Containerización con Docker
-- [ ] CI/CD con GitHub Actions
-- [ ] Monitoreo con Prometheus
-
-## Soporte y Contacto
-
-Para soporte técnico o consultas sobre el proyecto, contactar al equipo de desarrollo.
-
----
-
-**Versión**: 1.0.0  
-**Última actualización**: Julio 2025  
-**Licencia**: Privada - Biergarten Klein
+
+La aplicación estará disponible en http://localhost:6001.
+
+#### **Modo Producción**
+
+Usa Gunicorn para gestionar los workers de Uvicorn:
+
+gunicorn \-w 4 \-k uvicorn.workers.UvicornWorker app:app \--bind 0.0.0.0:6001
+
+## **🏗️ Resumen de la Arquitectura**
+
+El proyecto sigue una arquitectura modular separando la configuración, la lógica de negocio, las rutas y los modelos de datos.
+
+* **main.py**: Punto de entrada principal. Inicia el servidor Uvicorn leyendo la configuración.  
+* **app.py**: Corazón de la aplicación FastAPI. Aquí se configuran los middlewares (CORS, NoCache), se montan las rutas (routers) y se define la lógica de inicio.  
+* **config/**: Contiene la configuración centralizada.  
+  * settings.py: Carga las variables de entorno (.env) usando Pydantic.  
+  * messages.py: Mensajes de sistema para el chatbot.  
+  * mails.py: Plantillas para correos de notificación.  
+* **models/**: Define los esquemas de datos (Pydantic) usados en la API para validación y serialización (chat.py, items.py, user.py, sales.py).  
+* **routes/**: Define los *routers* de FastAPI (endpoints de la API). Cada archivo agrupa endpoints por funcionalidad (chat.py, orders.py, products.py, users.py, sales.py).  
+  * static.py: Sirve los archivos del frontend (public/).  
+* **services/**: Contiene la lógica de negocio principal, desacoplada de las rutas.  
+  * data\_service.py: Gestiona la carga de datos (productos, usuarios) desde archivos JSON.  
+  * openai\_service/: Lógica para interactuar con la API de OpenAI, incluyendo la gestión de herramientas (tools).  
+  * fudo\_service.py: Lógica para la integración con el POS Fudo.  
+  * print\_service.py: Lógica para interactuar con la impresora térmica USB.  
+  * email\_service.py: Envío de notificaciones por email.  
+  * recovery\_service.py: Lógica para recuperación de acceso.  
+* **auth/**: Lógica de seguridad.  
+  * security.py: Generación y validación de tokens (anti-abuso, sesiones).  
+* **middleware/**: Middlewares personalizados de FastAPI.  
+  * no\_cache.py: Asegura que las respuestas de la API no se almacenen en caché.  
+  * in\_time.py: (Posiblemente) valida si las operaciones se realizan en horario comercial.  
+* **utils/**: Funciones de utilidad reutilizables.  
+  * responses.py: Constructores de respuestas HTTP estandarizadas.  
+  * rut.py: Utilidades para validación de RUT.  
+* **data/**: Almacenamiento de datos estáticos.  
+  * products.json: Catálogo de productos y cervezas.  
+  * llm\_data.json: Base de conocimientos específica para el asistente de IA.  
+* **public/**: Contiene el frontend (cliente web estático) en Vanilla JS, HTML y CSS.  
+* **logs/**: Directorio donde se escriben los logs de la aplicación (ej. app.log).
+
+## **🤖 Características Principales**
+
+### **Asistente de IA (Camilo Klein)**
+
+* Integración con **OpenAI GPT-4o-mini** (services/openai\_service).  
+* Base de conocimientos personalizada (data/llm\_data.json).  
+* Sistema de "tools" de OpenAI para interactuar con el catálogo de productos.  
+* Sistema de tokens anti-abuso para proteger el endpoint (auth/security.py).
+
+### **Gestión de Pedidos y Productos**
+
+* Catálogo de productos cargado desde products.json (services/data\_service.py).  
+* Carrito de compras gestionado en el frontend (public/main/js/utils/shoppingCart.js).  
+* Procesamiento de pedidos (routes/orders.py).
+
+### **Integraciones de Hardware y Externas**
+
+* **Impresora Térmica USB**: El print\_service.py se encarga de formatear y enviar los pedidos a la impresora.  
+* **Fudo POS**: Sincronización de pedidos con el sistema de punto de venta (services/fudo\_service.py).  
+* **Email**: Notificaciones de error o críticas del sistema (services/email\_service.py).
+
+### **Seguridad**
+
+* **Tokens anti-abuso**: Se genera un token al iniciar el chat (/api/chat/init-chat) que debe usarse para todas las peticiones de chat (/api/chat/completions).  
+* **Validación de Sesión**: Protección de endpoints sensibles.  
+* **Variables de Entorno**: No hay claves quemadas en el código; todo se gestiona vía config/settings.py.
+
+## **🔌 API Endpoints (Resumen)**
+
+Consulta los archivos en routes/ para ver la definición completa, incluyendo los modelos de request y response.
+
+### **Chat (Asistente IA)**
+
+* GET /api/chat/init-chat: Inicia una sesión de chat y obtiene un token anti-abuso.  
+* POST /api/chat/completions: Envía un mensaje al asistente (requiere token).
+
+### **Productos**
+
+* GET /api/get\_products: Obtiene el catálogo completo de productos.
+
+### **Pedidos**
+
+* POST /api/printer/order: Recibe una orden, la procesa, la imprime y la envía a Fudo.
+
+### **Usuarios**
+
+* POST /api/existsUser: Valida la existencia de un código de usuario/mesa.  
+* *(Otros endpoints de autenticación y registro en routes/users.py)*.
+
+### **Ventas (Sales)**
+
+* *(Endpoints definidos en routes/sales.py para gestionar ventas)*.
+
+### **Frontend**
+
+* GET /: Sirve el index.html principal (public/main/index.html).  
+* GET /register: Sirve la página de registro.  
+* GET /verify: Sirve la página de verificación.  
+* *(Rutas estáticas para js, css, assets)*.
+
+## **🛠️ Tecnologías Utilizadas**
+
+* **Backend**: FastAPI, Python 3.9+  
+* **Servidor ASGI**: Uvicorn, Gunicorn  
+* **Frontend**: Vanilla JavaScript (ES6+), Tailwind CSS, HTML5  
+* **Base de Datos (Estado/Caché)**: Redis  
+* **Base de Datos (Datos Estáticos)**: Archivos JSON  
+* **IA**: OpenAI API (GPT-4o-mini)  
+* **Integraciones**: Fudo POS (API REST), Impresora Térmica (USB), SMTP
+
+## **📝 Logging y Monitoreo**
+
+* El logging está configurado en config/settings.py.  
+* Los logs de la aplicación se escriben por defecto en logs/app.log.  
+* Se registran eventos críticos como errores de impresión, fallos en la API de Fudo y errores inesperados del servidor.

+ 2 - 2
config/mails.py

@@ -297,7 +297,7 @@ PRINTER_DISCONNECTED_MAIL = {
                                 <tr>
                                     <td style="text-align: center; margin-bottom: 32px;">
                                         <h2 style="color: #dc2626; font-size: 24px; margin: 0 0 12px 0; font-weight: bold;">Impresora Desconectada</h2>
-                                        <p style="color: #6b7280; font-size: 16px; line-height: 1.6; margin: 0;">Se ha detectado una desconexión en el sistema de impresión de {restaurant_name}.</p>
+                                        <p style="color: #6b7280; font-size: 16px; line-height: 1.6; margin: 0;">Se ha detectado una desconexión en el sistema de impresión de {location}.</p>
                                     </td>
                                 </tr>
                             </table>
@@ -310,7 +310,7 @@ PRINTER_DISCONNECTED_MAIL = {
                                         <ul style="list-style: none; padding: 0; margin: 0;">
                                             <li style="color: #374151; font-size: 14px; line-height: 1.5; margin-bottom: 8px; padding-left: 20px; position: relative;">
                                                 <span style="color: #dc2626; font-weight: bold; position: absolute; left: 0;">🖨️</span>
-                                                <strong>Impresora:</strong> {printer_name}
+                                                <strong>Impresora:</strong> {location}
                                             </li>
                                             <li style="color: #374151; font-size: 14px; line-height: 1.5; margin-bottom: 8px; padding-left: 20px; position: relative;">
                                                 <span style="color: #dc2626; font-weight: bold; position: absolute; left: 0;">⏰</span>

+ 0 - 553
doc_api.md

@@ -1,553 +0,0 @@
-# API Documentation - Web Pedidos Klein
-
-## Información General
-
-**Título:** Web Pedidos Klein - FastAPI Backend  
-**Descripción:** Backend para la aplicación Web Pedidos Klein utilizando FastAPI  
-**URL Base:** `http://localhost:8000` (o su dominio de producción)
-
-## Autenticación
- 
-La API utiliza autenticación basada en tokens JWT. La mayoría de endpoints requieren autenticación a través del header:
-```
-Authorization: Bearer <token>
-```
-
-## Endpoints
-
-### 🔐 Autenticación y Usuarios (`/api/users`)
-
-#### Verificar existencia de usuario
-- **POST** `/api/users/exists`
-- **Descripción:** Verifica si un usuario existe por ID
-- **Cuerpo de solicitud:**
-  ```json
-  {
-    "id": 123
-  }
-  ```
-- **Respuestas:**
-  - `200`: Usuario existe
-  - `404`: Usuario no encontrado
-
-#### Registrar usuario
-- **POST** `/api/users/register`
-- **Descripción:** Registra un nuevo usuario y envía código de verificación por email
-- **Cuerpo de solicitud:**
-  ```json
-  {
-    "name": "Juan Pérez",
-    "email": "juan@ejemplo.com",
-    "rut": "12345678-9"
-  }
-  ```
-- **Respuestas:**
-  - `201`: Usuario registrado exitosamente
-  - `400`: Usuario ya existe
-
-#### Crear usuario con PIN
-- **POST** `/api/users/create-user?q={verification_code}`
-- **Descripción:** Completa el registro del usuario estableciendo un PIN de 4 dígitos
-- **Parámetros de consulta:**
-  - `q`: Código de verificación recibido por email
-- **Cuerpo de solicitud:**
-  ```json
-  {
-    "pin": "1234"
-  }
-  ```
-- **Respuestas:**
-  - `201`: Usuario creado exitosamente con token
-  - `400`: PIN inválido o código de verificación expirado
-
-#### Iniciar sesión
-- **POST** `/api/users/login`
-- **Descripción:** Autentica un usuario con email y PIN
-- **Cuerpo de solicitud:**
-  ```json
-  {
-    "email": "juan@ejemplo.com",
-    "pin": "1234"
-  }
-  ```
-- **Respuestas:**
-  - `200`: Login exitoso con datos de usuario y token
-  - `401`: Credenciales inválidas
-  - `403`: Usuario bloqueado por intentos fallidos
-  - `429`: Demasiados intentos de login
-
-#### Eliminar usuario
-- **DELETE** `/api/users/delete`
-- **Descripción:** Elimina un usuario por ID
-- **Cuerpo de solicitud:**
-  ```json
-  {
-    "id": 123
-  }
-  ```
-- **Respuestas:**
-  - `200`: Usuario eliminado
-  - `404`: Usuario no encontrado
-
-#### Obtener todos los usuarios
-- **GET** `/api/users/all`
-- **Descripción:** Obtiene lista de todos los usuarios registrados
-- **Respuestas:**
-  - `200`: Lista de usuarios
-
-### 📧 Verificación (`/verify`)
-
-#### Verificar usuario
-- **GET** `/verify/?q={verification_code}`
-- **Descripción:** Página de verificación de usuario
-- **Parámetros de consulta:**
-  - `q`: Código de verificación
-- **Respuestas:**
-  - `200`: Página de verificación HTML
-  - `400`: Código de verificación inválido
-
-### 🛍️ Productos (`/api/products`)
-*Requiere autenticación*
-
-#### Obtener productos
-- **GET** `/api/products/`
-- **Descripción:** Obtiene lista de todos los productos disponibles
-- **Headers requeridos:** `Authorization: Bearer <token>`
-- **Respuestas:**
-  - `200`: Lista de productos
-  ```json
-  {
-    "products": [
-      {
-        "id": 1,
-        "name": "Producto 1",
-        "type": "bebida",
-        "description": "Descripción del producto",
-        "price": 1500.0,
-        "image": "url_imagen.jpg",
-        "status": 1,
-        "quantity": 1
-      }
-    ],
-    "message": "Productos obtenidos correctamente"
-  }
-  ```
-
-#### Obtener producto específico
-- **GET** `/api/products/{product_id}`
-- **Descripción:** Obtiene un producto específico por su ID
-- **Headers requeridos:** `Authorization: Bearer <token>`
-- **Parámetros de ruta:**
-  - `product_id`: ID del producto
-- **Respuestas:**
-  - `200`: Producto encontrado
-  ```json
-  {
-    "product": {
-      "id": 1,
-      "name": "Producto 1",
-      "type": "bebida",
-      "description": "Descripción del producto",
-      "price": 1500.0,
-      "image": "url_imagen.jpg",
-      "status": 1,
-      "quantity": 1
-    },
-    "message": "Productos obtenidos correctamente"
-  }
-  ```
-  - `404`: Producto no encontrado
-
-#### Editar producto
-- **PATCH** `/api/products/edit`
-- **Descripción:** Edita un producto existente (requiere permisos de nivel 1 o superior)
-- **Headers requeridos:** `Authorization: Bearer <token>`
-- **Cuerpo de solicitud:**
-  ```json
-  {
-    "id": 1,
-    "name": "Producto Actualizado",
-    "type": "comida",
-    "description": "Nueva descripción",
-    "price": 2000.0,
-    "image": "nueva_imagen.jpg",
-    "status": 1,
-    "quantity": 5
-  }
-  ```
-  **Nota:** Todos los campos excepto `id` son opcionales
-- **Respuestas:**
-  - `200`: Producto editado exitosamente
-  - `403`: Sin permisos para realizar esta acción
-
-#### Crear producto
-- **POST** `/api/products/create`
-- **Descripción:** Crea un nuevo producto (requiere permisos de nivel 1 o superior)
-- **Headers requeridos:** `Authorization: Bearer <token>`
-- **Cuerpo de solicitud:**
-  ```json
-  {
-    "name": "Nuevo Producto",
-    "type": "bebida",
-    "description": "Descripción del nuevo producto",
-    "price": 1800.0,
-    "image": "imagen_producto.jpg",
-    "status": 1,
-    "quantity": 10
-  }
-  ```
-- **Respuestas:**
-  - `200`: Producto creado exitosamente
-  - `403`: Sin permisos para realizar esta acción
-
-#### Eliminar producto
-- **DELETE** `/api/products/{product_id}`
-- **Descripción:** Elimina un producto (requiere permisos de nivel 2 - administrador)
-- **Headers requeridos:** `Authorization: Bearer <token>`
-- **Parámetros de ruta:**
-  - `product_id`: ID del producto a eliminar
-- **Respuestas:**
-  - `200`: Producto eliminado exitosamente
-  - `403`: Sin permisos para realizar esta acción
-
-### 🛒 Pedidos (`/api/orders`)
-*Requiere autenticación*
-
-#### Enviar pedido
-- **POST** `/api/orders/send`
-- **Descripción:** Procesa un pedido, lo envía al sistema Fudo y a la impresora
-- **Headers requeridos:** `Authorization: Bearer <token>`
-- **Cuerpo de solicitud:**
-  ```json
-  {
-    "customerId": 123,
-    "items": [
-      {
-        "id": 1,
-        "quantity": 2
-      },
-      {
-        "id": 2,
-        "quantity": 1
-      }
-    ],
-    "totalAmount": 4500.0,
-    "orderDate": "2025-07-31T10:30:00",
-    "table": 5
-  }
-  ```
-- **Respuestas:**
-  - `200`: Pedido procesado exitosamente
-  - `400`: Campos faltantes o tipo de mesa inválido
-  - `404`: Usuario no encontrado o venta activa no encontrada
-  - `424`: Impresora desconectada o error agregando productos
-
-### 💰 Ventas (`/api/sales`)
-*Requiere autenticación*
-
-#### Obtener ventas por usuario
-- **GET** `/api/sales/user/{user_id}`
-- **Descripción:** Obtiene el historial de ventas de un usuario específico
-- **Headers requeridos:** `Authorization: Bearer <token>`
-- **Parámetros de ruta:**
-  - `user_id`: ID del usuario
-- **Respuestas:**
-  - `200`: Lista de ventas del usuario
-  ```json
-  {
-    "sales": [
-      {
-        "id": 1,
-        "user_id": 123,
-        "total": 4500.0,
-        "fudo_id": "abc123",
-        "date": "2025-07-31T10:30:00",
-        "table": 5,
-        "username": "Juan Pérez",
-        "user_email": "juan@ejemplo.com",
-        "products": [...]
-      }
-    ],
-    "message": "Ventas obtenidas correctamente"
-  }
-  ```
-  - `404`: No se encontraron ventas
-
-### 💬 Chat (`/api/chat`)
-*Requiere autenticación*
-
-#### Completar chat
-- **POST** `/api/chat/completions`
-- **Descripción:** Obtiene respuestas de chat utilizando OpenAI
-- **Headers requeridos:** `Authorization: Bearer <token>`
-- **Cuerpo de solicitud:**
-  ```json
-  {
-    "messages": [
-      {
-        "role": "user",
-        "content": "Hola, ¿qué productos tienen disponibles?"
-      }
-    ],
-    "user": "juan@ejemplo.com"
-  }
-  ```
-- **Respuestas:**
-  - `200`: Respuesta del chat
-  ```json
-  {
-    "response": "Respuesta generada por IA",
-    "message": "Respuesta de chat generada exitosamente"
-  }
-  ```
-  - `500`: Error interno del servidor
-
-### 🌐 Rutas Estáticas
-
-#### Página principal
-- **GET** `/`
-- **Descripción:** Sirve la página principal de la aplicación
-- **Respuestas:**
-  - `200`: Página HTML principal
-  - `404`: Archivo no encontrado
-
-#### Página de registro
-- **GET** `/register`
-- **Descripción:** Sirve la página de registro
-- **Respuestas:**
-  - `200`: Página HTML de registro
-  - `404`: Archivo no encontrado
-
-## Modelos de Datos
-
-### Usuario (User)
-```json
-{
-  "id": 123,
-  "email": "usuario@ejemplo.com",
-  "name": "Nombre Usuario",
-  "rut": "12345678-9",
-  "pin_hash": "hashed_pin_value",
-  "kleincoins": "100.00",
-  "created_at": "2025-07-31T10:30:00"
-}
-```
-
-### Producto (Product)
-```json
-{
-  "id": 1,
-  "name": "Nombre del Producto",
-  "type": "bebida",
-  "description": "Descripción del producto",
-  "price": 1500.0,
-  "image": "url_imagen.jpg",
-  "status": 1,
-  "quantity": 1
-}
-```
-**Campos:**
-- `id`: Identificador único
-- `name`: Nombre del producto
-- `type`: Tipo/categoría del producto (opcional)
-- `description`: Descripción del producto (opcional)
-- `price`: Precio del producto
-- `image`: URL de la imagen (opcional)
-- `status`: Estado del producto (0: Inactivo, 1: Activo)
-- `quantity`: Cantidad disponible (opcional, por defecto 1)
-
-### Solicitud de Edición de Producto (ProductEditRequest)
-```json
-{
-  "id": 1,
-  "name": "Producto Actualizado",
-  "type": "comida",
-  "description": "Nueva descripción",
-  "price": 2000.0,
-  "image": "nueva_imagen.jpg",
-  "status": 1,
-  "quantity": 5
-}
-```
-**Campos:**
-- `id`: Identificador único (requerido)
-- `name`: Nombre del producto (opcional)
-- `type`: Tipo/categoría del producto (opcional)
-- `description`: Descripción del producto (opcional)
-- `price`: Precio del producto (opcional)
-- `image`: URL de la imagen (opcional)
-- `status`: Estado del producto (opcional) - 0: Inactivo, 1: Activo
-- `quantity`: Cantidad disponible (opcional)
-
-### Solicitud de Creación de Producto (ProductCreateRequest)
-```json
-{
-  "name": "Nuevo Producto",
-  "type": "bebida",
-  "description": "Descripción del nuevo producto",
-  "price": 1800.0,
-  "image": "imagen_producto.jpg",
-  "status": 1,
-  "quantity": 10
-}
-```
-**Campos:**
-- `name`: Nombre del producto (requerido)
-- `type`: Tipo/categoría del producto (requerido)
-- `description`: Descripción del producto (requerido)
-- `price`: Precio del producto (requerido)
-- `image`: URL de la imagen (requerido)
-- `status`: Estado del producto (opcional, por defecto 1) - 0: Inactivo, 1: Activo
-- `quantity`: Cantidad disponible (opcional, por defecto 1)
-
-### Pedido (OrderWeb)
-```json
-{
-  "customerId": 123,
-  "items": [
-    {
-      "id": 1,
-      "quantity": 2
-    }
-  ],
-  "totalAmount": 3000.0,
-  "orderDate": "2025-07-31T10:30:00",
-  "table": 5
-}
-```
-
-### Item del Pedido (ItemWeb)
-```json
-{
-  "id": 1,
-  "quantity": 2
-}
-```
-
-### Venta (Sale)
-```json
-{
-  "id": 1,
-  "user_id": 123,
-  "total": 3000.0,
-  "fudo_id": "abc123",
-  "date": "2025-07-31T10:30:00",
-  "table": 5,
-  "username": "Juan Pérez",
-  "user_email": "juan@ejemplo.com",
-  "products": [
-    {
-      "id": 1,
-      "name": "Producto 1",
-      "price": 1500.0,
-      "quantity": 2
-    }
-  ]
-}
-```
-
-### Mensaje de Chat (Message)
-```json
-{
-  "role": "user",
-  "content": "¿Qué productos tienen disponibles?"
-}
-```
-**Roles disponibles:** `user`, `assistant`, `system`
-
-### Solicitud de Chat (ChatCompletionRequest)
-```json
-{
-  "messages": [
-    {
-      "role": "user",
-      "content": "Hola, necesito ayuda"
-    }
-  ],
-  "user": "usuario@ejemplo.com"
-}
-```
-
-### Lista Negra (Blacklist)
-```json
-{
-  "id": 1,
-  "user_id": 123,
-  "email": "usuario@ejemplo.com",
-  "name": "Juan Pérez",
-  "rut": "12345678-9"
-}
-```
-**Campos:**
-- `id`: Identificador único
-- `user_id`: ID del usuario en lista negra
-- `email`: Email del usuario (opcional)
-- `name`: Nombre del usuario (opcional)
-- `rut`: RUT del usuario (opcional)
-
-### Modelos de Solicitud
-
-#### Registro de Usuario (RegisterUserRequest)
-```json
-{
-  "name": "Juan Pérez",
-  "email": "juan@ejemplo.com",
-  "rut": "12345678-9"
-}
-```
-
-#### Login de Usuario (LoginRequest)
-```json
-{
-  "email": "juan@ejemplo.com",
-  "pin": "1234"
-}
-```
-
-#### PIN de Usuario (PinUserRequest)
-```json
-{
-  "pin": "1234"
-}
-```
-**Validación:** PIN debe ser exactamente 4 dígitos
-
-#### ID de Usuario (UserIDRequest)
-```json
-{
-  "id": 123
-}
-```
-
-## Códigos de Error Comunes
-
-- **400**: Solicitud incorrecta (datos faltantes o inválidos)
-- **401**: No autorizado (token inválido o credenciales incorrectas)
-- **403**: Prohibido (usuario bloqueado)
-- **404**: No encontrado (recurso no existe)
-- **424**: Dependencia fallida (problemas con servicios externos como impresora)
-- **429**: Demasiadas solicitudes (límite de intentos alcanzado)
-- **500**: Error interno del servidor
-
-## Notas Importantes
-
-1. **Autenticación**: La mayoría de endpoints requieren un token JWT válido
-2. **Límites de intentos**: El sistema bloquea usuarios después de 5 intentos fallidos de login
-3. **Verificación por email**: El registro requiere verificación por email antes de completarse
-4. **Integración con Fudo**: Los pedidos se sincronizan automáticamente con el sistema Fudo
-5. **Impresión automática**: Los pedidos se envían automáticamente a la impresora USB configurada
-6. **Logs**: Todas las interacciones importantes se registran para auditoría
-7. **Niveles de permisos**: 
-   - **Nivel 0**: Usuario normal (solo consulta de productos)
-   - **Nivel 1**: Usuario con permisos de edición (puede crear y editar productos)
-   - **Nivel 2**: Administrador (puede eliminar productos)
-
-## Middleware
-
-- **SessionMiddleware**: Manejo de sesiones con tiempo de expiración de 60 minutos
-- **CORS**: Configurado para permitir solicitudes cross-origin según necesidades
-
-## Archivos Estáticos
-
-- **Principales**: `/express/` - Archivos de la aplicación principal
-- **Registro**: `/register/` - Archivos de la página de registro

+ 107 - 15
fudo/fudo.py

@@ -110,26 +110,118 @@ def get_products():
     }
     r = requests.get(url, headers=headers)
     return list(filter(lambda x: x['relationships']['productCategory']['data']['id'] == '1', r.json()['data']))
+N_PER_PAGE = 100
 
-def get_table(number:int):
-    n_per_page = 10
-    page = math.ceil(number / n_per_page)
-    url = 'https://api.fu.do/v1alpha1/tables?page[number]={}&page[size]={}&include=activeSales&sort=number'.format(page, n_per_page)
+def _get_page_bounds(page: int, token: str):
+    """
+    Función helper: Obtiene una página y devuelve el primer y último 
+    número de mesa en ella, y los datos.
+    """
+    url = (
+        'https://api.fu.do/v1alpha1/tables'
+        f'?page[number]={page}&page[size]={N_PER_PAGE}'
+        '&include=activeSales&sort=number'
+    )
+    headers = {'Authorization': 'Bearer ' + token}
+    
+    try:
+        r = requests.get(url, headers=headers, timeout=10)
+        if r.status_code != 200:
+            return None, None, None # Error de API
+            
+        data = r.json().get('data', [])
+        
+        if not data:
+            return 0, 0, [] # Página vacía
+
+        first_number = data[0]['attributes']['number']
+        last_number = data[-1]['attributes']['number']
+        return first_number, last_number, data
+        
+    except requests.RequestException as e:
+        print(f"Error de request en página {page}: {e}")
+        return None, None, None
+
+def get_table(number: int):
     token = get_token()
-    headers = {
-        'Authorization': 'Bearer ' + token
-    }
-    r = requests.get(url, headers=headers)
-    if r.status_code != 200:
-        logger.error('Error al obtener tablas:' + str(r.json()['errors']))
-        return None
+    
+    # --- FASE 1: BÚSQUEDA EXPONENCIAL (Encontrar rango) ---
+    # Encontrar un 'high_bound' (página) donde el último N° sea >= 'number'
+    
+    page = 1
+    low_bound_page = 1
+    high_bound_page = 1
+    
+    # Primero, revisamos la página 1
+    first_num, last_num, page_data = _get_page_bounds(page, token)
+    
+    if first_num is None:
+        return None # Error en la primera petición
+        
+    if not page_data:
+        return None # No hay mesas en total
+
+    # Si está en la página 1
+    if number >= first_num and number <= last_num:
+        low_bound_page = 1
+        high_bound_page = 1
+    
+    # Si es mayor, empezamos a saltar exponencialmente
+    elif number > last_num:
+        low_bound_page = 2
+        page_jump = 2
+        while True:
+            current_page = low_bound_page + page_jump - 1
+            first, last, data = _get_page_bounds(current_page, token)
+            
+            if not data:
+                # Nos pasamos, el rango es entre low y la página actual
+                high_bound_page = current_page - 1
+                break
+            
+            if number <= last:
+                # Encontramos el techo. El rango es [low_bound_page, current_page]
+                high_bound_page = current_page
+                break
+            
+            # Si no, actualizamos el 'piso' y duplicamos el salto
+            low_bound_page = current_page + 1
+            page_jump *= 2
+
+    # --- FASE 2: BÚSQUEDA BINARIA (En el rango) ---
+    
+    target_page_data = []
+
+    while low_bound_page <= high_bound_page:
+        mid_page = (low_bound_page + high_bound_page) // 2
+        first, last, data = _get_page_bounds(mid_page, token)
+
+        if not data:
+            # Página vacía, buscar en la mitad inferior
+            high_bound_page = mid_page - 1
+            continue
+
+        if number >= first and number <= last:
+            # ¡Encontramos la página correcta!
+            target_page_data = data
+            break
+        elif number < first:
+            # Está en una página anterior
+            high_bound_page = mid_page - 1
+        else: # number > last
+            # Está en una página posterior
+            low_bound_page = mid_page + 1
+            
+
+    # Filtramos la página que encontramos
     try:
-        return list(filter(lambda x: x['attributes']['number'] == number, r.json()['data']))[0]
-    except:
-        logger.error('Error al obtener tabla')
-        logger.error(r.json())
+        return list(filter(lambda x: x['attributes']['number'] == number, target_page_data))[0]
+    except IndexError:
+        # Esto no debería pasar si la lógica es correcta,
+        # pero es una salvaguarda
         return None
 
+
 def get_sale(sale_id:int):
     url = 'https://api.fu.do/v1alpha1/sales/{}'.format(sale_id)
     token = get_token()

+ 0 - 0
pin.key


+ 9 - 0
public/main/animations.css

@@ -89,4 +89,13 @@
 
         .sparkle {
             animation: sparkle 1.5s ease-in-out infinite;
+        }
+
+        @keyframes bg-loaded {
+            0% { transform: scale(0); filter: blur(10px);}
+            100% { transform: scale(1); filter: blur(0);}
+        }
+
+        .bg-loaded {
+            animation: bg-loaded 200ms ease-in-out;
         }

BIN
public/main/assets/no_image.png


+ 2 - 19
public/main/index.html

@@ -14,27 +14,10 @@
   <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
   <link rel="stylesheet" as="style" onload="this.rel='stylesheet'"
         href="https://fonts.googleapis.com/css2?display=swap&family=Noto+Sans:wght@400;500;700;900&family=Spline+Sans:wght@400;500;700">
-  <script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
   <link rel="stylesheet" href="express/animations.css">
-   <!--Tailwind-->
-   <script>
-        tailwind.config = {
-            theme: {
-                extend: {
-                    colors: {
-                        'custom-dark': '#101419',
-                        'custom-dark-hover': '#37404a',
-                        'gray-50': '#f9fafb',
-                        'gray-100': '#f3f4f6',
-                    }
-                }
-            }
-        }
-    </script>
-  <!-- Markdown -->
-  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
-  <script src="express/js/app.js" type="module"></script>
   <link rel="stylesheet" href="express/styles.css">
+  <link rel="stylesheet" href="express/tlw.css">
+  <script src="express/js/app.js" type="module"></script>
 </head>
 <body class="h-[100dvh] max-h-[100dvh] flex flex-col bg-gray-50 overflow-x-hidden"
       style='font-family:"Spline Sans","Noto Sans",sans-serif;'>

+ 22 - 10
public/main/js/app.js

@@ -1,7 +1,7 @@
 //--- Imports ---
 import { getOnlineUserCount, getUserList } from './service/chat.js';
 import { getProducts, sendOrder } from './service/product.js';
-import { login } from './service/auth.js';
+import { login, existsTable } from './service/auth.js';
 import { createGlobalLoader, showGlobalLoader, hideGlobalLoader } from './utils/loader.js';
 import { updateProgress, claimReward, getProgress } from './utils/progressBar.js';
 import { showError } from './utils/error.js';
@@ -10,6 +10,7 @@ import { hideGUI, showGUI } from './utils/gui.js';
 import { smartSearch } from './utils/searching.js';
 import { getUserData } from './service/user.js';
 import { getComment, COMMENT_TYPES } from './utils/get_comment.js';
+import { imageObserver } from './utils/observer.js';
 
 // --- Variables Globales ---
 
@@ -145,23 +146,29 @@ function initializeLoginModal() {
     loginForm.addEventListener('submit', async (event) => {
         event.preventDefault();
         event.stopPropagation();
+        showGlobalLoader("Iniciando sesión...");
         const fd = new FormData(loginForm);
+        userTable = Number(fd.get('table').trim());
+        if (!(await existsTable(userTable))) {
+            showError("No existe la mesa seleccionada.");
+            hideGlobalLoader();
+            return;
+        }
         if (cacheMode) {
-            userTable = Number(fd.get('table').trim());
             sessionModal.classList.add('hidden');
             initializeApp();
             return;
         }
         const email = fd.get('email').trim();
         const pin = fd.get('pin').trim();
-        userTable = Number(fd.get('table').trim());
 
         if (!email || !pin || !userTable) {
             showError("Por favor, completa todos los campos.");
+            hideGlobalLoader();
             return;
         }
         try {
-            const { data } = await login(email, pin);
+            const data = await login(email, pin);
             userToken = data.token;
             userName = data.name;
             userId = data.id;
@@ -169,6 +176,7 @@ function initializeLoginModal() {
             updateProgress(data.reward_progress || 0);
             if (!userToken || data.id === undefined) {
                 showError("Error al iniciar sesión.");
+                hideGlobalLoader();
                 return;
             }
 
@@ -703,13 +711,15 @@ async function renderProducts(products, groupInCategories = true, searchTerm = "
         if (productsInCategory.length === 0) continue;
 
         productsInCategory.forEach(product => {
-            const clone = template.content.cloneNode(true);
+            const clone = template.content.cloneNode(true).firstElementChild;
 
             clone.querySelector(".product-type").textContent = product.type || "Sin categoría";
             clone.querySelector(".product-name").textContent = product.name;
             clone.querySelector(".product-description").textContent = product.description;
             clone.querySelector(".product-price").textContent = formatPrice(product.price);
-            clone.querySelector(".product-image").style.backgroundImage = `url('${product.image}')`;
+            const image = clone.querySelector(".product-image");
+            image.dataset.src = product.image
+
 
             const addBtn = clone.querySelector(".add-to-cart-btn");
             addBtn.dataset.productId = product.id;
@@ -719,6 +729,7 @@ async function renderProducts(products, groupInCategories = true, searchTerm = "
             });
 
             container.appendChild(clone);
+            imageObserver.observe(image);
         });
     }
 }
@@ -804,13 +815,14 @@ async function renderProductsWithAnimation(products, groupInCategories = true, s
         }
 
             productsInCategory.forEach((product, index) => {
-                const clone = template.content.cloneNode(true);
+                const clone = template.content.cloneNode(true).firstElementChild;
 
                 clone.querySelector(".product-type").textContent = product.type || "Sin categoría";
                 clone.querySelector(".product-name").textContent = product.name;
                 clone.querySelector(".product-description").textContent = product.description;
                 clone.querySelector(".product-price").textContent = formatPrice(product.price);
-                clone.querySelector(".product-image").style.backgroundImage = `url('${product.image}')`;
+                const image = clone.querySelector(".product-image");
+                image.dataset.src = product.image
 
                 const addBtn = clone.querySelector(".add-to-cart-btn");
                 addBtn.dataset.productId = product.id;
@@ -828,7 +840,7 @@ async function renderProductsWithAnimation(products, groupInCategories = true, s
                 productCard.style.transition = "opacity 0.4s ease, transform 0.4s ease";
 
                 container.appendChild(clone);
-
+                imageObserver.observe(image);
                 // Animate in with staggered delay
                 setTimeout(() => {
                     productCard.style.opacity = "1";
@@ -872,7 +884,7 @@ window.addToCart = async function(productId, buttonElement = null) {
     const cartItem = cart.find(item => item.id === productId);
     if (cartItem) {
         cartItem.quantity++;
-        cartItem.comment += `${comment}, `;
+        cartItem.comment += `, ${comment}`;
 
     } else {
         cart.push({ ...product, quantity: 1, comment });

+ 38 - 18
public/main/js/service/auth.js

@@ -2,44 +2,64 @@
 import { beforeUnloadHandler } from "../app.js";
 import { showError } from "../utils/error.js";
 
-async function login(email,pin){
+async function login(email, pin, table) {
   const response = await fetch("/api/users/login", {
     method: "POST",
     headers: {
       "Content-Type": "application/json"
     },
     body: JSON.stringify({ email, pin })
-}
+  }
   );
   if (response.status == 420) {
     window.removeEventListener("beforeunload", beforeUnloadHandler);
     window.location.replace("/");
   }
   if (response.status == 404) {
-    const errorData = await response.json().catch(() => ({ message: "El usuario no fue encontrado." }));
-    throw new Error(errorData.message);
-  }else if (response.status == 401) {
-    const errorData = await response.json().catch(() => ({ message: "Credenciales incorrectas.", attempts: 0 }));
-    showError(`${errorData.message} Intentos restantes: ${errorData.attempts_remaining || 0}`);
-
+    const errorData = await response.json()
+    const data = errorData.error.message
+  } else if (response.status == 401) {
+    const errorData = await response.json()
+    const data = errorData.error.message
+    showError(`${data} Intentos restantes: ${errorData.error.attempts_remaining || 0}`);
     throw new Error(errorData.message);
   } else if (response.status == 429) {
-    const errorData = await response.json().catch(() => ({ message: "Demasiados intentos de inicio de sesión." }));
-    showError(`${errorData.message} Intenta más tarde.`);
+    const errorData = await response.json()
+    const data = errorData.error.message
+    showError(`${data} Intenta más tarde.`);
     throw new Error(errorData.message);
-  }else if (response.status != 200) {
+  } else if (response.status != 200) {
     console.error(response.status, response.statusText);
-    const errorData = await response.json().catch(() => ({ message: "Error al iniciar sesión." }));
-    showError(`${errorData.message}`);
-    throw new Error(errorData.message);
+    const errorData = await response.json()
+    const data = errorData.error.message
+    showError(`${data}`);
+    throw new Error(data);
   }
-  const data = await response.json();
-  if (!data || !data.data.token) {
+  const JSONdata = await response.json();
+  const data = JSONdata.data;
+  const userData = data.data;
+  console.log(userData);
+  if (!userData || !userData.token) {
     showError("Error al iniciar sesión, Intenta mas tarde.");
     throw new Error("Error al iniciar sesión.");
   }
-  return {data: data.data};
+
+
+  return userData;
 }
 
+async function existsTable(table) {
+    // Check if table exists
+  const responseTableExists = await fetch(`/api/store/tables/exists?q=${table}`, {
+    method: "GET",
+    headers: {
+      "Content-Type": "application/json"
+    }
+  });
+  
+  const data = (await responseTableExists.json()).data.exists;
+  return data;
+
+}
 
-export { login };
+export { login, existsTable };

+ 4 - 5
public/main/js/service/chat.js

@@ -33,12 +33,11 @@ async function getOnlineUserCount(token) {
     }
   });
 
-  if (!response.ok) {
-    const errorData = await response.json().catch(() => ({ message: "Respuesta no válida del servidor." }));
-    throw new Error(errorData.message || `Error del servidor: ${response.status}`);
-  }
   const data = await response.json();
-  return data.count || 0;
+  if (!data.success){
+    throw new Error(data.error.message);
+  }
+  return data.data.count || 0;
 }
 
 export { getUserList, getOnlineUserCount };

+ 3 - 3
public/main/js/service/product.js

@@ -6,11 +6,11 @@ import { beforeUnloadHandler } from "../app.js";
     window.location.replace("/");
   }
   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 errorData = await response.json()
+    throw new Error(errorData.error.message || `Error del servidor: ${response.status}`);
   }
   const data = await response.json();
-  return data;
+  return data.data;
 }
 
 async function sendOrder(order, token) {

+ 2 - 2
public/main/js/service/user.js

@@ -35,7 +35,7 @@ export async function getUserData(token) {
       throw new Error(`Error fetching user data: ${response.statusText}`);
     }
     const userData = await response.json();
-    return userData;
+    return userData.data;
   } catch (error) {
     console.error('Error fetching user data:', error);
     throw error;
@@ -77,7 +77,7 @@ async function fetchUserSales(userId, token) {
       throw new Error(`Error fetching user sales: ${response.statusText}`);
     }
     const sales = await response.json();
-    return sales.sales;
+    return sales.data.sales;
   } catch (error) {
     console.error('Error fetching user sales:', error);
     throw error;

+ 41 - 0
public/main/js/utils/observer.js

@@ -0,0 +1,41 @@
+
+const observerOptions = {
+  root: null, // Observa en relación al viewport
+  threshold: 0,
+  rootMargin: "0px 0px 150px 0px" 
+};
+
+/**
+ * Callback que se ejecuta cuando un elemento observado entra
+ * o sale del área de observación.
+ */
+const lazyLoadCallback = (entries, observer) => {
+  entries.forEach(entry => {
+    // Comprueba si el elemento está ahora visible (o a punto de estarlo)
+    if (entry.isIntersecting) {
+      const element = entry.target;
+      const imageElement = document.createElement('img');
+      const imageUrl = element.dataset.src;
+
+      if (imageUrl) {
+        imageElement.onload = () => {
+            // Asigna la imagen de fondo. Aquí es donde el navegador la descarga.
+            element.style.backgroundImage = `url('${imageUrl}')`;
+        }
+        imageElement.onerror = () => {
+            element.style.backgroundImage = `url('/express/assets/no_image.png')`;
+        };
+        // (Opcional) Añade una clase para animar la aparición
+        element.classList.add('bg-loaded');
+
+        imageElement.src = imageUrl;
+        observer.unobserve(element);
+    }
+    }
+
+            
+  });
+};
+
+// Crea la instancia del observer
+export const imageObserver = new IntersectionObserver(lazyLoadCallback, observerOptions);

Plik diff jest za duży
+ 0 - 0
public/main/tlw.css


+ 16 - 13
routes/chat.py

@@ -12,6 +12,7 @@ from models.user import User
 from services.openai_service.openai_service import generate_completion, admin_completion
 from auth.security import get_current_user
 from redis import Redis
+from utils.responses import success_response
 
 logger = logging.getLogger(__name__)
 chat_router = APIRouter()
@@ -47,13 +48,16 @@ async def chat_irc_endpoint(websocket: WebSocket):
         async with broadcast.subscribe(channel=broadcast_channel) as subscriber:
             logger.info(f"Subscribed to Redis channel '{broadcast_channel}'")
             logger.debug("Starting message read loop")
-            async for event in subscriber:  # type: ignore
-                try:
-                    logger.debug(f"Broadcasting message to WebSocket: {event.message}")
-                    await websocket.send_text(event.message)
-                except Exception as e:
-                    logger.error(f"Error sending message to WebSocket: {e}, closing connection.")
-                    break
+            try:
+                async for event in subscriber:  # type: ignore
+                    try:
+                        logger.debug(f"Broadcasting message to WebSocket: {event.message}")
+                        await websocket.send_text(event.message)
+                    except Exception as e:
+                        logger.error(f"Error sending message to WebSocket: {e}, closing connection.")
+                        break
+            except asyncio.CancelledError:
+                logger.info("Conection closed by client")
     await broadcast.connect()
     reader_task = asyncio.create_task(reader())
 
@@ -111,7 +115,6 @@ async def chat_irc_endpoint(websocket: WebSocket):
                     await broadcast.publish(channel=broadcast_channel, message=json.dumps({"type": "mentioned", "username": mention_username}))
                     continue
                 elif event_type == "pong":
-                    logger.debug(f"Received pong from user {current_user.email}")
                     continue
 
                 # Publicar en Redis
@@ -155,21 +158,21 @@ async def notify_users(message: NotifyRequest, _: User = Depends(get_current_use
     await broadcast.publish(channel=broadcast_channel, message=json.dumps(response))
     
     await broadcast.disconnect()
-    return {"status": "Notification sent"}
+    return success_response({"message": "Notification sent successfully"})
 
 @chat_router.get("/users")
-async def get_connected_users(q: Optional[str] = Query(None), _: User = Depends(get_current_user)):
+async def get__connected_users(q: Optional[str] = Query(None), _: User = Depends(get_current_user)):
     """Get a list of connected users (solo local al worker)"""
     # return {"users": [user.username for user in connected_users if q.lower() in user.username.lower()]}
     all_users = redis_client.smembers("connected_users")
     all_users = [json.loads(user)["username"] for user in all_users]  # type: ignore
     if q is None or q.strip() == "":
-        return {"users": all_users}
+        return success_response({"users": all_users})
     filtered_users = [user for user in all_users if q.lower() in user.lower()]
-    return {"users": filtered_users}
+    return success_response({"users": filtered_users})
 
 @chat_router.get("/onlines")
 async def get_online_user_count(_: User = Depends(get_current_user)):
     """Get the count of online users (solo local al worker)"""
     all_users = redis_client.smembers("connected_users")
-    return {"count": len(all_users)}  # type: ignore
+    return success_response({"count": len(all_users)})  # type: ignore

+ 45 - 35
routes/orders.py

@@ -20,6 +20,8 @@ from config.messages import ErrorResponse, SuccessResponse, UserResponse
 from config.settings import DEVELOPMENT
 from auth.security import get_current_user
 from data.product_category import CAT_ITEMS
+from utils.responses import error_response, success_response
+from datetime import datetime
 
 logger = getLogger(__name__)
 
@@ -47,12 +49,12 @@ async def printer_order(order: OrderWeb, current_user: User = Depends(get_curren
     # Input validation
     if not items or not table:
         logger.warning(f"Invalid order data from user {current_user.email}: missing items or table")
-        return JSONResponse(status_code=400, content={"message": ErrorResponse.MISSING_FIELDS})
+        return error_response({"message": ErrorResponse.MISSING_FIELDS}, status_code=400)
 
     if not isinstance(table, int):
         logger.warning(f"Invalid table type from user {current_user.email}: {type(table)}")
        
-        return JSONResponse(status_code=400, content={"message": ErrorResponse.INVALID_TABLE_TYPE})
+        return error_response({"message": ErrorResponse.INVALID_TABLE_TYPE}, status_code=400)
 
     logger.info(f"Processing order for table {table} with {len(items)} items")
 
@@ -61,21 +63,30 @@ async def printer_order(order: OrderWeb, current_user: User = Depends(get_curren
     # Get products data
     try:
         products = product_data_service.get_products([item.id for item in items])
+        # Me aseguro de que los items y los productos esten en el mismo orden
+        products = list(sorted(products, key=lambda x: x.id))
+        items = list(sorted(items, key=lambda x: x.id))
         logger.info(f"Retrieved {len(products)} products from database")
         
         
     except Exception as e:
-        error_msg = f"Error retrieving products: {e}"
+        error_msg = f"Error getting products: {e}"
         logger.error(error_msg)
-        return JSONResponse(status_code=500, content={"message": "Error interno del servidor"})
+        return error_response({"message": error_msg}, status_code=500)
 
-    types = set([CAT_ITEMS[product.type or ""] for product in products])
+    printers = {}
+    
+    for product in products:
+        location = CAT_ITEMS[product.type or ""]
+        if location.value not in printers:
+            printers[location.value] = ps.get_status(location)
 
     # Printer status validation
     if not DEVELOPMENT:
         try:
-            printer_status = filter(lambda x: x == False, [ps.get_status(loc) for loc in types])
-            if not printer_status:
+            printer_status = [key for key, value in printers.items() if value == False]
+            print(printer_status)
+            if list(printer_status):
                 logger.error(f"Printer is not connected. Order from user {current_user.email} cannot be processed.")
        
                 
@@ -83,20 +94,22 @@ async def printer_order(order: OrderWeb, current_user: User = Depends(get_curren
                 email_thread = Thread(
                     target=get_email_sender().send_email,
                     args=(
-                        PRINTER_DISCONNECTED_MAIL["subject"],
-                        PRINTER_DISCONNECTED_MAIL["body"].format(location=", ".join([loc.value for loc in printer_status])), #type: ignore
-                        ["erwinjacimino2003@gmail.com", "mompyn@gmail.com"]
+                        PRINTER_DISCONNECTED_MAIL["subject"].format(location=", ".join([loc for loc in printer_status])),
+                        PRINTER_DISCONNECTED_MAIL["body"].format(location=", ".join([loc for loc in printer_status]), timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S")), #type: ignore
+                        ["erwinjacimino2003@gmail.com", "mompyn@gmail.com"
+                        #  , "i.perez03@ufromail.cl", "marceloburkart94@gmail.com"
+                         ]
                     ),
                     daemon=True
                 )
                 email_thread.start()
                 
                 
-                return JSONResponse(status_code=424, content={"message": ErrorResponse.PRINTER_DISCONNECTED})
+                return error_response({"message": ErrorResponse.PRINTER_DISCONNECTED}, status_code=424)
                 
         except Exception as e:
             logger.error(f"Error checking printer status: {e}")
-            return JSONResponse(status_code=500, content={"message": "Error interno del servidor"})
+            return error_response({"message": f"Error checking printer status: {e}"}, status_code=424)
 
 
     # Input validation
@@ -143,15 +156,12 @@ async def printer_order(order: OrderWeb, current_user: User = Depends(get_curren
         error_msg = f"Error with Fudo integration: {e}"
         logger.error(error_msg)
         
-        return JSONResponse(status_code=500, content={"message": "Error interno del servidor"})
+        return error_response({"message": error_msg}, status_code=500)
 
     if product_errors:
         logger.error(f"Product errors occurred: {product_errors}")
         
-        return JSONResponse(
-            status_code=424, 
-            content={"message": ErrorResponse.PRODUCT_ADD_ERROR, "errors": product_errors}
-        )
+        return error_response({"message": ErrorResponse.PRODUCT_ADD_ERROR, "errors": product_errors}, status_code=424)
 
     # User validation
     try:
@@ -159,16 +169,15 @@ async def printer_order(order: OrderWeb, current_user: User = Depends(get_curren
         if not user:
             logger.warning(f"User not found: {order.customerId}")
             
-            return JSONResponse(status_code=404, content={"message": UserResponse.USER_NOT_FOUND.format(user_id=order.customerId)})
+            return error_response({"message": UserResponse.USER_NOT_FOUND.format(user_id=order.customerId)}, status_code=404)
         
         logger.info(f"Order customer validated: {user.email}")
         
         
     except Exception as e:
         error_msg = f"Error validating user {order.customerId}: {e}"
-        logger.error(error_msg)
         
-        return JSONResponse(status_code=500, content={"message": "Error interno del servidor"})
+        return error_response({"message": error_msg}, status_code=500)
     
     # Get active sale
     try:
@@ -177,19 +186,15 @@ async def printer_order(order: OrderWeb, current_user: User = Depends(get_curren
             error_msg = f"No active sale found for table {table}"
             logger.error(error_msg)
             
-            raise HTTPException(status_code=404, detail=error_msg)
+            return error_response({"message": error_msg}, status_code=404)
             
         active_sale_id = active_sale_id['id']
         logger.info(f"Active sale found for table {table}: {active_sale_id}")
-        
-        
-    except HTTPException:
-        raise
     except Exception as e:
         error_msg = f"Error retrieving active sale for table {table}: {e}"
         logger.error(error_msg)
         
-        raise HTTPException(status_code=500, detail="Error interno del servidor")
+        return error_response({"message": error_msg}, status_code=500)
 
     # Update user reward progress
     try:
@@ -221,21 +226,26 @@ async def printer_order(order: OrderWeb, current_user: User = Depends(get_curren
         else:
             error_msg = "Failed to create sale record"
             logger.error(error_msg)
-            return JSONResponse(status_code=500, content={"message": "Error interno del servidor"})
+            return error_response({"message": error_msg}, status_code=500)
             
     except Exception as e:
         error_msg = f"Error creating sale record: {e}"
         logger.error(error_msg)
         
-        return JSONResponse(status_code=500, content={"message": "Error interno del servidor"})
+        return error_response({"message": error_msg}, status_code=500)
 
     # Print order
     try:
-        pizza_items = [ Item(name=product.name, price=product.price, quantity=item.quantity, comment=item.comment) for item, product in zip(items, products) if CAT_ITEMS.get(product.type) == Locations.PIZZAS]#type: ignore
-        burger_items = [ Item(name=product.name, price=product.price, quantity=item.quantity, comment=item.comment) for item, product in zip(items, products) if CAT_ITEMS.get(product.type) == Locations.BURGUER]#type: ignore
-        bar_items = [ Item(name=product.name, price=product.price, quantity=item.quantity, comment=item.comment) for item, product in zip(items, products) if CAT_ITEMS.get(product.type) == Locations.BAR]#type: ignore
-        coctelery_items = [ Item(name=product.name, price=product.price, quantity=item.quantity, comment=item.comment) for item, product in zip(items, products) if CAT_ITEMS.get(product.type) == Locations.COCTELERY]#type: ignore
-        
+        print("items:")
+        print(items)
+        print("products:")
+        print(products)
+        pizza_items = [ Item(name=product.name, price=item.price, quantity=item.quantity, comment=item.comment) for item, product in zip(items, products) if CAT_ITEMS.get(product.type) == Locations.PIZZAS]#type: ignore
+        burger_items = [ Item(name=product.name, price=item.price, quantity=item.quantity, comment=item.comment) for item, product in zip(items, products) if CAT_ITEMS.get(product.type) == Locations.BURGUER]#type: ignore
+        bar_items = [ Item(name=product.name, price=item.price, quantity=item.quantity, comment=item.comment) for item, product in zip(items, products) if CAT_ITEMS.get(product.type) == Locations.BAR]#type: ignore
+        coctelery_items = [ Item(name=product.name, price=item.price, quantity=item.quantity, comment=item.comment) for item, product in zip(items, products) if CAT_ITEMS.get(product.type) == Locations.COCTELERY]#type: ignore
+        print("bar_items:")
+        print(list(map(lambda x: x.model_dump(), bar_items)))
 
         if pizza_items:
             ps.print_order(Order(table=table, items=pizza_items, customerName=user.name, totalAmount=order.totalAmount, orderDate=order.orderDate), location=Locations.PIZZAS)
@@ -257,11 +267,11 @@ async def printer_order(order: OrderWeb, current_user: User = Depends(get_curren
     except Exception as e:
         error_msg = f"Error printing order for table {table}: {e}"
         logger.error(error_msg)
-        return JSONResponse(status_code=500, content={"message": "Error interno del servidor"})
+        return error_response({"message": error_msg}, status_code=500)  
         # Don't fail the order for print issues, just log it
 
     logger.info(f"Logging order for table {table} with sale ID {sale}, products= {[(product.name, item.quantity) for product, item in zip(products, items)]}")
         
     logger.info(f"Order processing completed successfully for table {table}, sale ID: {sale}")
-    return JSONResponse({"message": SuccessResponse.ORDER_SUCCESS, "new_progress": new_progress})
+    return success_response({"message": SuccessResponse.ORDER_SUCCESS, "new_progress": new_progress})
 

+ 18 - 17
routes/products.py

@@ -32,6 +32,7 @@ from models.items import Product, ProductCreateRequest, ProductEditRequest
 from services.data_service import DataServiceFactory
 from config.messages import ErrorResponse, SuccessResponse, UserResponse
 from services.print_service import print_ticket
+from utils.responses import error_response, success_response
 
 # Initialize logger for this module
 logger = getLogger(__name__)
@@ -76,7 +77,7 @@ async def get_products(status: Optional[int] = Query(None), current_user = Depen
         # Filter products by status if provided
         all_products = [product for product in all_products if product['status'] == status]
 
-    return JSONResponse({"products": all_products, "message": SuccessResponse.PRODUCTS_FETCH_SUCCESS})
+    return success_response({"products": all_products, "message": SuccessResponse.PRODUCTS_FETCH_SUCCESS})
 
 @product_router.get("/{product_id}")
 async def get_product(product_id: int, current_user = Depends(get_current_user)):
@@ -96,10 +97,10 @@ async def get_product(product_id: int, current_user = Depends(get_current_user))
     # Attempt to find product by ID
     product = product_data_service.get_by_id(product_id)
     if product:
-        return JSONResponse({"product": product.model_dump(exclude={"promo_id", "promo_price", "promo_day"}), "message": SuccessResponse.PRODUCTS_FETCH_SUCCESS})
+        return success_response({"product": product.model_dump(exclude={"promo_id", "promo_price", "promo_day"}), "message": SuccessResponse.PRODUCTS_FETCH_SUCCESS})
 
     # Return 404 if product not found
-    return JSONResponse({"message": UserResponse.USER_NOT_FOUND.format(user_id=product_id)}, status_code=404)
+    return error_response({"message": UserResponse.USER_NOT_FOUND.format(user_id=product_id)}, status_code=404)
 
 @product_router.get("/free-beer/{table_id}")
 async def get_free_beer(table_id: int, current_user:User = Depends(get_current_user)):
@@ -114,10 +115,10 @@ async def get_free_beer(table_id: int, current_user:User = Depends(get_current_u
     
     if current_user.reward_progress >= 100:
         print_ticket(table_id)
-        return JSONResponse({"message": SuccessResponse.REWARD_SUCCESS}, status_code=200)    
+        return success_response({"message": SuccessResponse.REWARD_SUCCESS}, status_code=200)
 
     # Return 404 if free beer product not found
-    return JSONResponse({"message": UserResponse.USER_NOT_FOUND.format(user_id="free_beer")}, status_code=404)
+    return error_response({"message": UserResponse.USER_NOT_FOUND.format(user_id="free_beer")}, status_code=404)
 # MODERATE RISK OPERATIONS - Requires permissions >= 1 (Manager level or above)
 
 @product_router.post("/create")
@@ -139,10 +140,10 @@ async def create_product(product: ProductCreateRequest, current_user = Depends(g
     if user_data_service.permissions(current_user.id) > 0:
         # Create new product with provided data
         product_data_service.create(**product.model_dump(exclude_unset=True))
-        return JSONResponse({"message": SuccessResponse.PRODUCT_CREATE_SUCCESS, "product": product.model_dump()}, status_code=201)
+        return success_response({"message": SuccessResponse.PRODUCT_CREATE_SUCCESS, "product": product.model_dump()}, status_code=201)
     
     # Return 403 if user lacks permissions
-    return JSONResponse({"message": UserResponse.NOT_PERMITTED}, status_code=403)
+    return error_response({"message": UserResponse.NOT_PERMITTED}, status_code=403)
 
 @product_router.patch("/{product_id}/swap-status")
 async def switch_product_status(product_id: int, current_user = Depends(get_current_user)): 
@@ -165,13 +166,13 @@ async def switch_product_status(product_id: int, current_user = Depends(get_curr
         # Update only the status field of the specified product
         product = product_data_service.get_by_id(product_id)
         if not product:
-            return JSONResponse({"message": ErrorResponse.PRODDUCT_NOT_FOUND.format(product_id=product_id)}, status_code=404)
+            return error_response({"message": ErrorResponse.PRODDUCT_NOT_FOUND.format(product_id=product_id)}, status_code=404)
         status = 0 if product.status == 1 else 1
         product_data_service.update(product_id, status=status)
-        return JSONResponse({"message": SuccessResponse.PRODUCT_EDIT_SUCCESS})
+        return success_response({"message": SuccessResponse.PRODUCT_EDIT_SUCCESS})
     
     # Return 403 if user lacks permissions
-    return JSONResponse({"message": UserResponse.NOT_PERMITTED}, status_code=403)
+    return error_response({"message": UserResponse.NOT_PERMITTED}, status_code=403)
 
 # HIGH RISK OPERATIONS - Requires permissions == 2 (Admin level only)
 
@@ -197,10 +198,10 @@ async def delete_product(product_id: int, current_user = Depends(get_current_use
     if user_data_service.permissions(current_user.id) == 2:
         # Permanently delete the product
         product_data_service.delete(product_id)
-        return JSONResponse({"message": SuccessResponse.PRODUCT_DELETE_SUCCESS})
+        return success_response({"message": SuccessResponse.PRODUCT_DELETE_SUCCESS})
     
-    # Return 403 if user lacks admin permissions
-    return JSONResponse({"message": UserResponse.NOT_PERMITTED}, status_code=403)
+    # Return 403 if user lacks admin permissions    
+    return error_response({"message": UserResponse.NOT_PERMITTED}, status_code=403)
 
 @product_router.patch("/{product_id}/edit")
 async def edit_product(product_id: int, product: ProductEditRequest, current_user = Depends(get_current_user)):
@@ -226,10 +227,10 @@ async def edit_product(product_id: int, product: ProductEditRequest, current_use
         # Retrieve updated product to return in response
         edited_product = product_data_service.get_by_id(product_id)
         if not edited_product:
-            return JSONResponse({"message": UserResponse.USER_NOT_FOUND.format(user_id=product_id)}, status_code=404)
+            return error_response({"message": UserResponse.USER_NOT_FOUND.format(user_id=product_id)}, status_code=404)
         
         logger.info(f"Product {product_id} edited successfully")
-        return JSONResponse({"message": SuccessResponse.PRODUCT_EDIT_SUCCESS, "product": edited_product.model_dump()})
+        return success_response({"message": SuccessResponse.PRODUCT_EDIT_SUCCESS, "product": edited_product.model_dump()})
     
-    # Return 403 if user lacks permissions
-    return JSONResponse({"message": UserResponse.NOT_PERMITTED}, status_code=403)
+    # Return 403 if user lacks permissions    
+    return error_response({"message": UserResponse.NOT_PERMITTED}, status_code=403)

+ 4 - 12
routes/sales.py

@@ -6,6 +6,7 @@ from datetime import datetime, timedelta, time
 from services.data_service import DataServiceFactory
 from fastapi import APIRouter, Depends
 from models.sales import Sale
+from utils.responses import error_response, success_response
 
 
 sale_data_service = DataServiceFactory.get_sales_service()
@@ -18,10 +19,7 @@ sales_router = APIRouter()
 def get_user_sales(user_id: int):
     user = user_data_service.get_by_id(user_id)
     if not user:
-        return JSONResponse(
-            status_code=404,
-            content={"message": UserResponse.USER_NOT_FOUND}
-        )
+        return error_response(UserResponse.USER_NOT_FOUND.format(user_id=user_id), status_code=404)
     sales = sale_data_service.get_by_user(user_id)
     
     # solo las ventas del dia, estilo 12pm a 3am del dia siguiente
@@ -32,12 +30,6 @@ def get_user_sales(user_id: int):
     sales = [sale for sale in sales if start <= sale.date < end]
     
     if not sales:
-        return JSONResponse(status_code=200, content={
-            "sales": [],
-            "message": "No se encontraron ventas para el usuario."
-        })
+        return error_response(ErrorResponse.SALE_NOT_FOUND, status_code=404)
     logger.info(f"Sales found for user {user_id}: {len(sales)} sales")
-    return JSONResponse(
-        status_code=200,
-        content={"sales": [{**sale.model_dump(), "date": sale.date.isoformat()} for sale in sales], "message": "Ventas obtenidas correctamente."}
-    )
+    return success_response({"sales": [{**sale.model_dump(), "date": sale.date.isoformat()} for sale in sales], "message": "Ventas obtenidas correctamente."})

+ 10 - 2
routes/store.py

@@ -4,6 +4,8 @@ from models.user import User
 from pydantic import BaseModel
 from auth.security import get_current_user
 from config import settings
+from utils.responses import success_response
+from fudo.fudo import get_table
 
 
 
@@ -17,8 +19,14 @@ def set_store_state(state: AppStateBody, current_user: User = Depends(get_curren
     if (current_user.permissions or -1) >= 1:
         settings.IS_OPEN_STORE = state.state
     
-    return {"state": settings.IS_OPEN_STORE}
+    return success_response({"state": settings.IS_OPEN_STORE})
+
+@store_router.get("/tables/exists", response_class=JSONResponse)
+def get_table_exists(q: str = Query(..., description="q parameter")):
+    table = get_table(int(q))
+    exist = bool(table)
+    return success_response({"exists": exist}, status_code=200)
 
 @store_router.get("/state", response_class=JSONResponse)
 def get_store_state(_: User = Depends(get_current_user)):
-    return {"state": settings.IS_OPEN_STORE}
+    return success_response({"state": settings.IS_OPEN_STORE})

+ 49 - 54
routes/users.py

@@ -21,6 +21,7 @@ from services.data_service import BlacklistDataService, UserDataService
 from services.email_service import get_email_sender
 from services.print_service import print_ticket
 import services.recovery_service as recovery_service
+from utils.responses import error_response, success_response
 from utils.rut import validate_rut
 
 fernet = Fernet(PIN_KEY.encode())
@@ -31,20 +32,14 @@ user_router = APIRouter()
 
 redis_client = redis.Redis(host='localhost', port=6379, db=1 if DEVELOPMENT else 0, decode_responses=True)
 
-def unique_pin_generate():
-    """Generate a unique 4-digit PIN"""
-    import random
-    pin = str(random.randint(1000, 9999))
-    return pin
-
 @user_router.post("/exists")
 async def exists_user(request: UserIDRequest):
         """Check if user exists"""
         user = user_data_service.get_by_id(request.id)
         if user:
-            return JSONResponse(status_code=200, content={"exists": True, "message": UserResponse.USER_EXISTS})
+            return success_response({"exists": True, "message": UserResponse.USER_EXISTS})
         else:
-            return JSONResponse(status_code=404, content={"exists": False, "message": UserResponse.USER_DOES_NOT_EXIST})
+            return error_response({"exists": False, "message": UserResponse.USER_DOES_NOT_EXIST})
 
 @user_router.post("/register")
 async def register_user(request: RegisterUserRequest):
@@ -57,7 +52,7 @@ async def register_user(request: RegisterUserRequest):
     if not validate_rut(request.rut):
         logger.warning(f"Registration failed for {request.email}: invalid RUT {request.rut}")
         
-        raise HTTPException(status_code=400, detail=ErrorResponse.INVALID_RUT)
+        return error_response({"message": ErrorResponse.INVALID_RUT})
 
     # Check if user already exists by email
     try:
@@ -65,14 +60,14 @@ async def register_user(request: RegisterUserRequest):
         if user:
             logger.warning(f"Registration failed for {request.email}: user already exists")
             
-            return HTTPException(status_code=400, detail=UserResponse.USER_ALREADY_EXISTS)
+            return error_response({"message": UserResponse.USER_ALREADY_EXISTS})
             
         # Check if RUT already exists
         user = user_data_service.get_by_rut(request.rut)
         if user:
             logger.warning(f"Registration failed for {request.email}: RUT already exists")
             
-            return HTTPException(status_code=400, detail=UserResponse.USER_ALREADY_EXISTS)
+            return error_response({"message": UserResponse.USER_ALREADY_EXISTS})
 
     except Exception as e:
         error_msg = f"Database error during user validation: {e}"
@@ -106,20 +101,20 @@ async def register_user(request: RegisterUserRequest):
         )
         
         
-        return JSONResponse(status_code=201, content={"message": SuccessResponse.USER_CREATED_SUCCESS})
+        return success_response({"message": SuccessResponse.USER_CREATED_SUCCESS}, status_code=201)
         
     except Exception as e:
         error_msg = f"Error during registration process for {request.email}: {e}"
         logger.error(error_msg)
         
-        return JSONResponse(status_code=500, content={"message": "Error interno del servidor"})
+        return error_response({"message": f"Error interno del servidor: {e}"}, status_code=500)
 
 @user_router.post("/create-user")
 async def create_user(request: PinUserRequest, q: str):
     """Create a new user with PIN"""
     data = redis_client.get(f"verify:{q}")
     if not redis_client.get(f"verify:{q}"):
-        return JSONResponse(status_code=400, content={"message": ErrorResponse.INVALID_VERIFICATION_CODE})
+        return error_response({"message": ErrorResponse.INVALID_VERIFICATION_CODE})
     else:
         data = json.loads(str(data))
     name = data.get("name")
@@ -127,17 +122,17 @@ async def create_user(request: PinUserRequest, q: str):
     rut = data.get("rut")
     pin = request.pin
     if not request.pin or len(request.pin) != 4:
-        return JSONResponse(status_code=400, content={"message": ErrorResponse.INVALID_PIN})
+        return error_response({"message": ErrorResponse.INVALID_PIN})
     userID = user_data_service.create(name, email, rut, pin)
     if userID == -1:
-        return JSONResponse(status_code=400, content={"message": UserResponse.USER_ALREADY_EXISTS})
+        return error_response({"message": UserResponse.USER_ALREADY_EXISTS})
     user = user_data_service.get_by_id(userID)
     if not user:
         logger.error(f"User creation failed for {email}: user not found after creation")
-        return JSONResponse(status_code=500, content={"message": ErrorResponse.USER_CREATION_ERROR})
+        return error_response({"message": ErrorResponse.USER_CREATION_ERROR})
 
     logger.info(f"User created successfully: {email}")
-    return JSONResponse(status_code=201, content={"message": SuccessResponse.USER_CREATED_SUCCESS, "data": {
+    return success_response({"message": SuccessResponse.USER_CREATED_SUCCESS, "data": {
         **user.model_dump(exclude={"pin_hash"}),
         "token": generate_token(user.email)
     }})
@@ -148,21 +143,21 @@ async def force_register_user(request: ForceRegisterUserRequest, current_user: U
     """Force register a new user"""
     logger.info(f"Force register attempt for email: {request.email}")
     if (current_user.permissions or -1) >= 1:
-        return JSONResponse(status_code=403, content={"message": UserResponse.NOT_PERMITTED})
+        return error_response({"message": UserResponse.NOT_PERMITTED})
     
     
     if not request.pin or len(request.pin) != 4:
-        return JSONResponse(status_code=400, content={"message": ErrorResponse.INVALID_PIN})
+        return error_response({"message": ErrorResponse.INVALID_PIN})
     userID = user_data_service.create(request.name, request.email, request.rut, request.pin)
     if userID == -1:
-        return JSONResponse(status_code=400, content={"message": UserResponse.USER_ALREADY_EXISTS})
+        return error_response({"message": UserResponse.USER_ALREADY_EXISTS})
     user = user_data_service.get_by_id(userID)
     if not user:
         logger.error(f"User creation failed for {request.email}: user not found after creation")
-        return JSONResponse(status_code=500, content={"message": ErrorResponse.USER_CREATION_ERROR})
+        return error_response({"message": ErrorResponse.USER_CREATION_ERROR})
 
     logger.info(f"User created successfully: {request.email}")
-    return JSONResponse(status_code=201, content={"message": SuccessResponse.USER_CREATED_SUCCESS, "data": {
+    return success_response({"message": SuccessResponse.USER_CREATED_SUCCESS, "data": {
         **user.model_dump(exclude={"pin_hash"}),
         "token": generate_token(user.email)
     }})
@@ -186,9 +181,9 @@ async def login_user(request: LoginRequest, http_request: Request):
             
             logger.warning(f"Login attempt for blocked user: {request.email}, blocked for {blocked_minutes} minutes")
             
-            return JSONResponse(
-                status_code=403, 
-                content={"message": UserResponse.USER_FORMAT_BLOCKED.format(time=f"{blocked_minutes} minutos")}
+            return error_response(
+                {"message": UserResponse.USER_FORMAT_BLOCKED.format(time=f"{blocked_minutes} minutos")},
+                status_code=403
             )
 
         # Attempt login
@@ -198,9 +193,9 @@ async def login_user(request: LoginRequest, http_request: Request):
             if blacklist_data_service.is_user_blacklisted(user.id):
                 logger.warning(f"Login attempt for blacklisted user: {request.email}")
                 
-                return JSONResponse(
-                    status_code=403,
-                    content={"message": UserResponse.USER_BLACKLISTED}
+                return error_response(
+                    {"message": UserResponse.USER_BLACKLISTED},
+                    status_code=403
                 )
 
             # Successful login
@@ -214,14 +209,14 @@ async def login_user(request: LoginRequest, http_request: Request):
                 if user_permissions == 0:
                     logger.warning(f"Unauthorized admin access attempt by {request.email}")
                     
-                    return JSONResponse(status_code=403, content={"message": UserResponse.NOT_PERMITTED})
+                    return error_response({"message": UserResponse.NOT_PERMITTED}, status_code=403)
 
             # Clear login attempts and log successful login
             redis_client.delete(f"login_attempts:{request.email}")
             
             
             
-            return JSONResponse(status_code=200, content={
+            return success_response({
                 "message": SuccessResponse.LOGIN_SUCCESS, 
                 "data": {
                     "id": user.id,
@@ -248,50 +243,50 @@ async def login_user(request: LoginRequest, http_request: Request):
                 
                 logger.warning(f"Too many login attempts for {request.email}. User blocked.")
                 
-                return JSONResponse(status_code=429, content={"message": ErrorResponse.TOO_MANY_ATTEMPTS})
+                return error_response({"message": ErrorResponse.TOO_MANY_ATTEMPTS}, status_code=429)
             else:
                 logger.warning(f"Failed login attempt for {request.email}. Attempts: {attempts}")
                 
             
             # Return unauthorized with attempts remaining
-            return JSONResponse(status_code=401, content={
+            return error_response({
                 "message": ErrorResponse.INVALID_CREDENTIALS, 
                 "attempts_remaining": 5 - attempts if attempts else 5
-            })
+            }, status_code=401)
             
     except redis.RedisError as e:
         error_msg = f"Redis error during login for {request.email}: {e}"
         logger.error(error_msg)
         
-        return JSONResponse(status_code=500, content={"message": "Error interno del servidor"})
+        return error_response({"message": "Error interno del servidor"}, status_code=500)
         
     except Exception as e:
         error_msg = f"Unexpected error during login for {request.email}: {e}"
         logger.error(error_msg)
         
-        return JSONResponse(status_code=500, content={"message": "Error interno del servidor"})
+        return error_response({"message": "Error interno del servidor"}, status_code=500)
 
 @user_router.delete("/delete")
 async def delete_user(request: UserIDRequest, current_user: User = Depends(get_current_user)):
     if current_user.permissions != 2:
-        return JSONResponse(status_code=403, content={"message": UserResponse.NOT_PERMITTED})
+        return error_response({"message": UserResponse.NOT_PERMITTED}, status_code=403)
     """Delete a user by ID"""
     user = user_data_service.delete(request.id)
     if user:
-        return JSONResponse(status_code=200, content={"message": SuccessResponse.USER_DELETED_SUCCESS, "data": user})
+        return success_response({"message": SuccessResponse.USER_DELETED_SUCCESS, "data": user})
     else:
-        return JSONResponse(status_code=404, content={"message": UserResponse.USER_NOT_FOUND})
+        return error_response({"message": UserResponse.USER_NOT_FOUND}, status_code=404)
 
 @user_router.post("/pin-recovery")
 async def change_pin(request: PinRecoveryRequest):
     """Change a user's PIN"""
     user = user_data_service.get_by_email(request.email)
     if not user:
-        return JSONResponse(status_code=404, content={"message": UserResponse.USER_NOT_FOUND.format(user_id=request.email)})
+        return error_response({"message": UserResponse.USER_NOT_FOUND.format(user_id=request.email)}, status_code=404)
 
     real_token = recovery_service.get_token(user.id)
     if real_token and real_token != request.token:
-        return JSONResponse(status_code=400, content={"message": "Invalid token"})
+        return error_response({"message": "Invalid token"})
     logger.info(f"Pin change, to {request.new_pin} for user {user.email}")
     user_data_service.update(user_id=user.id, pin_hash=request.new_pin)
     sender = get_email_sender()
@@ -301,19 +296,19 @@ async def change_pin(request: PinRecoveryRequest):
         body=PIN_SUCCESSFULLY["body"].format(app_name=APPNAME, date=datetime.now().strftime("%Y-%m-%d"), time=datetime.now().strftime("%H:%M:%S"), name=user.name)
     )
 
-    return JSONResponse(status_code=200, content={"message": "Recovery email sent"})
+    return success_response({"message": "Recovery email sent"})
 
 @user_router.post("/reward")
 async def reward_user(request: UserRewardRequest, user: User = Depends(get_current_user)):
     """Reward a user with 1 free beer"""
     if user.reward_progress < 100:
-        return JSONResponse(status_code=400, content={"message": UserResponse.REWARD_INSUFFICIENT_PROGRESS.format(progress=user.reward_progress)})
+        return error_response({"message": UserResponse.REWARD_INSUFFICIENT_PROGRESS.format(progress=user.reward_progress)})
     if not user:
-        return JSONResponse(status_code=404, content={"message": UserResponse.USER_NOT_FOUND.format(user_id=request.id)})
+        return error_response({"message": UserResponse.USER_NOT_FOUND.format(user_id=request.id)}, status_code=404)
     
     user_data_service.set_reward_progress(user.id, 0)
     print_ticket(request.tableNumber)
-    return JSONResponse(status_code=200, content={"message": SuccessResponse.REWARD_SUCCESS, "data": {
+    return success_response({"message": SuccessResponse.REWARD_SUCCESS, "data": {
         "id": user.id,
         "name": user.name,
         "email": user.email,
@@ -323,19 +318,19 @@ async def reward_user(request: UserRewardRequest, user: User = Depends(get_curre
 @user_router.get("/user")
 async def get_cur_user(current_user:User = Depends(get_current_user)):
     """Get current user information"""
-    return JSONResponse(status_code=200, content={"data": current_user.model_dump(exclude={"pin_hash", "kleincoins", "rut"})})
+    return success_response({"data": current_user.model_dump(exclude={"pin_hash", "kleincoins", "rut"})})
 
 @user_router.get("/all")
 async def get_all_users():
     """Get all users"""
     users = list(map(lambda u: u.model_dump(), user_data_service.get_all()))
-    return JSONResponse(status_code=200, content={"data": users})
+    return success_response({"data": users})
 
 @user_router.get("/next")
 async def get_next_user_id():
     """Get the next user ID"""
     next_id = user_data_service.get_next_id()
-    return JSONResponse(status_code=200, content={"next_id": next_id})
+    return success_response({"next_id": next_id})
 from fastapi import Query
 
 verify_router = APIRouter()
@@ -370,7 +365,7 @@ async def pin_forgot_post(request: UserMail):
 
     user = user_data_service.get_by_email(request.email)
     if not user:
-        return JSONResponse(status_code=404, content={"message": UserResponse.USER_NOT_FOUND.format(user_id=request.email)})
+        return error_response({"message": UserResponse.USER_NOT_FOUND.format(user_id=request.email)}, status_code=404)
 
     recovery_key = recovery_service.generate_recovery_key(user.id)
     sender = get_email_sender()
@@ -380,20 +375,20 @@ async def pin_forgot_post(request: UserMail):
         body=PIN_RECOVERY_MAIL["body"].format(app_name=APPNAME, verification_code=recovery_key,name=user.name)
     )
     # Send recovery_key to user's email
-    return JSONResponse(status_code=200, content={"message": SuccessResponse.RECOVERY_EMAIL_SENT})
+    return success_response({"message": SuccessResponse.RECOVERY_EMAIL_SENT})
 
 @recovery_pin_router.post("/validate")
 async def pin_forgot_validate(request: PinRecoveryValidateRequest):
     """Validate the PIN recovery code"""    
     user = user_data_service.get_by_email(request.email)
     if not user:
-        return JSONResponse(status_code=404, content={"message": UserResponse.USER_NOT_FOUND.format(user_id=request.email)})
+        return error_response({"message": UserResponse.USER_NOT_FOUND.format(user_id=request.email)}, status_code=404)
     recovery_data = recovery_service.get_recovery_data(user.id)
     logger.info(f"Recovery data for {request.email}: {recovery_data}|{request.code}")
     if recovery_data.code == -1:
-        return JSONResponse(status_code=404, content={"message": UserResponse.USER_NOT_FOUND.format(user_id=request.email)})
+        return error_response({"message": UserResponse.USER_NOT_FOUND.format(user_id=request.email)}, status_code=404)
     if recovery_data.code != request.code:
-        return JSONResponse(status_code=400, content={"message": "Invalid recovery code"})
+        return error_response({"message": "Invalid recovery code"})
     token = uuid4().hex
     recovery_service.add_token(user.id, token)
-    return JSONResponse(status_code=200, content={"message": "Recovery code validated successfully", "token": token})
+    return success_response({"message": "Recovery code validated successfully", "token": token})

+ 29 - 15
services/openai_service/openai_service.py

@@ -34,21 +34,35 @@ async def generate_completion(messages_array: List[dict], user: User) -> str:
 
 
     preprompt = f"""
-Eres IAKlein, el asistente oficial del bar Klein 🍻.
-Hablas en estilo de chat corto (como en mensajería o IRC), usando emojis y mucho carisma.
-Tu rol:
-- Responder preguntas sobre el menú del bar Klein con la info de {data_string}.
-- Hacer bromas y charlar, pero siempre llevás la conversación de vuelta al bar Klein.
-- Dar recomendaciones de comidas y tragos.
-- Aceptar feedback y mandarlo con la herramienta 'feedback'.
-Reglas:
-- No tomás pedidos, solo informás.
-- Si te preguntan tu nombre, siempre respondés "Soy IAKlein".
-- Aunque uses el chat como contexto, siempre priorizás al último que diga @IAKlein, usando el chat como contexto de ser necesario.
-- No respondas preguntas que ya fueron respondidas en la conversación, ya si fue por ti o por otro usuario a no ser que te insistan.
-- No resolvés tareas, cálculos, ni programación: sos un amigo simpático del bar, no un esclavo LLM 🕺.
-Estilo:
-- Todo divertido, breve, con buena onda 😉.
+¡Hola! Eres IAKlein, el asistente oficial del bar Klein 🍻.
+
+Eres como ese amigo amigable que siempre está en la barra, listo para ayudar o tener una buena charla. Te comunicas en un estilo de chat corto (como mensajería o IRC), usando emojis y mucho carisma 😎.
+
+Tu Rol:
+
+Ser la guía experta del Menú: Conoces el menú . No solo "respondes", sino que inspiras. "Esa hamburguesa es increíble... 🤤"
+
+El/La Amigo/a del Bar: Charlas, haces bromas y mantienes un buen ambiente. Si la conversación se desvía, ¡no hay problema! Tu pasión es el Klein, así que naturalmente vuelves al tema, pero sin presionar 🍺. Eres un anfitrión, no un vendedor.
+
+El/La Recomendador/a Ideal: ¿Alguien no sabe qué pedir? ¡Ahí apareces tú! Preguntas qué les gusta y les ayudas a encontrar la opción perfecta.
+
+El Buzón de Sugerencias (amigable): Si alguien tiene feedback, lo recibes muy bien y lo envías con la herramienta 'feedback'.
+
+Tus Reglas Principales:
+
+Informas, no tomas pedidos: "Te cuento todo sobre el menú, pero para pedir tienes que llamar al personal 😉".
+
+Tu Identidad: Si te preguntan tu nombre... "¡Soy IAKlein! El corazón digital del Klein."
+
+Prioridad @IAKlein: Atiendes al último usuario que te mencione (@IAKlein), usando el chat anterior como contexto si es necesario.
+
+Evita Repetir: Si algo ya se respondió (por ti u otro), no intervienes. ¡A menos que insistan mucho!
+
+Relax, es un Bar: No estás para resolver tareas, ni programar, ni hacer cálculos. Eres un compañero para pasar un buen rato, no un asistente genérico 🤖... ¡eres el espíritu del bar! 🕺
+
+usa la siguiente informacion es toda tu memoria previa: 
+
+{data_string}
 """
 
 

+ 1 - 1
services/print_service.py

@@ -129,7 +129,7 @@ def get_status(location: Locations):
         logger.info(f"Printer service status: {'online' if status else 'offline'}")
         
         
-        return not not status  # Ensure a boolean is returned
+        return bool(status)
         
     except requests.RequestException as e:
         error_msg = f"Error connecting to printer service: {e}"

+ 0 - 10
users.json

@@ -1,10 +0,0 @@
-[
-    {
-        "userCode": "camilo1900",
-        "userName": "Camilo Klein"
-    },
-    {
-        "userCode": "superti1A",
-        "userName": "No Sacar(pruebas)"
-    }
-]

+ 23 - 0
utils/responses.py

@@ -0,0 +1,23 @@
+from typing import Union
+
+from fastapi.responses import JSONResponse
+
+def success_response(data: dict, success: bool = True, status_code: int = 200):
+    return JSONResponse(status_code=status_code, content={
+        "success": success,
+        "error": None,
+        "data": data
+    })
+
+def error_response(error: Union[str, Exception, dict], status_code: int = 500):
+    if isinstance(error, dict):
+        return JSONResponse(status_code=status_code, content={
+            "success": False,
+            "error": error,
+            "data": None
+        })
+    return JSONResponse(status_code=status_code, content={
+            "success": False,
+            "error": str(error),
+            "data": None
+        })

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików