# Auditoría Fudo → Toteat e Impresoras Fecha del informe: 2026-05-05 Repositorio: `pedidos_express_server` (FastAPI, "Biergarten Klein") Este documento contiene: 1. Auditoría completa del uso de la API de Fudo en el código actual. 2. Descripción del sistema de impresoras y su acoplamiento con Fudo. 3. Notas operativas para migrar a la API de Toteat usando el nuevo módulo `toteat/toteat.py`. --- ## 1. Auditoría del uso de la API de Fudo ### 1.1 Funciones definidas en `fudo/fudo.py` | Función | Tipo | Propósito | |---|---|---| | `get_token()` | sync | OAuth con `FUDO_API_KEY` / `FUDO_API_SECRET`, cachea token en Redis. | | `get_modifiers()` | sync | GET `/v1alpha1/product-modifiers`. **Definida pero nunca llamada.** | | `get_categories()` | sync | GET `/v1alpha1/product-categories`. | | `get_category_dict()` | sync | Diccionario `{id: nombre}` derivado de `get_categories`. | | `get_category(id)` | sync | GET de una categoría puntual. | | `get_product(id)` | sync | GET de un producto, devuelve `models.items.Product`. | | `get_products(page)` | sync | GET paginado de `/v1alpha1/products`. | | `get_all_indexed_products()` | async | Concurrencia con `aiohttp` (8 páginas en paralelo). Filtra `active && enableQrMenu`. | | `get_all_products()` | async | Wrapper que devuelve la lista. | | `_get_page_bounds(page, token)` | sync | Helper interno, retorna `(first, last, data)` de una página de mesas. | | `get_table(number)` | sync | Búsqueda exponencial + binaria sobre `/v1alpha1/tables` (Fudo no permite filtrar por número). | | `get_table_items(number)` | sync | Encadena `get_table` → `get_active_sale` → `get_sale` → `ThreadPoolExecutor` (max_workers=10) sobre los items. | | `get_sale(id)` | sync | GET `/v1alpha1/sales/{id}`. | | `create_sale(table_id)` | sync | POST `/v1alpha1/sales` con `people=1`, `saleType=EAT-IN`, `waiter.id=76`. | | `create_item(product_id, qty, sale_id, comment)` | sync | POST `/v1alpha1/items`, prefija comentario con "Pedido desde pedidos express". | | `get_active_sale(table)` | sync | Lee `relationships.activeSales.data[0]`. | | `clear_token()` | sync | Borra el token cacheado en Redis. | ### 1.2 Uso por archivo #### `routes/orders.py` | Línea | Llamado | Propósito | |---|---|---| | 180 | `fudo.get_token()` | Validación defensiva de conectividad antes de procesar el pedido. | | 190 | `add_product_to_fudo(item.id, item.quantity, table)` | Loop por cada item del pedido (wrapper de `services/fudo_service.py`). | | 221 | `fudo.get_active_sale(fudo.get_table(table))` | Recupera el `fudo_id` para guardar la venta en la BD local. | #### `routes/products.py` | Línea | Llamado | Propósito | |---|---|---| | 250 | `get_products_by_table(table_number)` | Endpoint `GET /products/table/{table_number}`. | #### `routes/store.py` | Línea | Llamado | Propósito | |---|---|---| | 33 | `get_table(int(q))` | Endpoint `GET /store/tables/exists?q=N`. | #### `services/fudo_service.py` | Línea | Llamado | Propósito | |---|---|---| | 8 | `fd.get_table(table_number)` | Resuelve mesa antes de añadir item. | | 13 | `fd.get_active_sale(table)` | Si no existe, crea venta. | | 15 | `fd.create_sale(table['id'])` | Crea venta nueva en Fudo. | | 20 | `fd.create_item(product_id, quantity, sale_id, comment)` | Añade item. | | 29 | `fd.get_table_items(table_number)` | Lectura de items de mesa. | #### `services/data_service.py` | Línea | Llamado | Propósito | |---|---|---| | 10 | `from fudo.fudo import get_all_products, get_category_dict` | Imports. | | 563 | `await get_all_products()` | Carga del catálogo en `ProductDataService.get_all` (con cache local). | | 569 | `get_category_dict()` | Resuelve nombre de categoría a partir de `categoryId`. | #### `load_products.py` | Línea | Llamado | Propósito | |---|---|---| | 1 | `from fudo.fudo import get_all_products, get_product` | Imports (script). | #### `update_prices.py` | Línea | Llamado | Propósito | |---|---|---| | 4 | `from fudo.fudo import get_product, get_products, get_category, get_all_products` | Imports (script). | | 72 | `get_category(int(...))` | Resuelve nombre de categoría para insertar productos nuevos. | | 121 | `get_all_products()` (sync, sin `await` — bug del script) | Carga el catálogo para sincronizar precios. | ### 1.3 Resumen por función | Función | Frecuencia | Archivos que la usan | |---|---|---| | `get_token` | 1+ por request | `fudo.py`, `routes/orders.py:180` | | `get_table` | 3-5 por pedido | `services/fudo_service.py:8`, `routes/orders.py:221`, `routes/store.py:33` | | `get_active_sale` | 2-3 por pedido | `services/fudo_service.py:13`, `routes/orders.py:221` | | `create_sale` | 0-1 por mesa nueva | `services/fudo_service.py:15` | | `create_item` | N por pedido | `services/fudo_service.py:20` | | `get_table_items` | 1 por endpoint | `services/fudo_service.py:29`, `routes/products.py:250` | | `get_all_products` (async) | 1 al iniciar / refrescar cache | `services/data_service.py:563`, `load_products.py`, `update_prices.py` | | `get_category_dict` | 1 al iniciar | `services/data_service.py:569` | | `get_category` | N (script) | `update_prices.py:72` | | `get_products` | raro (script) | `update_prices.py` | | `get_product` | raro (script) | `load_products.py`, `update_prices.py` | | `get_modifiers` | nunca | — | ### 1.4 Transformaciones notables - `get_product` (`fudo.py:138-150`): mapea el JSON:API de Fudo a `models.items.Product`, con conversión `active: bool → status: 0/1`. - `get_all_indexed_products` (`fudo.py:217-219`): filtra `active && enableQrMenu` antes de indexar. - `create_item` (`fudo.py:452`): siempre prefija el comentario con "Pedido desde pedidos express". - `get_table_items` (`fudo.py:388-392`): proyecta cada línea a `{id: int, quantity: int}` y descarta el resto. - `update_prices.add_missing_products` (`update_prices.py:80`): construye la URL de imagen como `https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/{id}` (acoplamiento al bucket S3 de Fudo). --- ## 2. Sistema de impresoras ### 2.1 Arquitectura El servicio `services/print_service.py` **no habla con hardware local**: es un cliente HTTP a un servidor remoto que se encarga de la impresión física (probablemente ESC/POS o CUPS detrás de él). | Item | Valor | |---|---| | Endpoint base | `http://10.10.12.3:8000` (hardcoded en `print_service.py`) | | Auth | `Authorization: Bearer PRINTER123cerveza@` (hardcoded) | | Timeouts | 10s pedidos/tickets · **1000s facturación (anómalo)** | | Librerías Python | `requests` y `subprocess` (para `ping`). No usa `python-escpos` ni `pycups`. | ### 2.2 Endpoints del servidor de impresión | Función Python | HTTP | Disparado por | |---|---|---| | `print_order(Order)` | `POST /print` | `POST /api/orders/send` (`routes/orders.py:281`) | | `print_ticket(table_id)` | `GET /ticket/{table}` | `GET /api/products/free-beer/{table_id}` y `routes/users.py:313` | | `print_billing(OrderBilling)` | `GET /billing/{table}/{payment}` | `POST /api/orders/billing` | | `get_status()` | `GET /status` | No invocada en routes (solo manual). | ### 2.3 Validación de conectividad En `routes/orders.py:126-142`, antes de procesar el pedido: ```python printers = {"ServerPrincipal": "10.10.12.3"} for name, ip in printers.items(): response = subprocess.run( ["/usr/bin/ping", "-c", "1", "-W", "10", ip], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) ``` Si el ping falla: - HTTP 424 al cliente con `ErrorResponse.PRINTER_DISCONNECTED`. - Email a `erwinjacimino2003@gmail.com` y `mompyn@gmail.com` usando la plantilla `PRINTER_DISCONNECTED_MAIL` (`config/mails.py`). ### 2.4 Acoplamiento con Fudo 1. `kitchen_id` enviado al servidor de impresión sale de `Product.kitchen_id`, y este venía originalmente de `fudo.relationships.kitchen.data.id` (`fudo.py:146`). El módulo `toteat/toteat.py` mantiene este campo mapeándolo desde `categoryId` (Toteat no expone "kitchen" como Fudo). 2. Orden de operaciones en `printer_order`: ping → POST a Fudo (loop de items) → guardar venta en BD local con `fudo_id` (de Fudo) → POST `/print`. 3. Riesgo: si Fudo cae **después** del ping, todo el pedido falla con 500. Si la impresora se desconecta **después** de los `create_item` exitosos en Fudo, hay venta registrada en Fudo + BD pero sin comanda física. ### 2.5 Configuración No hay variables de entorno para impresoras. Toda la configuración (IP, token, timeouts) está hardcoded en `services/print_service.py`. Mover a `config/settings.py` es trabajo futuro. --- ## 3. Plan de migración usando `toteat/toteat.py` ### 3.1 Variables de entorno nuevas ```env TOTEAT_API_TOKEN=... TOTEAT_RESTAURANT_ID=... TOTEAT_LOCAL_ID=... TOTEAT_USER_ID=... ``` La URL base alterna entre `apidev.toteat.com` y `api.toteat.com` según la flag `DEVELOPMENT` ya existente. ### 3.2 Equivalencias El módulo `toteat/toteat.py` expone **exactamente** las mismas funciones públicas que `fudo/fudo.py`, devolviendo datos en la forma JSON:API estilo Fudo. Para activarlo: 1. En `services/fudo_service.py`: cambiar `import fudo.fudo as fd` por `import toteat.toteat as fd`. 2. En `services/data_service.py:10`: cambiar `from fudo.fudo import get_all_products, get_category_dict` por `from toteat.toteat import get_all_products, get_category_dict`. 3. En `routes/orders.py:13`: cambiar `from fudo import fudo` por `from toteat import toteat as fudo`. 4. En `routes/store.py:8`: cambiar `from fudo.fudo import get_table` por `from toteat.toteat import get_table`. 5. En `load_products.py` y `update_prices.py`: idem. `fudo/` queda intacto durante la transición; cuando la migración esté validada en producción se puede borrar. ### 3.3 Diferencias funcionales conocidas - **`get_modifiers`**: nunca se usa, solo está por compatibilidad. En Toteat los modificadores vienen embebidos en `/products`. - **`get_products(page)`**: Toteat no pagina. Retorna todo en `page=1` y lista vacía para `page > 1` (compatible con loops `while data`). - **`create_sale` + `create_item`**: el flujo `crear venta vacía + N items` de Fudo se preserva. En Toteat la primera llamada a `create_item` para una venta hace `POST /orders` con `orderId=0`; las siguientes hacen `POST /orders` con `orderId=` para añadir líneas a la orden de mesa existente. El `orderId` se cachea en Redis (`toteat:table_order:{N}`, TTL 8h) para que `get_active_sale` siga devolviendo el mismo ID después del loop. - **`kitchen_id`**: por defecto se mapea a `categoryId`. Si en el futuro hace falta enrutar items a impresoras distintas según otra dimensión, hay que agregar un mapping configurable en `_to_fudo_product`. - **Rate limits Toteat**: - `/products`: 3 req/min → cache local de 5 min. - `/tables`: 3 req/min → cache local de 1 min. - `/orders`: 1 req/seg → para pedidos largos puede agregar latencia. Si se vuelve cuello de botella, se puede refactorizar `routes/orders.py` para enviar todos los items en una sola llamada (el módulo ya soporta el payload `line=[...]`). ### 3.4 Auditoría rápida post-migración Antes de borrar `fudo/`: 1. Probar `python -m toteat.toteat` (suite de smoke tests embebida). 2. Validar un pedido end-to-end en ambiente dev (`apidev.toteat.com`). 3. Revisar logs por `Toteat GET ... falló` o `status=4xx`. 4. Confirmar que `fudo_id` en la tabla `sales` ahora guarda el `orderId` de Toteat (es un entero largo, no string corto).