# Integración de Webhooks — Endpoint interno (sistema legacy)

Guía para que el **sistema legacy** emita eventos de webhook reutilizando el mismo
pipeline de entrega que el resto de la plataforma, garantizando consistencia entre
sistemas.

El legacy solo necesita enviar el **nombre del evento** y el **id de la entidad
relacionada**; el backend carga la entidad, arma el payload, lo firma y lo entrega
de forma asíncrona.

---

## Datos de conexión

| Campo | Valor |
|-------|-------|
| **Método** | `POST` |
| **URL** | `https://TU_DOMINIO/internal/webhook/emit` |
| **Header obligatorio** | `X-Internal-Token: <token secreto>` |
| **Content-Type** | `application/json` |

> El token lo provee el equipo de backend (variable `INTERNAL_WEBHOOK_TOKEN`).
> **No** se genera un token por compañía: un único token sirve para todas. La
> compañía se identifica con el campo `companyId` del body.

---

## Estructura del body

```json
{
  "companyId": 13,
  "event": "shipment.status_changed",
  "entityId": 8840,
  "context": { "status": "DELIVERED" }
}
```

| Campo | Tipo | Obligatorio | Notas |
|-------|------|-------------|-------|
| `companyId` | int | Sí | Id de la compañía |
| `event` | string | Sí | Clave del catálogo (ver abajo) |
| `entityId` | int | Sí | Id de la entidad relacionada al evento |
| `context` | object | Solo en `*.status_changed` | Requiere `status` (string no vacío). Otros campos son libres (p. ej. `comment`) |

### `entityId` según el tipo de evento

| Prefijo del evento | `entityId` es el id de… |
|--------------------|--------------------------|
| `warehouse.*` | WHrec (recepción de almacén) |
| `shipment.*` | Guía |
| `package.*` | Receipt (paquete) |
| `pobox.*` | Casillero (POBOX) |
| `customer.*` | Cliente |
| `consolidated.*` | Consolidado |

---

## Catálogo de eventos

| Entidad | Eventos disponibles |
|---------|---------------------|
| `warehouse` | `warehouse.created`, `warehouse.updated`, `warehouse.deleted`, `warehouse.status_changed` |
| `shipment` | `shipment.created`, `shipment.updated`, `shipment.deleted`, `shipment.status_changed` |
| `package` | `package.created`, `package.updated`, `package.deleted`, `package.status_changed` |
| `pobox` | `pobox.created`, `pobox.updated`, `pobox.deleted` |
| `customer` | `customer.created`, `customer.updated`, `customer.deleted` |
| `consolidated` | `consolidated.status_changed` |

> Los eventos terminados en `.status_changed` **requieren** `context.status`.

---

## Ejemplos (curl)

> Reemplaza `$TOKEN`, `companyId` y `entityId` por valores reales.

### Warehouse (recepción de almacén)

```bash
# warehouse.created
curl -X POST https://TU_DOMINIO/internal/webhook/emit \
  -H "X-Internal-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{"companyId":13,"event":"warehouse.created","entityId":2451}'

# warehouse.updated
curl -X POST https://TU_DOMINIO/internal/webhook/emit \
  -H "X-Internal-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{"companyId":13,"event":"warehouse.updated","entityId":2451}'

# warehouse.deleted
curl -X POST https://TU_DOMINIO/internal/webhook/emit \
  -H "X-Internal-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{"companyId":13,"event":"warehouse.deleted","entityId":2451}'

# warehouse.status_changed  (REQUIERE context.status)
curl -X POST https://TU_DOMINIO/internal/webhook/emit \
  -H "X-Internal-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{"companyId":13,"event":"warehouse.status_changed","entityId":2451,
       "context":{"status":"RECIBIDO","comment":"Llegó a bodega"}}'
```

### Shipment (guía)

```bash
# shipment.created
curl -X POST https://TU_DOMINIO/internal/webhook/emit \
  -H "X-Internal-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{"companyId":13,"event":"shipment.created","entityId":8840}'

# shipment.updated
curl -X POST https://TU_DOMINIO/internal/webhook/emit \
  -H "X-Internal-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{"companyId":13,"event":"shipment.updated","entityId":8840}'

# shipment.deleted
curl -X POST https://TU_DOMINIO/internal/webhook/emit \
  -H "X-Internal-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{"companyId":13,"event":"shipment.deleted","entityId":8840}'

# shipment.status_changed  (REQUIERE context.status)
curl -X POST https://TU_DOMINIO/internal/webhook/emit \
  -H "X-Internal-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{"companyId":13,"event":"shipment.status_changed","entityId":8840,
       "context":{"status":"DELIVERED","comment":"Entregado en recepción"}}'
```

### Package (paquete)

```bash
# package.created
curl -X POST https://TU_DOMINIO/internal/webhook/emit \
  -H "X-Internal-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{"companyId":13,"event":"package.created","entityId":21892}'

# package.updated
curl -X POST https://TU_DOMINIO/internal/webhook/emit \
  -H "X-Internal-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{"companyId":13,"event":"package.updated","entityId":21892}'

# package.deleted
curl -X POST https://TU_DOMINIO/internal/webhook/emit \
  -H "X-Internal-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{"companyId":13,"event":"package.deleted","entityId":21892}'

# package.status_changed  (REQUIERE context.status)
curl -X POST https://TU_DOMINIO/internal/webhook/emit \
  -H "X-Internal-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{"companyId":13,"event":"package.status_changed","entityId":21892,
       "context":{"status":"EN TRANSITO"}}'
```

### POBOX (casillero) — *no tiene `status_changed`*

```bash
# pobox.created
curl -X POST https://TU_DOMINIO/internal/webhook/emit \
  -H "X-Internal-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{"companyId":13,"event":"pobox.created","entityId":26275}'

# pobox.updated
curl -X POST https://TU_DOMINIO/internal/webhook/emit \
  -H "X-Internal-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{"companyId":13,"event":"pobox.updated","entityId":26275}'

# pobox.deleted
curl -X POST https://TU_DOMINIO/internal/webhook/emit \
  -H "X-Internal-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{"companyId":13,"event":"pobox.deleted","entityId":26275}'
```

### Customer (cliente) — *no tiene `status_changed`*

```bash
# customer.created
curl -X POST https://TU_DOMINIO/internal/webhook/emit \
  -H "X-Internal-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{"companyId":13,"event":"customer.created","entityId":106453}'

# customer.updated
curl -X POST https://TU_DOMINIO/internal/webhook/emit \
  -H "X-Internal-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{"companyId":13,"event":"customer.updated","entityId":106453}'

# customer.deleted
curl -X POST https://TU_DOMINIO/internal/webhook/emit \
  -H "X-Internal-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{"companyId":13,"event":"customer.deleted","entityId":106453}'
```

### Consolidated (consolidado) — *solo `status_changed`*

```bash
# consolidated.status_changed  (REQUIERE context.status)
curl -X POST https://TU_DOMINIO/internal/webhook/emit \
  -H "X-Internal-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{"companyId":13,"event":"consolidated.status_changed","entityId":555,
       "context":{"status":"CERRADO"}}'
```

---

## Respuestas del endpoint

### 202 Accepted — encolado correctamente (éxito)

```json
{ "accepted": true, "event": "shipment.status_changed", "entityId": 8840, "outcome": "queued" }
```

### 200 OK — petición válida pero sin entrega

El `outcome` explica por qué no se entregó. Normalmente es configuración del lado
del cliente, **no** un error del legacy:

| `outcome` | Significado |
|-----------|-------------|
| `no_config` | La compañía no tiene webhook configurado |
| `not_subscribed` | El webhook existe pero no está suscrito a ese evento |
| `disabled` | El webhook está deshabilitado |
| `no_mapper` | No debería ocurrir; todos los tipos tienen mapper |

```json
{ "accepted": false, "event": "shipment.created", "entityId": 8840, "outcome": "not_subscribed" }
```

### 400 Bad Request — error de validación del request

- Falta o es inválido `companyId`, `event` o `entityId`.
- `event` no existe en el catálogo.
- Evento `*.status_changed` sin `context.status` (o vacío).

```json
{ "error": "...", "message": "A non-empty \"status\" is required in context for status_changed events." }
```

### 401 Unauthorized

`X-Internal-Token` ausente o incorrecto.

---

## Notas importantes para el integrador

1. **`*.status_changed` siempre requiere `context.status`** (string no vacío). El
   resto de `context` es libre (p. ej. `comment`).
2. El `entityId` debe **existir y pertenecer al `companyId`** indicado. Si no existe
   para esa compañía, el endpoint igual responde `202 queued` (la entidad se valida
   después, en background) pero el webhook **no se entrega**. Hay que asegurarse de
   enviar ids válidos.
3. Es **fire-and-forget**: un `202` significa "aceptado y encolado", no "entregado".
   La entrega real al sistema destino ocurre de forma asíncrona vía worker de
   Messenger.
4. Ante errores de red, el legacy puede **reintentar** con seguridad; cada llamada
   encola un nuevo envío.
5. El token es un secreto: debe viajar siempre por HTTPS y nunca exponerse en
   logs o en el cliente.
