# 10 · Pruebas y puesta en producción

## Probar en local, sin Meta

Todo el sistema se puede ejecutar en vuestra máquina con un **Meta simulado**: no envía mensajes reales ni necesita cuenta de Meta.

```bash
pnpm install && cp .env.example .env     # rellenar MASTER_KEY: openssl rand -base64 32
pnpm db:up && pnpm db:migrate
pnpm playground                           # http://localhost:4000 (app de ejemplo con chat incrustado)
```

La consola de administración está en http://localhost:3002/ (el token de administración de demo está en `.dev/playground.json`). El playground escribe credenciales de demo en `.dev/playground.json` (URL base, API keys, token de administración) y deja sembrados un tenant, una licencia, un número y plantillas aprobadas. Desde la app de ejemplo podéis **simular que un cliente escribe** y que **Meta confirma entregas**.

```bash
pnpm docs:verify     # ejecuta TODOS los ejemplos de docs/ejemplos contra el playground
pnpm test            # la suite completa (190+ pruebas, PostgreSQL real + Meta simulado)
```

Contra el playground podéis probar con curl, Postman o vuestro código:

```bash
export BASE_URL=http://localhost:3000
export API_KEY=$(jq -r .apiKey .dev/playground.json)
```

**Lo que no se puede probar en local:** el comportamiento real de Meta (aprobación de plantillas, entrega, calidad, pagos) y el flujo de Embedded Signup. Eso requiere una cuenta de Meta; ver «Antes de producción».

### Recibir eventos en local

Registrar un destino exige `https` y una dirección pública. Opciones:

1. Un túnel (`cloudflared tunnel --url http://localhost:5001`, ngrok…) y registrar su URL.
2. Probar solo vuestro verificador con el [vector de prueba](ejemplos/vectores/firma-evento.json) y con los [eventos de ejemplo](ejemplos/eventos/).
3. `POST /v1/webhook-subscriptions/{id}/test` os envía un evento `whatsapp.test` firmado.

## Lista de comprobación de la integración

- [ ] La API key está solo en el servidor y con **los alcances mínimos**.
- [ ] Cada `POST /v1/messages` lleva `Idempotency-Key` derivada de un hecho de negocio.
- [ ] Se **reintentan** `429`, `502` y `503` con retroceso y **la misma** `Idempotency-Key`; no se reintenta un `4xx`.
- [ ] Un `202` no se trata como «entregado»: se espera el evento `whatsapp.message.status`.
- [ ] El receptor **verifica la firma sobre el cuerpo crudo**, comprueba la marca de tiempo, **deduplica por `id`** y responde 2xx enseguida.
- [ ] Los estados no retroceden (`read` no vuelve a `delivered`).
- [ ] Se gestionan `window_closed` (enviar plantilla), `template_not_approved`, `payment_required`, `license_inactive` y `license_limit_reached` con mensajes útiles para el usuario, no con un error genérico.
- [ ] Un mensaje `failed` con `outcome_unknown` **no se reenvía a ciegas**.
- [ ] Se guarda `external_ref` en cada envío para reconciliar.
- [ ] El consentimiento se registra (`opt-in`) con origen y fecha, y se respeta la baja.
- [ ] Hay un plan de recuperación si el receptor estuvo caído ([05](05-recibir-eventos.md#6-plan-de-recuperación-si-estuvisteis-caídos)).
- [ ] La integración de las pantallas: orígenes registrados, mapeo de roles revisado, endpoint de token sin caché.
- [ ] Se loguea siempre el `request_id` de los errores.

## Antes de producción

### En Meta (una sola vez, por la plataforma)

1. **Business Portfolio de la empresa** con al menos dos administradores y verificación en dos pasos, y **verificación del negocio** completada.
2. Una **App de Meta** de tipo Business con el producto WhatsApp, y una por entorno (desarrollo, preproducción, producción).
3. **Webhook de la App**: URL `https://hooks.<dominio>/webhooks/meta`, token de verificación (`META_VERIFY_TOKEN`) y suscripción a los campos `messages`, `message_template_status_update`, `message_template_quality_update`, `message_template_components_update`, `template_category_update`, `phone_number_name_update`, `phone_number_quality_update`, `account_update`.
4. **Configuración de Facebook Login for Business** con los productos de WhatsApp → `META_CONFIG_ID`.
5. **App Review**: acceso avanzado a `whatsapp_business_management` y `whatsapp_business_messaging`, con una grabación y una descripción **por permiso** (enviar un mensaje desde el chat; crear una plantilla desde la pantalla de plantillas). Mientras no esté aprobada, solo funcionan los usuarios con rol en la App.
6. Textos legales (política de privacidad, condiciones, instrucciones de borrado de datos) en la App.

### En la plataforma

| Elemento | Qué hacer |
| --- | --- |
| Dominios | `wa.` (API, UI y embebido), `hooks.` (recepción de Meta) y `admin.` (administración), con TLS y, a ser posible, protegidos por separado |
| `MASTER_KEY` | 32 bytes en base64, desde un gestor de secretos. **Si se pierde, se pierden todos los tokens de los clientes** (habría que repetir todas las altas): guardad copia segura |
| `EMBED_PRIVATE_KEY` | Clave Ed25519 en PEM. Sin ella se usa una efímera y las sesiones se pierden al reiniciar |
| `META_APP_ID`, `META_APP_SECRET`, `META_CONFIG_ID`, `META_VERIFY_TOKEN` | De la App de Meta del entorno |
| `ADMIN_TOKEN` | Sustituir por SSO corporativo con segundo factor y lista de IPs |
| Base de datos | Ejecutar migraciones con el rol dueño; la aplicación usa roles distintos (`gateway_app` sin `BYPASSRLS`, `gateway_ingest` solo inserta). Copias de seguridad cifradas con restauración probada |
| `ALLOW_MANUAL_CONNECT` | `false` en producción |
| `ALLOW_PRIVATE_WEBHOOK_URLS` | `false` en producción |
| Procesos | `api`, `ingest`, `admin` y `worker` (varios workers son seguros: se reparten el trabajo) |
| Vigilancia | `GET /admin/v1/health`: cola de mensajes, eventos de Meta fallidos, entregas `dead`, cuentas con pago bloqueado y números con problemas |

### Limitaciones conocidas hoy (ver README del repositorio)

- Los límites de peticiones y de envío por número viven **en memoria de cada instancia**: con varias réplicas de la API el límite efectivo se multiplica. Planificado: Redis.
- El chat sondea cada 4 s (sin WebSocket todavía).
- La caché de licencia dura 5 s entre procesos distintos.
- Sin exportación de un tenant, envíos masivos, plantillas con cabecera multimedia ni antivirus de archivos.
- **No se ha probado contra un Meta real.** Haced el piloto con un cliente y una cuenta de pruebas antes de abrirlo al resto: nombres de campo de la API de Meta, aprobación real de plantillas, Embedded Signup y facturación.

## Versionado y cambios

`/v1` solo recibe cambios compatibles (campos nuevos opcionales, valores nuevos en enumeraciones). **Programad vuestros clientes para tolerar campos y eventos desconocidos.** Una retirada se anuncia con las cabeceras `Deprecation` y `Sunset` al menos 6 meses antes.
