migracion_fudo_toteat.md 11 KB

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_tableget_active_saleget_saleThreadPoolExecutor (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:

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

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=<real> 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).