# Mantener el catálogo de un campo de lista desde una planilla

Tu sistema reemplaza las opciones de los campos de lista de un embudo. Sirve cuando el catálogo vive en otro lado —una hoja de cálculo, tu ERP, tu base de productos— y cambia seguido: en vez de cargarlo a mano en la pantalla, lo sincronizas.

Base de la API de Lelemon: `https://api.lelemon.cl`

La idea de fondo: **mandas el catálogo completo y queda ese.** No hay que agregar ni borrar opción por opción, y mandar dos veces lo mismo no cambia nada.

## Antes de empezar

Necesitas una credencial con el alcance `pipelines:write`. Se emite en Integraciones API del dashboard.

Va separado de la lectura a propósito: `GET /v1/external/pipelines` pide `agents:read`, y quien sólo audita la configuración no tiene por qué poder cambiar lo que el agente ofrece.

Las claves de los campos salen de `GET /v1/external/pipelines/{id}`, en `fields[].key`.

## El pedido

```
PUT https://api.lelemon.cl/v1/external/pipelines/{id}/fields/options
Authorization: Bearer <tu credencial>
Content-Type: application/json

{
  "fields": [
    {
      "key": "tipificado_producto",
      "options": [
        { "value": "tuu-pos", "label": "TUU POS" },
        { "value": "hosting" }
      ]
    },
    {
      "key": "tipificado_descripcion",
      "options": [
        { "value": "no-imprime", "label": "No imprime el voucher", "whenParent": ["tuu-pos"] },
        { "value": "sitio-caido", "label": "El sitio no carga", "whenParent": ["hosting"] }
      ]
    }
  ]
}
```

Respuesta:

```json
{
  "id": "2f8c...",
  "changed": true,
  "fields": [
    { "key": "tipificado_producto", "options": 2 },
    { "key": "tipificado_descripcion", "options": 2 }
  ]
}
```

- `value` es lo que se guarda en el caso. Es único dentro del campo, sin distinguir mayúsculas ni espacios al borde.
- `label` es lo que se muestra. Si no lo mandas, vale lo mismo que `value`.
- `whenParent` sólo se usa en un campo que depende de otro: son los valores del padre para los que esa opción existe. Si no lo mandas, la opción vale para cualquier valor del padre.
- Los espacios al borde se recortan al guardar.

## Padre e hijo van en el mismo pedido

Si un campo depende de otro, los dos se mandan juntos. No es una recomendación: es lo que hace que la sincronización funcione sin que tengas que pensar el orden.

Sacar un producto del padre mientras sus descripciones todavía lo nombran en `whenParent` deja el embudo inconsistente, y agregar descripciones de un producto que el padre aún no tiene, también. Mandados juntos, se validan juntos: o entran los dos o no entra ninguno, y el embudo nunca queda a medias.

Los campos que el cuerpo no nombra quedan exactamente como estaban.

## Repetir el mismo pedido no cambia nada

El reemplazo es total, así que no hace falta que mandes una referencia para no duplicar. Si lo que llega es igual a lo que ya estaba, no se escribe y la respuesta trae `changed: false`. Puedes correr la sincronización cada hora sin pensarlo.

## Los casos ya guardados conservan su valor

Si una opción sale del catálogo, los casos que la tenían guardada la siguen mostrando. No se borra ni se vacía nada: la validación de opciones corre cuando se escribe un caso, no cuando se lee.

## Topes

| Tope | Cuánto | Dónde aplica |
|---|---|---|
| Campos por pedido | 50 | Cada pedido |
| Opciones por campo | 2.000 | Cada campo |
| Caracteres de `value` y de `label` | 80 | Cada uno por separado |
| Valores del padre por opción | 200 | Cada `whenParent` |
| Opciones para un mismo valor del padre | 200 | **Sólo** en campos que llena el agente |

El último merece una línea. Las opciones de un campo en cascada que el agente pide se le escriben al modelo en cada turno, y ahí 200 ya es mucho. Un campo que llena una persona en la bandeja no llega al modelo, así que no tiene ese tope: ahí puedes cargar un catálogo grande sin problema.

## Errores

| Código | Qué pasó |
|---|---|
| 400 | Una clave que el embudo no declara, o un campo que no es de lista. El mensaje lo nombra. |
| 400 | El mismo campo dos veces en el pedido. |
| 400 | Un `value` vacío, o dos que sólo se distinguen por mayúsculas o espacios: para el sistema son la misma opción. |
| 400 | `whenParent` en un campo que no depende de ningún otro: ahí no filtra nada. |
| 400 | La cascada queda inválida: una opción que espera un valor del padre que no existe. El mensaje nombra el campo, la opción y el valor. |
| 400 | Un tope pasado, o un cuerpo mal armado. |
| 401 | Falta la credencial o no sirve. |
| 403 | La credencial no tiene `pipelines:write`. |
| 404 | El embudo no existe en tu organización. |

## Ejemplo con curl

```bash
curl -X PUT "https://api.lelemon.cl/v1/external/pipelines/$EMBUDO/fields/options" \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  -H "Content-Type: application/json" \
  -d @catalogo.json
```

## Cómo verificar que quedó

```bash
curl "https://api.lelemon.cl/v1/external/pipelines/$EMBUDO" \
  -H "Authorization: Bearer $LELEMON_API_KEY"
```

Las opciones vuelven en `fields[].options`, con `whenParent` en las que lo tengan.

## Lo que esta ruta no hace

- No crea ni borra campos. El campo se crea en la pantalla; acá sólo se cambian sus opciones.
- No toca el nombre del campo, si es obligatorio, de quién depende ni quién lo llena.
- No crea ni borra embudos ni etapas.
- No cambia los valores que ya tienen los casos.

Un cambio puede tardar hasta un minuto en reflejarse en lo que el agente ofrece, igual que si lo editaras en la pantalla.

## Mantener las respuestas rápidas de un embudo

Las respuestas rápidas son los textos que tu equipo inserta en el chat escribiendo `/` y el atajo, por ejemplo `/saludo`. Cada embudo tiene las suyas. Tu sistema las puede leer y reemplazar.

### Leerlas

```
GET https://api.lelemon.cl/v1/external/pipelines/{id}/respuestas-rapidas
```

Pide `agents:read`, como el resto de la lectura de embudos.

```json
{
  "id": "2f8c...",
  "respuestas": [
    { "atajo": "saludo", "texto": "Hola {cliente}, soy {agente}. ¿En qué te ayudo?" }
  ]
}
```

Vuelven como están guardadas, con `{agente}` y `{cliente}` sin reemplazar. Un embudo sin respuestas devuelve una lista vacía.

### Reemplazarlas

```
PUT https://api.lelemon.cl/v1/external/pipelines/{id}/respuestas-rapidas
Content-Type: application/json

{
  "respuestas": [
    { "atajo": "saludo", "texto": "Hola {cliente}, soy {agente}. ¿En qué te ayudo?" },
    { "atajo": "despedida", "texto": "Gracias por escribir." }
  ]
}
```

Pide `pipelines:write`, el mismo alcance que cambiar las opciones de un campo.

- **Mandas la lista completa y queda esa.** Lo que no viene se borra, y una lista vacía deja el embudo sin respuestas.
- **No toca nada más del embudo.** Los campos, el reparto, el horario y el resto de la configuración quedan como estaban.
- Repetir el mismo pedido no cambia nada y responde `changed: false`. La respuesta trae la lista como quedó.

Se validan igual que en el panel:

| Regla | Valor |
|---|---|
| Respuestas por embudo | 50 |
| Atajo | De 1 a 30 letras minúsculas, números, `-` o `_`, sin la barra, y único en el embudo |
| Texto | No vacío, hasta 4.096 caracteres |

Si algo no pasa, responde `400` con el mensaje de qué atajo falló y por qué, y no se guarda nada.

### Variables

En el texto puedes usar:

- `{agente}`: el nombre de la persona del equipo que inserta la respuesta.
- `{cliente}`: el nombre del contacto.

Se reemplazan **al insertar la respuesta en el chat**, no al guardarla. Si falta el dato, queda vacío sin dejar la llave ni espacios de sobra: `Hola {cliente}, soy {agente}.` sin nombre del contacto queda `Hola, soy Camila.` Cualquier otra cosa entre llaves o corchetes, como `[nombre]`, se inserta tal cual.

### Errores

| Código | Qué pasó |
|---|---|
| 400 | El cuerpo no es una lista de `atajo` y `texto`, o una respuesta no pasa las reglas de arriba. |
| 401 | Falta la credencial o no sirve. |
| 403 | A la credencial le falta `agents:read` (leer) o `pipelines:write` (reemplazar). |
| 404 | El embudo no existe en tu organización. |

## Campos obligatorios para entrar a una etapa

Una etapa puede exigir que el caso tenga completos ciertos campos del embudo antes de recibirlo. Se configuran en la pantalla de la etapa, en "Campos obligatorios para entrar". Por ejemplo, la etapa Escalado puede pedir RUT, Email y Detalles del caso.

### Leerlos

`GET /v1/external/pipelines/{id}` trae `camposObligatorios` en cada etapa de `stages`: la lista de claves (`fields[].key`) que exige. Una etapa que no exige nada trae la lista vacía.

```json
{
  "stages": [
    { "id": "7c1e...", "name": "Atención", "camposObligatorios": [] },
    { "id": "a93b...", "name": "Escalado", "camposObligatorios": ["rut", "email", "detalles"] }
  ]
}
```

Se leen con `agents:read`. Desde la API no se cambian: se editan en la pantalla de la etapa.

### Qué cuenta como completo

- Texto: algo que no sea sólo espacios.
- Lista de opciones: una opción elegida.
- Archivo: archivo subido.
- Nombre, correo y teléfono del contacto: el dato del contacto.
- Un campo sensible puede ser obligatorio. Se comprueba que tenga valor; el valor no se muestra ni se devuelve.

### Quién se rechaza y quién no

Mientras falte algún campo, el caso no entra a la etapa:

- Mover una tarjeta en el tablero, o varias a la vez, y derivar un caso con `POST /v1/external/conversations/{id}/derivar`.
- El agente, al mover o derivar el caso.

No se bloquean los movimientos automáticos del sistema, para que ningún caso quede atascado: el cierre por inactividad, el fin del seguimiento, el cierre de la conversación, tomar el control, pasar a una persona y las transiciones del flujo.

Cuando se rechaza, la respuesta es `422` con los campos que faltan:

```json
{
  "statusCode": 422,
  "code": "CAMPOS_OBLIGATORIOS",
  "message": "Para pasar a Escalado falta: RUT, Email",
  "etapa": { "id": "a93b...", "nombre": "Escalado" },
  "faltantes": [
    { "key": "rut", "nombre": "RUT" },
    { "key": "email", "nombre": "Email" }
  ]
}
```

Al mover varias tarjetas a la vez se mueven las que cumplen y la respuesta trae `noMovidos`, con el `id` de cada caso y sus `faltantes`.

Si se borra un campo del embudo, deja de ser obligatorio en todas las etapas.
