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