# TomaNota: Un asistente que toma los pedidos por WhatsApp sin poder inventar un precio.

> Atiende el WhatsApp del comercio, arma el pedido con la carta real y lo deja en el mismo tablero que la carta con QR y el mostrador. La IA interpreta; los totales los calcula el servidor.

Rubro: Producto propio para comercios · Estado: En producción · Desarrollado por Ricardo Brossard
URL: https://ricardobrossard.com/proyectos/tomanota/

- tomanota.lat: https://tomanota.lat
- Probar el asistente: https://app.tomanota.lat/probar

## En números

- **+900** pruebas automáticas
- **1.203** conversaciones simuladas contra el modelo real
- **8** herramientas para el modelo, ninguna acepta un precio
- **46** tablas con permisos por comercio (RLS)

## El problema

La pizzería, la rotisería o la carnicería de barrio vende por WhatsApp. El cliente escribe como habla ("dos grandes y una mitad muzza mitad napo"), pregunta por el envío y espera que alguien le confirme. Ese alguien es el dueño, que al mismo tiempo atiende el mostrador.

En la hora pico los mensajes se acumulan, los pedidos se anotan en papel con errores en las mitades y en los totales, y a fin de mes nadie sabe cuánto entró por ese canal. Las apps de delivery cobran comisión por clientes que ya son del comercio, y un bot de respuestas armadas no entiende cómo escribe la gente.

## Qué construí

**TomaNota**, un producto propio con tres puertas de entrada que terminan en un mismo tablero:

- **Un asistente de WhatsApp** que conversa en lenguaje natural, arma el pedido con la carta real del comercio, muestra un resumen y espera que la persona toque Confirmar. Si el cliente pide hablar con alguien, o el dueño escribe desde su teléfono, se hace a un lado.
- **Una carta online con QR**, para el cliente que prefiere elegir sin escribir.
- **Pedidos de mostrador**, para lo que entra en persona o por teléfono.
- **La pantalla del comercio**: el tablero en tiempo real con cada estado, el catálogo, las zonas de envío, los textos del asistente y un informe del mes por canal.

Por dentro son tres piezas. Un **motor de conversación** en TypeScript sin red ni disco, que corre igual en producción y en los tests. Un **servidor en Fastify** que lo conecta con WhatsApp, con Redis y con el modelo, a través de **Vercel AI SDK sobre una API compatible con OpenAI**. Y **PostgreSQL en Supabase**, con seguridad por fila en las 46 tablas: cada comercio ve solo lo suyo, y el servidor entra con un rol que no puede saltear esos permisos.

Antes de que intervenga la IA, cada mensaje pasa por **controles fijos**: un turno por chat a la vez (dos globos seguidos suman las dos cosas al carrito), descarte de reintentos del webhook, silencio cuando escribe el dueño y un corte a las 60 respuestas por hora que frena a dos bots contestándose en loop.

## Lo más difícil

### Que el modelo no pueda inventar un monto

Si el asistente dice un precio distinto al de la carta, el comercio pierde plata o discute con un cliente que tiene la captura. Lo resolví por diseño: **el modelo no tiene ningún camino para escribir un precio en el pedido.**

Trabaja con **8 herramientas cerradas** (agregar, mitad y mitad, extra, cambiar cantidad, quitar, envío, mostrar el carrito y avisar que está listo) y solo emite **ids de catálogo y cantidades**. Un producto agotado ni figura entre los ids válidos. El servidor ejecuta cada herramienta, calcula y escribe el resumen: el texto que la persona confirma nunca lo redacta el modelo.

Al confirmar, una única función de la base vuelve a cotizar todo con los precios de ese momento, con una clave de idempotencia para que un reintento no duplique el pedido. Bot, carta y mostrador pasan por esa misma función.

La media pizza lo puso a prueba: en una carta real, la media no es la mitad de la entera. Encontré cuatro lugares que dividían por dos cuando faltaba el dato, incluida la regla que medía al asistente. Hoy la media tiene precio propio y, si no está cargado, ese producto se vende solo entero.

### La confirmación es de la persona

La herramienta que avisa "listo" no confirma nada: le pide al sistema que muestre el resumen con Confirmar, Modificar y Cancelar. **Ningún pedido existe hasta que la persona toca Confirmar.** Y como el modelo puede escribir "pedido confirmado" por su cuenta, una regla cruza cada frase de cierre contra los pedidos realmente guardados en ese turno.

## Cómo se prueba

Cinco capas, cada una para un tipo de error distinto:

- **Más de 900 pruebas automáticas**: 847 en la suite rápida y 88 de integración contra PostgreSQL real, con el rol del servidor. Una de ellas recorre todas las funciones de la base y falla si alguna queda ejecutable sin permiso explícito.
- **13 conversaciones completas guardadas como especificación**, en cada cambio.
- **21 suites SQL** que prueban la base desde adentro: el aislamiento entre comercios y que las tres entradas cobren lo mismo.
- **Un banco de pruebas contra el modelo real**, con 77 casos y 11 reglas que revisan cada respuesta: no inventa precios, no promete medios de pago que no hay, no anuncia un pedido sin confirmar. Suma 1.203 conversaciones simuladas, difíciles a propósito: inyección de prompt, descuentos que no existen, una mitad pizza mitad empanada.
- **GitHub Actions** en cada push: escaneo de secretos sobre toda la historia, chequeo de tipos y la suite rápida.

## Stack

- Aplicación: React 18, Vite, TypeScript, Tailwind CSS, TanStack Query, PWA, Leaflet
- Servidor e IA: Node.js 22, Fastify, Vercel AI SDK, API compatible con OpenAI, Tool calling, WhatsApp Cloud API, Redis
- Datos: Supabase, PostgreSQL, Row Level Security, Realtime, Funciones plpgsql
- Calidad y despliegue: Vitest, Suites SQL, GitHub Actions, gitleaks, Docker Compose, Vercel
