# Estándar de Integraciones Animal Charlie

Animal Charlie centraliza las APIs externas en una capa de integraciones. Los módulos internos no deben hablar con cada proveedor externo ni conocer sus parámetros originales. Deben pedir una **capacidad normalizada** a Animal Charlie y recibir una respuesta común.

Endpoint único para módulos:

```http
POST /api/integrations/query
Authorization: Bearer {token}
Content-Type: application/json
```

## Regla de arquitectura

- Los módulos llaman siempre a `/api/integrations/query`.
- La petición usa `integration`, `capability` y `filters`.
- El backend traduce los filtros al proveedor externo que corresponda.
- El backend normaliza la respuesta antes de devolverla.
- Si mañana se cambia CIMAVet por otra fuente, el módulo no debería cambiar si la capacidad sigue igual.

## Formato de petición

```json
{
  "integration": "cimavet",
  "capability": "veterinary_medicines.search",
  "filters": {
    "query": "eq-zona"
  }
}
```

Campos:

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---:|---|
| `integration` | string | Sí | Integración registrada. Ejemplo: `cimavet`, `ligal` o `cegcol`. |
| `capability` | string | Sí | Acción normalizada que se solicita. Ejemplo: `veterinary_medicines.search`, `ligal.tests.search` o `cegcol.censo.search`. |
| `filters` | object | No | Filtros normalizados de Animal Charlie. |
| `includeRaw` | boolean | No | Si es `true`, incluye la respuesta bruta externa para depuración. No usar en módulos de negocio. |

## Formato de respuesta

Todas las integraciones deben responder con esta envoltura:

```json
{
  "ok": true,
  "integration": "cimavet",
  "capability": {
    "requested": "veterinary_medicines.search",
    "resolved": "veterinary_medicines.search"
  },
  "source": {
    "provider": "AEMPS",
    "system": "CIMAVet",
    "adapter": "animal-charlie-cimavet-v1",
    "fetchedAt": "2026-05-18T21:00:00Z",
    "upstream": {
      "path": "medicamentos",
      "status": 200
    }
  },
  "request": {
    "filters": {
      "query": "eq-zona"
    },
    "translatedQuery": {
      "nombre": "eq-zona"
    }
  },
  "result": {
    "kind": "veterinary_medicine",
    "total": 1,
    "page": 1,
    "pageSize": 25,
    "items": []
  }
}
```

Campos estables para consumir:

| Campo | Uso |
|---|---|
| `ok` | Indica que la integración respondió correctamente. |
| `capability.resolved` | Capacidad final ejecutada. Permite alias internos. |
| `source` | Trazabilidad del proveedor, adaptador y momento de consulta. |
| `request.translatedQuery` | Parámetros a los que Animal Charlie tradujo la petición. Útil para depurar. |
| `result.kind` | Tipo normalizado de los elementos devueltos. |
| `result.items` | Lista normalizada que deben usar los módulos. |
| `result.item` | Objeto único normalizado cuando la capacidad devuelve detalle. |

## Formato de error

Los módulos consumidores deben tratar los errores como errores del adaptador, no como errores del proveedor externo. El backend intenta devolver siempre un `error` estable y un `detail` legible:

```json
{
  "error": "outlook_not_configured",
  "detail": "Configura OUTLOOK_TENANT_ID, OUTLOOK_CLIENT_ID, OUTLOOK_CLIENT_SECRET y OUTLOOK_MAILBOX."
}
```

Errores habituales:

| Error | Significado | Acción del módulo |
|---|---|---|
| `unknown_integration` | La integración no está registrada. | No reintentar; revisar configuración del módulo. |
| `unknown_capability` | La capacidad no existe en esa integración. | Usar una capacidad publicada en `GET /api/integrations`. |
| `*_not_configured` | Faltan credenciales externas. | Mostrar estado de configuración pendiente. |
| `cegcol_credentials_required` | Faltan `CEGCOL_CLIENT_ID`, `CEGCOL_CLIENT_SECRET`, usuario o contraseña. | Mostrar CEGCOL sin credenciales; no pedir secretos en UI. |
| `cegcol_return_error` | CEGCOL respondió `returnCode` distinto de `0`. | Mostrar `detail`/`returnCode` y revisar filtros. |
| `integration_write_confirmation_required` | Operación de escritura sin `confirmWrite=true`. | Pedir confirmación explícita al usuario. |
| `integration_write_forbidden` | Usuario sin `integrations.write` ni rol legacy de escritura. | No reintentar con el mismo usuario. |
| `*_graph_error`, `*_api_error`, `*_bridge_error` | El proveedor externo rechazó o no pudo completar la operación. | Mostrar `detail` y registrar trazabilidad. |

Las escrituras externas nunca deben ejecutarse de forma implícita. Cualquier capacidad con `method: "write"` requiere `confirmWrite=true` y permiso global `integrations.write`, manteniendo compatibilidad con los roles legacy que ya podían escribir.

## Cómo consultar desde frontend

Usar el helper de `app.js`:

```js
const data = await integrationRequest("cimavet", "veterinary_medicines.search", {
  query: "eq-zona",
});

const medicines = data.result.items;
```

Detalle por código nacional:

```js
const data = await integrationRequest("cimavet", "veterinary_presentations.get", {
  nationalCode: "570828",
});

const presentation = data.result.item;
```

Consulta LIGAL:

```js
const data = await integrationRequest("ligal", "ligal.tests.search", {
  top: 20,
  skip: 0,
  orderBy: "Code",
});

const tests = data.result.items;
```

Consulta CEGCOL:

```js
const data = await integrationRequest("cegcol", "cegcol.controles.search", {
  fecha: "2026-05-31",
  status: 0,
  rega: "ES150000000000",
});

const controles = data.result.items;
```

## Cómo consultar desde backend

Dentro de `server.py`, usar el adaptador en vez de construir URLs externas desde otro módulo:

```python
response, status = self.execute_cimavet_query(
    "veterinary_medicines.search",
    {"query": "eq-zona"},
)

items = response["result"]["items"]
```

Para exponer una consulta a otro endpoint interno, ese endpoint debe llamar al adaptador y devolver datos ya normalizados o transformarlos a su propio caso de uso.

Para LIGAL:

```python
response, status = self.execute_ligal_query(
    "ligal.customers.search",
    {"top": 20, "skip": 0, "orderBy": "Code"},
    user=user,
)

customers = response["result"]["items"]
```

Para CEGCOL:

```python
response, status = self.execute_cegcol_query(
    "cegcol.censo.search",
    {"fechaIni": "2026-05-01", "fechaFin": "2026-05-31", "status": 0},
    user=user,
)

animals = response["result"]["items"]
```

Para mensajería y correo:

```python
response, status = self.execute_outlook_query(
    "outlook.mail.messages.search",
    {"folder": "Inbox", "top": 25},
    user=user,
)

messages = response["result"]["items"]
```

```python
response, status = self.execute_whatsapp_query(
    "whatsapp.messages.search",
    {"limit": 30},
    user=user,
)

messages = response["result"]["items"]
```

```python
response, status = self.execute_telegram_query(
    "telegram.updates.receive",
    {"limit": 30, "timeout": 0},
    user=user,
)

updates = response["result"]["items"]
```

Cuando un módulo necesita escribir, debe construir una acción explícita de usuario y pasar `confirmWrite=true` solo después de confirmarla:

```python
response, status = self.execute_outlook_query(
    "outlook.mail.messages.send",
    {
        "to": "cliente@empresa.com",
        "subject": "Factura",
        "body": "Adjuntamos factura.",
        "confirmWrite": True,
    },
    user=user,
)
```

## Catálogo de integraciones

Consultar integraciones disponibles:

```http
GET /api/integrations
Authorization: Bearer {token}
```

La respuesta incluye:

- `integrations[].id`
- `integrations[].endpoint`
- `integrations[].capabilities[]`
- `integrations[].connection`
- `integrations[].statistics`
- `policy.access`

`connection` describe cómo está conectada la integración sin entregar secretos:

| Campo | Uso |
|---|---|
| `connection.mode` | Tipo de integración. Ejemplo: `adapter` o `local_bridge`. |
| `connection.baseUrl` | URL base del proveedor externo. |
| `connection.docs` | Documento interno de referencia. |
| `connection.auth` | Tipo de autenticación externa. Ejemplo: `oauth_password_grant`. |
| `connection.credentials` | `configured`, `missing` o `not_required`. |
| `connection.timeoutSeconds` | Timeout configurado para llamadas externas. |
| `connection.token.cached` | Indica si existe token externo cacheado y vigente. No incluye el token. |
| `connection.token.expiresInSeconds` | Vida restante aproximada del token cacheado. |

`statistics` permite ver cobertura y actividad operativa:

| Campo | Uso |
|---|---|
| `statistics.capabilities.total` | Número de capacidades registradas. |
| `statistics.capabilities.read` | Capacidades de lectura. |
| `statistics.capabilities.write` | Capacidades de escritura. |
| `statistics.capabilities.filters` | Total de filtros normalizados declarados. |
| `statistics.runtime.totalCalls` | Consultas ejecutadas desde que arrancó el servidor. |
| `statistics.runtime.successCalls` | Consultas con respuesta correcta. |
| `statistics.runtime.errorCalls` | Consultas con error o respuesta no OK. |
| `statistics.runtime.lastStatus` | Último código HTTP devuelto por el adaptador. |
| `statistics.runtime.lastCapability` | Última capacidad ejecutada. |
| `statistics.runtime.byCapability` | Contadores por capacidad. |

Los contadores `runtime` viven en memoria y se reinician al reiniciar el proceso. Sirven para operación inmediata en el módulo Integraciones, no como auditoría histórica.

## UI operativa del módulo

La pantalla `#integraciones` está pensada como consola de operación, no como página de documentación. Mantiene cuatro zonas estables:

- `#integrationsOverview`: banda de resumen con activas, atención, capacidades y consultas runtime.
- `.integration-list-panel`: índice lateral de proveedores con estado, categoría, capacidades y llamadas.
- `.integration-selected-panel`: cabecera de la integración seleccionada con metadatos, documentación y `#integrationSubnav`.
- `.integration-query-panel`: consola de trabajo con scroll interno para pruebas, formularios, QR, inbox o resultados.

El submenú marca la sección activa con `aria-current`, y los saltos desplazan el panel interno para no mover toda la página. Cualquier proveedor nuevo debe encajar en este patrón: resumen lateral, panel de detalle, secciones navegables y resultados contenidos.

## Consumo desde Almacén Lechero

Los datos externos que pertenezcan a una explotación concreta no deben quedar solo como respuesta temporal de `/api/integrations/query`. Deben materializarse en el módulo `Almacén Lechero` cuando exista una clave de explotación:

- cliente interno: `customers.id`;
- explotación interna: `customer_farms.id`;
- REGA: `customer_farms.rega`;
- proveedor: `source_id`;
- tipo de dato: `entity_type`.

LIGAL usa este patrón para sincronizar una vez al día clientes, muestras y medias por REGA. La documentación del almacén está en `docs/ALMACEN_LECHERO.md`.

## CIMAVet

Integración registrada:

```json
{
  "id": "cimavet",
  "provider": "AEMPS",
  "access": "adapter",
  "endpoint": "/api/integrations/query"
}
```

### Capacidades

| Capacidad | Resultado | Filtros normalizados |
|---|---|---|
| `veterinary_medicines.search` | `veterinary_medicine[]` | `query`, `name`, `laboratory`, `activeSubstance`, `activeSubstance2`, `activeSubstanceId`, `activeSubstance2Id`, `nationalCode`, `atc`, `registrationNumber`, `registrationSearch`, `excipient`, `excipientId`, `species`, `speciesId`, `commercialized`, `administrativeStatus`, `routeId`, `route`, `pharmaceuticalFormId`, `pharmaceuticalForm`, `medicineType`, `administration`, `narcotic`, `psychotropic`, `dispensing`, `page` |
| `veterinary_medicines.get` | `veterinary_medicine` | `registrationNumber`, `nationalCode` |
| `veterinary_presentations.search` | `veterinary_presentation[]` | `registrationNumber`, `nationalCode`, `commercialized`, `page` |
| `veterinary_presentations.get` | `veterinary_presentation` | `nationalCode` |
| `veterinary_safety_notes.search` | `veterinary_safety_note[]` | `registrationNumber` |
| `veterinary_master_data.search` | `master_item[]` | `masterType`, `name`, `id`, `code`, `page` |
| `veterinary_change_log.search` | `veterinary_change[]` | `dateFrom`, `registrationNumber`, `registrationNumbers`, `page` |

### Tipos normalizados

#### `veterinary_medicine`

Campos principales:

| Campo | Descripción |
|---|---|
| `id` | Identificador preferente. CN si existe, si no número de registro. |
| `externalId.registrationNumber` | Número de registro CIMAVet. |
| `externalId.nationalCode` | Código nacional si aplica. |
| `name` | Nombre del medicamento. |
| `registrationNumber` | Número de registro. |
| `nationalCode` | Código nacional. |
| `holder` | Laboratorio titular. |
| `status.status` | `authorized`, `suspended`, `revoked` o `unknown`. |
| `commercialized` | Estado de comercialización. |
| `activeSubstances` | Lista normalizada de principios activos. |
| `excipients` | Lista normalizada de excipientes. |
| `documents` | Documentos normalizados con `label`, `url` y `type`. |
| `flags` | Indicadores como `ema`, `narcotic`, `psychotropic`, `antibiotic`, `safetyNotes`. |
| `source` | Siempre `cimavet`. |

#### `veterinary_presentation`

Usa el mismo modelo base que `veterinary_medicine`, añadiendo de forma habitual `nationalCode`.

#### `master_item`

```json
{
  "type": "master_item",
  "id": 166,
  "code": "166A",
  "name": "FENILBUTAZONA",
  "source": "cimavet"
}
```

#### `veterinary_change`

```json
{
  "type": "veterinary_change",
  "registrationNumber": "EU001 IP",
  "changedAt": "2026-05-01",
  "changeType": "modified",
  "changedFields": [
    { "id": "otros", "label": "Otros cambios" }
  ],
  "source": "cimavet"
}
```

#### `veterinary_safety_note`

```json
{
  "type": "veterinary_safety_note",
  "number": "1/2026",
  "reference": "AEMPS",
  "subject": "Asunto publicado por AEMPS",
  "publishedAt": "2026-05-01",
  "url": "https://...",
  "source": "cimavet"
}
```

## Outlook Microsoft 365

Integración registrada:

```json
{
  "id": "outlook",
  "provider": "Microsoft Graph",
  "access": "adapter",
  "endpoint": "/api/integrations/query",
  "docs": "/docs/OUTLOOK_INTEGRATION.md"
}
```

Usa `OUTLOOK_TENANT_ID`, `OUTLOOK_CLIENT_ID`, `OUTLOOK_CLIENT_SECRET` y `OUTLOOK_MAILBOX`, o los ficheros equivalentes en `data/outlook-*.txt`.

### Capacidades

| Capacidad | Resultado | Filtros normalizados |
|---|---|---|
| `outlook.auth.status` | `outlook_mailbox` | `force` |
| `outlook.mail.folders.search` | `outlook_mail_folder[]` | `mailbox`, `includeHidden`, `top` |
| `outlook.mail.messages.search` | `outlook_message[]` | `mailbox`, `folderId`, `folder`, `query`, `from`, `unreadOnly`, `since`, `top`, `skip` |
| `outlook.mail.messages.get` | `outlook_message` | `mailbox`, `messageId`, `bodyContentType` |
| `outlook.mail.messages.send` | `outlook_send_result` | `mailbox`, `to`, `cc`, `bcc`, `subject`, `body`, `bodyType`, `attachments`, `saveToSentItems`, `confirmWrite` |
| `outlook.mail.documents.send` | `outlook_send_result` | `mailbox`, `to`, `subject`, `body`, `fileName`, `mimeType`, `dataBase64`, `path`, `saveToSentItems`, `confirmWrite` |
| `outlook.mail.messages.mark_read` | `outlook_message` | `mailbox`, `messageId`, `isRead`, `confirmWrite` |

### `outlook_message`

```json
{
  "type": "outlook_message",
  "id": "AAMk...",
  "subject": "Factura",
  "from": { "name": "Cliente", "address": "cliente@empresa.com" },
  "receivedAt": "2026-05-19T08:00:00Z",
  "bodyPreview": "Texto inicial",
  "hasAttachments": true,
  "isRead": false,
  "source": "outlook"
}
```

## Telegram

Integración registrada:

```json
{
  "id": "telegram",
  "provider": "Telegram Bot API",
  "access": "adapter",
  "endpoint": "/api/integrations/query",
  "docs": "/docs/TELEGRAM_INTEGRATION.md"
}
```

Usa `TELEGRAM_BOT_TOKEN` o `data/telegram-bot-token.txt`. Los módulos internos no exponen ni consumen el token.

### Capacidades

| Capacidad | Resultado | Filtros normalizados |
|---|---|---|
| `telegram.bot.status` | `telegram_bot` | Ninguno |
| `telegram.webhook.info` | `telegram_webhook` | Ninguno |
| `telegram.webhook.delete` | `telegram_operation` | `dropPendingUpdates`, `confirmWrite` |
| `telegram.updates.receive` | `telegram_message[]` | `offset`, `limit`, `timeout`, `allowedUpdates` |
| `telegram.messages.send` | `telegram_message` | `chatId`, `text`, `parseMode`, `disableNotification`, `replyToMessageId`, `confirmWrite` |
| `telegram.documents.send` | `telegram_message` | `chatId`, `fileName`, `mimeType`, `dataBase64`, `path`, `caption`, `parseMode`, `confirmWrite` |

### `telegram_message`

```json
{
  "type": "telegram_message",
  "id": 42,
  "updateId": 123456,
  "chatId": 123456789,
  "direction": "inbound",
  "text": "Texto recibido",
  "messageType": "text",
  "timestamp": "2026-05-19",
  "source": "telegram"
}
```

## WhatsApp Web

Integración registrada:

```json
{
  "id": "whatsapp",
  "provider": "WhatsApp Web / Baileys",
  "access": "local_bridge",
  "endpoint": "/api/integrations/query",
  "docs": "/docs/WHATSAPP_INTEGRATION.md"
}
```

Usa una pasarela local en `integrations/whatsapp-bridge` basada en `baileys`, siguiendo el patrón de OpenClaw: QR en un proceso sidecar actualizable y contrato interno estable para Animal Charlie.

### Capacidades

| Capacidad | Resultado | Filtros normalizados |
|---|---|---|
| `whatsapp.connection.status` | `whatsapp_connection` | Ninguno |
| `whatsapp.connection.qr` | `whatsapp_qr` | Ninguno |
| `whatsapp.chats.search` | `whatsapp_chat[]` | `query`, `limit` |
| `whatsapp.messages.search` | `whatsapp_message[]` | `chatId`, `direction`, `query`, `limit` |
| `whatsapp.messages.media` | `whatsapp_media` | `messageId` |
| `whatsapp.messages.send` | `whatsapp_send_result` | `to`, `text`, `confirmWrite` |
| `whatsapp.documents.send` | `whatsapp_send_result` | `to`, `fileName`, `mimeType`, `dataBase64`, `path`, `caption`, `confirmWrite` |
| `whatsapp.connection.disconnect` | `whatsapp_connection` | `confirmWrite` |

Las capacidades de escritura requieren `confirmWrite=true`. El adaptador permite escritura a usuarios `admin` y `technician`.

### `whatsapp_message`

```json
{
  "type": "whatsapp_message",
  "id": "ABC123",
  "chatId": "34600111222@s.whatsapp.net",
  "from": "34600111222@s.whatsapp.net",
  "to": "me",
  "direction": "inbound",
  "text": "Texto recibido",
  "messageType": "conversation",
  "hasMedia": false,
  "timestamp": "2026-05-18T22:00:00.000Z",
  "source": "whatsapp"
}
```

## LIGAL

Integración registrada:

```json
{
  "id": "ligal",
  "provider": "LIGAL",
  "access": "adapter",
  "endpoint": "/api/integrations/query",
  "docs": "/docs/LIGAL_API.md"
}
```

La autenticación externa de LIGAL se gestiona dentro del adaptador con `LIGAL_USER`/`LIGAL_PASSWORD` o con `data/ligal-username.txt` y `data/ligal-password.txt`. El token externo no se entrega a los módulos consumidores.

El adaptador conserva dos niveles de analítica de leche: medias de periodo (`ligal_average`) y muestreos por fecha (`ligal_sample`). La sincronización del Almacén Lechero consulta `ligal.sample_types.search` (`/api/SampleTypes`) para localizar el tipo real de pago por calidad; en la cuenta probada el código es `I0` (`Pago por calidade`) y devuelve los muestreos casi diarios de leche. Si se define `DATAHUB_LIGAL_SAMPLE_TYPES`, esos códigos prevalecen. Si `/api/samples` no devuelve filas para los códigos seleccionados, también aprovecha las determinaciones anidadas en medias (`MEDIAS_DETALLE`, `MEDIAS_DETALLE_DETERMINACIONES`, `DETERMINACIONES` y `MUESTRAS`) para materializar muestreos como registros y métricas `ligal.sample.*` cuando el servicio los incluya ahí.

### Capacidades principales de lectura

| Capacidad | Resultado | Filtros normalizados |
|---|---|---|
| `ligal.tests.search` | `lab_test[]` | `materialCode`, `sampleTypeCode`, `averageTypeCode`, `graphicable`, `alerts`, `top`, `skip`, `orderBy` |
| `ligal.tests.get` | `lab_test` | `testCode` |
| `ligal.customers.search` | `lab_customer[]` | `typeCode`, `code`, `relatedCode`, `name`, `vatReg`, `rega`, `tagSerie`, `relationCode`, `vigente`, `route`, `top`, `skip`, `orderBy` |
| `ligal.customers.related.search` | `lab_related_customer[]` | `customerCode`, `top`, `skip`, `orderBy` |
| `ligal.sample_types.search` | `lab_sample_type[]` | `top`, `skip`, `orderBy` |
| `ligal.samples.search` | `lab_sample[]` | `sampleTypeCode`, `sampleDateFilterType`, `fromDate`, `toDate`, `customerCode`, `relatedCustomerCode`, `materialCode`, `reference`, `testCodes`, `testFilters`, `collaborativeId`, `revisionCodes`, `farmerTank`, `tankCode`, `top`, `skip`, `orderBy` |
| `ligal.samples.get` | `lab_sample` | `code` |
| `ligal.samples.download_url.get` | `lab_sample_download_url` | `code` |
| `ligal.averages.search` | `lab_average[]` | `fromDate`, `toDate`, `customerCode`, `relatedCustomerCode`, `averageTypeCode`, `route`, `top`, `skip`, `orderBy` |
| `ligal.account.limit_values.search` | `ligal_limit_value[]` | `testCode` |
| `ligal.requests.*.search` | modelos `lab_request_*` | Según `docs/LIGAL_API.md`; algunas rutas requieren permisos no disponibles para todos los usuarios LIGAL. |
| `ligal.pickers.*` | `milk_picker[]` o resultado de escritura | Según `docs/LIGAL_API.md`; algunas rutas requieren permisos específicos. |

### Capacidades de escritura

Las capacidades externas de escritura existen para cubrir el Swagger completo, pero Animal Charlie las bloquea salvo que:

- el usuario interno sea `admin`;
- la petición incluya `confirmWrite: true`;
- el payload llegue dentro de `filters.body` cuando la operación requiera cuerpo.

Capacidades de escritura registradas:

- `ligal.account.users.create`
- `ligal.account.limit_values.save`
- `ligal.account.subscription.renew`
- `ligal.pickers.create`
- `ligal.pickers.create_many`
- `ligal.requests.receptions.create`

### Tipos normalizados

#### `lab_test`

```json
{
  "type": "lab_test",
  "id": 1,
  "code": "000001",
  "name": "Ensayo",
  "description": "Ensayo",
  "abbreviation": "ENS",
  "graphicable": true,
  "alert": false,
  "source": "ligal"
}
```

#### `lab_customer`

```json
{
  "type": "lab_customer",
  "id": 1,
  "code": "1578100008",
  "name": "Cliente",
  "vat": "B00000000",
  "rega": "ES000000000000",
  "relation": { "code": "R1", "route": "Ruta" },
  "source": "ligal"
}
```

#### `lab_sample`

```json
{
  "type": "lab_sample",
  "code": "M000001",
  "sampleDate": "2026-05-18T00:00:00",
  "receptionDate": "2026-05-18T00:00:00",
  "customer": {},
  "material": {},
  "reports": [],
  "source": "ligal"
}
```

#### `lab_average`

```json
{
  "type": "lab_average",
  "id": 1,
  "period": "2026-05",
  "customer": {},
  "relatedCustomer": {},
  "nonConformity": false,
  "source": "ligal"
}
```

## Ejemplos curl

Buscar medicamento:

```bash
curl -sS -X POST http://127.0.0.1:8020/api/integrations/query \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"integration":"cimavet","capability":"veterinary_medicines.search","filters":{"query":"eq-zona"}}'
```

Detalle de presentación:

```bash
curl -sS -X POST http://127.0.0.1:8020/api/integrations/query \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"integration":"cimavet","capability":"veterinary_presentations.get","filters":{"nationalCode":"570828"}}'
```

Registro de cambios:

```bash
curl -sS -X POST http://127.0.0.1:8020/api/integrations/query \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"integration":"cimavet","capability":"veterinary_change_log.search","filters":{"dateFrom":"01/05/2026"}}'
```

Buscar ensayos LIGAL:

```bash
curl -sS -X POST http://127.0.0.1:8020/api/integrations/query \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"integration":"ligal","capability":"ligal.tests.search","filters":{"top":20,"skip":0,"orderBy":"Code"}}'
```

Buscar clientes LIGAL:

```bash
curl -sS -X POST http://127.0.0.1:8020/api/integrations/query \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"integration":"ligal","capability":"ligal.customers.search","filters":{"top":20,"skip":0,"orderBy":"Code"}}'
```

Comprobar Outlook:

```bash
curl -sS -X POST http://127.0.0.1:8020/api/integrations/query \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"integration":"outlook","capability":"outlook.auth.status","filters":{}}'
```

Listar correos Outlook:

```bash
curl -sS -X POST http://127.0.0.1:8020/api/integrations/query \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"integration":"outlook","capability":"outlook.mail.messages.search","filters":{"folder":"Inbox","top":25}}'
```

Enviar correo Outlook:

```bash
curl -sS -X POST http://127.0.0.1:8020/api/integrations/query \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"integration":"outlook","capability":"outlook.mail.messages.send","filters":{"to":"cliente@empresa.com","subject":"Factura","body":"Adjuntamos factura.","confirmWrite":true}}'
```

Estado WhatsApp:

```bash
curl -sS -X POST http://127.0.0.1:8020/api/integrations/query \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"integration":"whatsapp","capability":"whatsapp.connection.status","filters":{}}'
```

Leer mensajes WhatsApp:

```bash
curl -sS -X POST http://127.0.0.1:8020/api/integrations/query \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"integration":"whatsapp","capability":"whatsapp.messages.search","filters":{"limit":30}}'
```

Estado Telegram:

```bash
curl -sS -X POST http://127.0.0.1:8020/api/integrations/query \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"integration":"telegram","capability":"telegram.bot.status","filters":{}}'
```

Leer actualizaciones Telegram:

```bash
curl -sS -X POST http://127.0.0.1:8020/api/integrations/query \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"integration":"telegram","capability":"telegram.updates.receive","filters":{"limit":30,"timeout":0}}'
```

## Añadir una nueva integración

Para añadir otro proveedor:

1. Registrar la integración en `INTEGRATIONS` con `id`, `provider`, `endpoint` y `capabilities`.
2. Crear una función tipo `execute_{integration}_query(capability, filters, include_raw=False)`.
3. Traducir filtros normalizados a parámetros externos dentro del adaptador.
4. Normalizar la respuesta a la envoltura común.
5. No exponer detalles del proveedor al módulo consumidor salvo en `source` y `request.translatedQuery`.
6. Añadir ejemplos de uso en este documento.

## Reglas para módulos consumidores

- No usar `fetch()` contra CIMAVet, LIGAL, Outlook, Telegram, WhatsApp ni contra otra API externa.
- No depender de nombres de campos externos como `nregistro`, `cn`, `resultados` o `totalFilas`.
- Usar campos normalizados como `registrationNumber`, `nationalCode`, `code`, `name`, `result.items`, `result.total`.
- Tratar `source` como trazabilidad, no como modelo de negocio.
- Pedir nuevas capacidades si un módulo necesita otro tipo de consulta.
- Consultar `GET /api/integrations` para descubrir capacidades, credenciales y estado antes de mostrar acciones al usuario.
