{
  "openapi": "3.1.0",
  "info": {
    "title": "FusionCRM — API externa",
    "version": "1.0.0",
    "description": "API REST para integraciones. Crea un token en Ajustes → API Tokens y asigna\nlos permisos de cada operación (indicados en x-required-permissions).\nAutentica con Authorization: Bearer <token> o X-API-Key: <token>; basta uno.\nEstos permisos son propios de FusionCRM, no scopes OAuth de OpenAPI.\n\nCobertura incremental: por ahora documenta contactos, empresas, negocios, formularios, landing pages, conversaciones, cotizaciones, productos,\nfacturas, compras, envíos, WhatsApp, proyectos, tareas, listas, fragments, biblioteca, RAG, reportes, agentes y agent-context. La\nausencia de otros módulos aquí no significa que no existan. El código es la fuente\nde verdad; la documentación manual de abril solo sirve como estructura.\n\nEl POST de contactos externos actualiza cuando encuentra coincidencia;\nno comparte el contrato duplicate:true de la API interna.\n"
  },
  "servers": [
    {
      "url": "/api",
      "description": "Servidor que aloja esta documentación"
    },
    {
      "url": "https://api.fusioncol.com/api",
      "description": "Producción"
    }
  ],
  "tags": [
    {
      "name": "Me",
      "description": "Identidad del token — organización, usuario, permisos y plan. Úsalo para confirmar a qué cuenta apunta un token antes de actuar."
    },
    {
      "name": "Catalogs",
      "description": "Catálogos nacionales estáticos (municipios y departamentos DANE/DIVIPOLA). Solo lectura, cualquier token válido; cacheables un día con ETag. Dan el `cityCode` que aceptan contactos y empresas."
    },
    {
      "name": "Contacts",
      "description": "Consulta, creación, actualización y eliminación de contactos."
    },
    {
      "name": "Companies",
      "description": "Empresas/cuentas B2B y su vínculo con contactos y negocios."
    },
    {
      "name": "Deals",
      "description": "Negocios del pipeline de ventas, etapas y campos personalizados."
    },
    {
      "name": "Forms",
      "description": "Formularios de captación, sus envíos y el submit server-to-server."
    },
    {
      "name": "Landing pages",
      "description": "Páginas de aterrizaje del constructor del CRM (forms.fusioncol.com/p/<slug>) y su analítica propia. Solo lectura."
    },
    {
      "name": "Campaigns",
      "description": "Campañas masivas de WhatsApp (/mass-whatsapp) y su embudo real (aceptados → entregados → leídos → respondieron, fallidos con motivo). Lectura con contacts:read; crear y editar borradores con automations:write. Ejecutar una campaña no está en la API."
    },
    {
      "name": "Conversations",
      "description": "Conversaciones multicanal (WhatsApp, Messenger, Instagram), sus mensajes, pipelines y etapas."
    },
    {
      "name": "Quotations",
      "description": "Cotizaciones con ítems y totales, su PDF y el contacto asociado (contrato de creación legacy en español)."
    },
    {
      "name": "Products",
      "description": "Catálogo de productos con precio, costo, SKU, dimensiones y sus variantes activas."
    },
    {
      "name": "Invoices",
      "description": "Facturación electrónica DIAN — copia local de los documentos emitidos y recibidos, y emisión de facturas de venta vía el proveedor de facturación."
    },
    {
      "name": "Purchases",
      "description": "Órdenes de compra a proveedores — registro de gasto, sin emisión ante la DIAN. Van con el módulo de facturación electrónica del plan y con permisos propios (purchases:read/write)."
    },
    {
      "name": "Shipments",
      "description": "Envíos con Envia.com (plugin Envíos, módulo envios del plan, apagado por defecto). Cotiza, genera guías, rastrea y cancela desde un contacto; permisos shipments:read (listar, ver, cotizar) y shipments:write (generar, rastrear, cancelar). El origen se configura en la app, no por API. Sin DELETE ni recogida en v1."
    },
    {
      "name": "WhatsApp",
      "description": "Envío de mensajes de sesión y plantillas por la Cloud API de Meta con la integración de WhatsApp de la organización, y listado de plantillas."
    },
    {
      "name": "Projects",
      "description": "Proyectos con owner, fechas y presupuesto; borrar uno elimina sus tareas en cascada."
    },
    {
      "name": "Tasks",
      "description": "Tareas con responsable, proyecto y contacto opcionales, estados y prioridades en español, y el recordatorio de vencimiento."
    },
    {
      "name": "Lists",
      "description": "Listas de contactos estáticas (por ids, máx. 2000) y dinámicas (por filtros evaluados en cada lectura); usan los permisos contacts:*."
    },
    {
      "name": "Fragments",
      "description": "Respuestas rápidas / plantillas de texto o HTML reutilizables por atajo, único por organización."
    },
    {
      "name": "Library",
      "description": "Biblioteca de archivos (S3) — solo lectura y edición de nombre/tags; la subida es exclusiva de la app."
    },
    {
      "name": "RAG",
      "description": "Base de conocimiento por organización — búsqueda semántica, ingesta de texto plano y borrado de documentos; la indexación es asíncrona."
    },
    {
      "name": "Agents",
      "description": "Agentes IA de WhatsApp (prompt, modelo, tools, triggers, esquema de lead). Permisos agents:read (consultas) y agents:write (crear, editar, borrar, activar)."
    },
    {
      "name": "AgentContext",
      "description": "Contexto del cliente (contacto, conversación, mensajes, negocios, patrón) que reciben los agentes (conversations:read), y prueba de respuesta con OpenAI (agents:write)."
    },
    {
      "name": "Reports",
      "description": "Reportes de ventas y de compras — los mismos números de la pantalla Reportes de la app. Solo lectura. Ventas con deals:read; compras con purchases:read y el módulo de facturación electrónica del plan."
    },
    {
      "name": "Webhooks",
      "description": "Webhooks SALIENTES — lo que FusionCRM envía a su servidor (webhook de etapa y peticiones HTTP de automatizaciones)."
    },
    {
      "name": "Deal pipelines",
      "description": "Pipelines de negocios y sus etapas (columnas del tablero) — nombre, orden, color, probabilidad, ganada/perdida y días máximos. Permisos pipelines:read y pipelines:write. Borrar con negocios responde 409."
    },
    {
      "name": "Conversation pipelines",
      "description": "Configuración de los pipelines de conversaciones y sus etapas (nombre, orden, color, asignación automática, webhook de etapa). Permisos pipelines:read / pipelines:write."
    },
    {
      "name": "Deal fields",
      "description": "Campos personalizados de negocios por pipeline (fields:read / fields:write). Borrar un campo borra sus valores en todos los negocios."
    },
    {
      "name": "Contact properties",
      "description": "Propiedades (campos) de los contactos de la organización y sus grupos. Sin DELETE; se ocultan con isVisible=false."
    },
    {
      "name": "Company properties",
      "description": "Propiedades (campos) de las empresas de la organización y sus grupos. Sin DELETE; se ocultan con isVisible=false."
    },
    {
      "name": "Email templates",
      "description": "Plantillas de correo (editor por bloques o HTML) que usan campañas y automatizaciones. templates:read para leer; templates:write para crear, editar y borrar."
    },
    {
      "name": "Document templates",
      "description": "Plantillas de documentos PDF con campos de formulario (AcroForm) y generación del PDF rellenado. Todo con templates:read; subirlas y editarlas se hace desde la app."
    },
    {
      "name": "Organization",
      "description": "Datos de la empresa y ajustes generales (zona horaria, idioma, moneda, ventana de envío, textos por defecto de cotizaciones y compras). settings:read para leer; settings:write y ser propietario para editar."
    },
    {
      "name": "Users",
      "description": "Usuarios (equipo) de la organización con su rol y última actividad. Solo lectura con users:read."
    },
    {
      "name": "Scoring rules",
      "description": "Reglas de puntuación de leads (lead scoring) y recálculo del puntaje de los contactos. Permisos settings:read / settings:write."
    },
    {
      "name": "Deal checklists",
      "description": "Checklist de cierre por pipeline de negocios (ítems con peso, auto-marcado por actividad y requisito de etapa). Permisos pipelines:read / pipelines:write."
    },
    {
      "name": "Product variants",
      "description": "Variantes de producto (SKU único por organización, precio, stock y atributos). Permisos products:read / products:write / products:delete."
    },
    {
      "name": "Invoice config",
      "description": "Configuración de facturación electrónica (DIAN) en solo lectura con settings:read — empresa emisora, resoluciones y consecutivos, ambiente, certificado y correo sin secretos."
    },
    {
      "name": "Integrations",
      "description": "Integraciones conectadas (WhatsApp, Meta, OpenAI, Brevo…) en solo lectura con settings:read. Nunca devuelve credenciales ni settings; solo los nombres de las llaves configuradas."
    },
    {
      "name": "Mail accounts",
      "description": "Cuentas de correo IMAP de la bandeja compartida en solo lectura con settings:read — estado, último error y sincronización; nunca la contraseña."
    },
    {
      "name": "Activities",
      "description": "Seguimientos (llamadas, reuniones, correos, notas, WhatsApp) en la ficha de contactos y empresas. Permisos activities:read y activities:write."
    },
    {
      "name": "Calendar events",
      "description": "Eventos del calendario con recurrencias e invitados. Permisos calendar:read y calendar:write. Los correos a invitados solo salen si se piden (sendInvitations / sendCancellations)."
    },
    {
      "name": "Notifications",
      "description": "Notificaciones in-app (la campana) del usuario del token. Lectura con notifications:read; marcar leídas con notifications:write."
    },
    {
      "name": "Exports",
      "description": "Export completo de los datos de la organización (ZIP por correo y URL firmada). Solo el propietario."
    },
    {
      "name": "Automations",
      "description": "Automatizaciones en LECTURA + BORRADOR con automations:read / automations:write. Crear y editar solo tocan el borrador; publicar, pausar, probar y ejecutar se hace en la web."
    },
    {
      "name": "Automation triggers",
      "description": "Disparadores reales de las automatizaciones, solo lectura con automations:read."
    },
    {
      "name": "Import",
      "description": "Importación de contactos por filas JSON (mismo motor que el Excel de la app, con dryRun), historial de importaciones y reversión. Permisos contacts:write (importar, revertir) y contacts:read (historial)."
    }
  ],
  "security": [
    {
      "ApiTokenBearer": []
    },
    {
      "ApiTokenHeader": []
    }
  ],
  "paths": {
    "/me": {
      "get": {
        "operationId": "getMe",
        "tags": [
          "Me"
        ],
        "summary": "Organización, usuario y permisos del token",
        "description": "Cualquier token de API válido, sin permiso especial. Responde a qué organización apunta el token, a nombre de qué usuario se emitió, sus permisos, su vencimiento y el plan efectivo con sus módulos. Úsalo antes de acciones de salida (WhatsApp, correo) cuando haya varias cuentas conectadas: equivocarse de token es enviar desde el número de otra organización. `plan` es `null` si no se pudo resolver; nunca tumba la respuesta.",
        "responses": {
          "200": {
            "description": "Identidad del token.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "organization",
                        "user",
                        "token",
                        "plan"
                      ],
                      "properties": {
                        "organization": {
                          "type": "object",
                          "required": [
                            "id",
                            "name"
                          ],
                          "properties": {
                            "id": {
                              "type": "string",
                              "description": "Id de la organización del token (ObjectId)."
                            },
                            "name": {
                              "type": [
                                "string",
                                "null"
                              ],
                              "description": "Razón social configurada en Ajustes → Organización."
                            }
                          }
                        },
                        "user": {
                          "type": "object",
                          "required": [
                            "id",
                            "email",
                            "firstName",
                            "lastName",
                            "role"
                          ],
                          "properties": {
                            "id": {
                              "type": "string"
                            },
                            "email": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "firstName": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "lastName": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "role": {
                              "type": [
                                "string",
                                "null"
                              ],
                              "enum": [
                                "propietario",
                                "empleado",
                                "lector",
                                null
                              ]
                            }
                          }
                        },
                        "token": {
                          "type": "object",
                          "required": [
                            "id",
                            "name",
                            "permissions",
                            "expiresAt",
                            "createdAt"
                          ],
                          "properties": {
                            "id": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "name": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "permissions": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              },
                              "description": "`*` = todos los permisos."
                            },
                            "expiresAt": {
                              "type": [
                                "string",
                                "null"
                              ],
                              "format": "date-time",
                              "description": "`null` = no expira."
                            },
                            "createdAt": {
                              "type": [
                                "string",
                                "null"
                              ],
                              "format": "date-time"
                            }
                          }
                        },
                        "plan": {
                          "oneOf": [
                            {
                              "type": "null"
                            },
                            {
                              "type": "object",
                              "required": [
                                "name",
                                "displayName",
                                "modules"
                              ],
                              "properties": {
                                "name": {
                                  "type": "string",
                                  "description": "free, pro, unlimited…"
                                },
                                "displayName": {
                                  "type": [
                                    "string",
                                    "null"
                                  ]
                                },
                                "modules": {
                                  "type": "object",
                                  "additionalProperties": {
                                    "type": "boolean"
                                  },
                                  "description": "Módulos del plan efectivo (incluye overrides de la organización)."
                                }
                              }
                            }
                          ]
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "me": {
                    "summary": "Token del MCP de Manillas",
                    "value": {
                      "success": true,
                      "data": {
                        "organization": {
                          "id": "659d89b73c6aa865f1e7d6fb",
                          "name": "Manillas de Control"
                        },
                        "user": {
                          "id": "659d89b73c6aa865f1e7d6f9",
                          "email": "ventas@example.com",
                          "firstName": "Ana",
                          "lastName": "Ejemplo",
                          "role": "propietario"
                        },
                        "token": {
                          "id": "66f0a1b2c3d4e5f6a7b8c9d0",
                          "name": "Claude Code",
                          "permissions": [
                            "*"
                          ],
                          "expiresAt": null,
                          "createdAt": "2026-09-09T15:04:05.000Z"
                        },
                        "plan": {
                          "name": "unlimited",
                          "displayName": "Unlimited",
                          "modules": {
                            "api": true,
                            "invoices": true
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Token ausente, inválido, expirado, revocado o sin organización válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "example": {
                  "message": "Token de API no proporcionado",
                  "code": "API_TOKEN_MISSING",
                  "details": "Proporcione el token en el header 'Authorization: Bearer <token>' o 'X-API-Key: <token>'"
                }
              }
            }
          },
          "403": {
            "description": "Permiso insuficiente (INSUFFICIENT_API_PERMISSIONS), módulo API no incluido\nen el plan si su enforcement está activo (PLAN_MODULE_REQUIRED), límite\ndel plan alcanzado al crear (PLAN_LIMIT_REACHED), o `X-Organization-Id` /\n`?organizationId=` distinto de la organización del token\n(ORGANIZATION_ID_MISMATCH).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "example": {
                  "message": "Permiso insuficiente. Se requiere: contacts:read",
                  "code": "INSUFFICIENT_API_PERMISSIONS",
                  "requiredPermission": "contacts:read",
                  "availablePermissions": []
                }
              }
            }
          },
          "500": {
            "description": "Error interno del servidor o al validar límites del plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "example": {
                  "message": "Error interno del servidor",
                  "code": "INTERNAL_SERVER_ERROR"
                }
              }
            }
          }
        }
      }
    },
    "/catalogs/municipalities": {
      "get": {
        "operationId": "listMunicipalitiesCatalog",
        "tags": [
          "Catalogs"
        ],
        "summary": "Municipios de Colombia (DANE/DIVIPOLA)",
        "description": "Catálogo oficial DANE de 1.122 municipios. En la API externa (`/api/catalogs`) basta cualquier token válido, sin permiso especial; en la interna (`/api/v1/catalogs`) se autentica con la sesión de la app (JWT/cookie), no con token de API. Sin `q` devuelve el catálogo completo (o el departamento entero con `state`); con `q` busca por prefijo o inclusión sobre el nombre oficial y el corto, sin tildes, capitales primero (por defecto 20 resultados). Úsalo para hallar el `cityCode` antes de crear o actualizar contactos y empresas: los nombres repetidos entre departamentos (Rionegro, La Unión, Villanueva…) obligan a mirar `stateName`. Respuesta cacheable un día (`Cache-Control: public`) con `ETag` por versión del catálogo: repite la llamada con `If-None-Match` y recibirás 304 mientras no cambie. La cabecera `X-Catalog-Version` trae la versión del catálogo (la misma de `version` en el cuerpo).",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "description": "Texto a buscar (\"medel\", \"cali\", \"rionegro\"). Sin tildes ni mayúsculas necesarias."
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^\\d{1,2}$"
            },
            "description": "Código DANE del departamento para acotar (\"05\" Antioquia, \"11\" Bogotá)."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1200
            },
            "description": "Máximo de filas. Por defecto 20 con `q` y el catálogo completo sin `q`."
          }
        ],
        "responses": {
          "200": {
            "description": "Municipios encontrados.",
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                },
                "description": "Versión del catálogo (+ hash de los filtros)."
              },
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "example": "public, max-age=86400"
              },
              "X-Catalog-Version": {
                "schema": {
                  "type": "string"
                },
                "description": "Versión del catálogo DANE; cambia solo cuando se regenera.",
                "example": "divipola-2024-12-30.b17287ee"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "version",
                    "count",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "version": {
                      "type": "string",
                      "description": "Versión del catálogo (misma que el ETag). Cambia solo cuando se regenera.",
                      "example": "divipola-2024-12-30.b17287ee"
                    },
                    "count": {
                      "type": "integer",
                      "example": 2
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "code",
                          "name",
                          "shortName",
                          "stateCode",
                          "stateName",
                          "stateIso2",
                          "dane8"
                        ],
                        "properties": {
                          "code": {
                            "type": "string",
                            "pattern": "^\\d{5}$",
                            "description": "Código DANE de municipio (5 dígitos). Es el valor de `cityCode` en contactos y empresas.",
                            "example": "76001"
                          },
                          "name": {
                            "type": "string",
                            "description": "Nombre oficial DIVIPOLA.",
                            "example": "Santiago de Cali"
                          },
                          "shortName": {
                            "type": "string",
                            "description": "Nombre corto de uso común; es lo que el CRM muestra y escribe en `city`.",
                            "example": "Cali"
                          },
                          "stateCode": {
                            "type": "string",
                            "pattern": "^\\d{2}$",
                            "description": "Código DANE del departamento.",
                            "example": "76"
                          },
                          "stateName": {
                            "type": "string",
                            "example": "Valle del Cauca"
                          },
                          "stateIso2": {
                            "type": "string",
                            "description": "Código ISO 3166-2:CO del departamento sin el prefijo CO- (ANT, DC, VAC).",
                            "example": "VAC"
                          },
                          "dane8": {
                            "type": "string",
                            "pattern": "^\\d{8}$",
                            "description": "Código DANE de centro poblado (cabecera) = code + \"000\".",
                            "example": "76001000"
                          }
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "rionegro": {
                    "summary": "Homónimo — siempre mirar el departamento",
                    "value": {
                      "success": true,
                      "version": "divipola-2024-12-30.b17287ee",
                      "count": 2,
                      "data": [
                        {
                          "code": "05615",
                          "name": "Rionegro",
                          "shortName": "Rionegro",
                          "stateCode": "05",
                          "stateName": "Antioquia",
                          "stateIso2": "ANT",
                          "dane8": "05615000"
                        },
                        {
                          "code": "68615",
                          "name": "Rionegro",
                          "shortName": "Rionegro",
                          "stateCode": "68",
                          "stateName": "Santander",
                          "stateIso2": "SAN",
                          "dane8": "68615000"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "304": {
            "description": "El cliente ya tiene esta versión (`If-None-Match` coincide). Sin cuerpo."
          },
          "400": {
            "description": "`state` no es un departamento válido (STATE_UNKNOWN) o `limit` está fuera de rango (LIMIT_INVALID).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "message",
                    "code"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": false
                    },
                    "message": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "STATE_UNKNOWN",
                        "LIMIT_INVALID"
                      ]
                    }
                  }
                },
                "example": {
                  "success": false,
                  "message": "Departamento desconocido: 99x. Use el código DANE de 2 dígitos (p. ej. \"05\"); GET /catalogs/states los lista.",
                  "code": "STATE_UNKNOWN"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/catalogs/states": {
      "get": {
        "operationId": "listStatesCatalog",
        "tags": [
          "Catalogs"
        ],
        "summary": "Departamentos de Colombia (DANE)",
        "description": "Los 33 departamentos (Bogotá D.C. cuenta como uno, código 11) con su código DANE de 2 dígitos, nombre oficial, nombre corto e ISO 3166-2. API externa: cualquier token, sin permiso especial; interna: sesión de la app. Mismo cache/ETag/`X-Catalog-Version` que municipios.",
        "responses": {
          "200": {
            "description": "Departamentos.",
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "example": "public, max-age=86400"
              },
              "X-Catalog-Version": {
                "schema": {
                  "type": "string"
                },
                "description": "Versión del catálogo DANE.",
                "example": "divipola-2024-12-30.b17287ee"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "version",
                    "count",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "version": {
                      "type": "string",
                      "example": "divipola-2024-12-30.b17287ee"
                    },
                    "count": {
                      "type": "integer",
                      "example": 33
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "code",
                          "name",
                          "shortName",
                          "iso2"
                        ],
                        "properties": {
                          "code": {
                            "type": "string",
                            "pattern": "^\\d{2}$",
                            "example": "05"
                          },
                          "name": {
                            "type": "string",
                            "description": "Nombre oficial DANE.",
                            "example": "Antioquia"
                          },
                          "shortName": {
                            "type": "string",
                            "description": "Nombre corto para selects (\"Bogotá D.C.\", \"San Andrés y Providencia\").",
                            "example": "Antioquia"
                          },
                          "iso2": {
                            "type": "string",
                            "description": "ISO 3166-2:CO sin el prefijo CO-.",
                            "example": "ANT"
                          }
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "states": {
                    "summary": "Primeros departamentos",
                    "value": {
                      "success": true,
                      "version": "divipola-2024-12-30.b17287ee",
                      "count": 33,
                      "data": [
                        {
                          "code": "05",
                          "name": "Antioquia",
                          "shortName": "Antioquia",
                          "iso2": "ANT"
                        },
                        {
                          "code": "08",
                          "name": "Atlántico",
                          "shortName": "Atlántico",
                          "iso2": "ATL"
                        },
                        {
                          "code": "11",
                          "name": "Bogotá, D.C.",
                          "shortName": "Bogotá D.C.",
                          "iso2": "DC"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "304": {
            "description": "El cliente ya tiene esta versión. Sin cuerpo."
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/contacts": {
      "get": {
        "operationId": "listContacts",
        "tags": [
          "Contacts"
        ],
        "summary": "Listar contactos",
        "description": "Requiere contacts:read o *. Filtra por la organización del API Token. Los parámetros de query no reservados se interpretan como filtros de properties: por ejemplo city=Medellín o email=ana@example.com. Repetir una clave produce OR entre sus valores; claves diferentes se combinan con AND. Las búsquedas son literales (regex escapada). Enviar solo parámetros reconocidos o claves de properties.",
        "x-required-permissions": [
          "contacts:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            },
            "description": "Página de resultados, desde 1."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            },
            "description": "Cantidad por página; el servidor acota valores numéricos entre 1 y 1000."
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Coincidencia parcial sin distinguir mayúsculas en nombre, apellido, email, mobile, phone y empresa.",
            "example": "Ana"
          },
          {
            "name": "matchMode",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "contains",
                "exact"
              ],
              "default": "contains"
            },
            "description": "Modo de filtros por property; exact distingue mayúsculas. No afecta search."
          },
          {
            "name": "sortBy",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "createdAt",
                "updatedAt"
              ],
              "default": "createdAt"
            },
            "description": "Campo de ordenamiento."
          },
          {
            "name": "sortOrder",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            },
            "description": "Dirección de ordenamiento."
          },
          {
            "name": "createdFrom",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Fecha mínima inclusiva de creación (ISO 8601; también admite YYYY-MM-DD)."
          },
          {
            "name": "createdTo",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Fecha máxima inclusiva; YYYY-MM-DD se interpreta a medianoche UTC, no al fin del día."
          },
          {
            "name": "missing",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Claves separadas por coma que deben faltar o no tener ningún valor distinto de vacío/null.",
            "example": "email,city"
          },
          {
            "name": "exists",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Claves separadas por coma que deben tener al menos un valor no vacío/null.",
            "example": "mobile"
          },
          {
            "name": "city",
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "style": "form",
            "explode": true,
            "description": "Ejemplo de filtro por property; también admite cualquier otra clave no reservada.",
            "example": [
              "Medellín",
              "Bogotá"
            ]
          },
          {
            "name": "cityCode",
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "pattern": "^\\d{5}$"
              }
            },
            "style": "form",
            "explode": true,
            "description": "Filtro por código DANE del municipio (5 dígitos, Épica Y). Usar con matchMode=exact. Para todo un departamento usar stateCode. Repetir la clave produce OR.",
            "example": [
              "05001",
              "76001"
            ]
          },
          {
            "name": "stateCode",
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "pattern": "^\\d{2}$"
              }
            },
            "style": "form",
            "explode": true,
            "description": "Filtro por código DANE del departamento (2 dígitos); trae todos los municipios del departamento. Usar con matchMode=exact.",
            "example": [
              "05"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Contactos y paginación; data puede estar vacío.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactListResult"
                },
                "examples": {
                  "list": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "507f1f77bcf86cd799439011",
                          "firstName": "Ana",
                          "lastName": "Ejemplo",
                          "fullName": "Ana Ejemplo",
                          "company": "",
                          "companyId": null,
                          "email": "ana@example.com",
                          "mobile": "573000000000",
                          "leadScore": 0,
                          "attribution": {
                            "firstTouchAt": "2026-09-14T14:20:00.000Z",
                            "channel": "web",
                            "utmSource": "google",
                            "utmMedium": "cpc",
                            "utmCampaign": "manillas-eventos",
                            "utmTerm": null,
                            "utmContent": null,
                            "referrer": "https://www.google.com/",
                            "landingPage": "https://www.manillasdecontrol.com/manillas-sublimadas",
                            "gclid": "Cj0KCQjw-ejemplo",
                            "fbclid": null,
                            "formId": "507f1f77bcf86cd799439020",
                            "landingPageId": null,
                            "landingPageName": null
                          },
                          "tags": [
                            "web",
                            "cotizacion"
                          ],
                          "createdAt": "2026-09-05T15:00:00.000Z",
                          "updatedAt": "2026-09-05T15:00:00.000Z"
                        }
                      ],
                      "pagination": {
                        "total": 1,
                        "page": 1,
                        "limit": 100,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "post": {
        "operationId": "createContact",
        "tags": [
          "Contacts"
        ],
        "summary": "Crear o actualizar un contacto por email o móvil",
        "description": "Requiere contacts:write o *. Busca primero email exacto sin espacios exteriores (distingue mayúsculas), y después mobile normalizado a dígitos. Si ya existe, ACTUALIZA sus datos y devuelve 200 con contact como documento sin aplanar. Si no existe, crea y devuelve 201 con data aplanada. No devuelve duplicate:true. El límite de contactos del plan se verifica antes del dedupe y puede impedir incluso actualizar un contacto existente por este endpoint. Ubicación (Épica Y, modo estricto): cityCode válido o city que resuelva sin ambigüedad contra el catálogo DANE; si no, 422 CITY_UNRESOLVED con candidates (400 INVALID_CITY_CODE para un cityCode inexistente) y no se escribe nada. Sin city/cityCode/state en el body la ubicación no se toca; con country distinto de Colombia se guarda como texto libre.",
        "x-required-permissions": [
          "contacts:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactCreate"
              },
              "examples": {
                "email": {
                  "summary": "Dedupe por email (escenario de contactQueryScoping.test.ts con datos ficticios).",
                  "value": {
                    "email": " ana@example.com ",
                    "first_name": "Ana",
                    "last_name": "Ejemplo"
                  }
                },
                "mobile": {
                  "summary": "Crear por teléfono normalizado.",
                  "value": {
                    "whatsapp_phone": "+57 300 000-0000",
                    "first_name": "Ana",
                    "lastName": "Ejemplo",
                    "email": "ana@example.com"
                  }
                },
                "company": {
                  "summary": "Vincular una empresa existente de la organización.",
                  "value": {
                    "email": "ana@example.com",
                    "companyId": "507f1f77bcf86cd799439013",
                    "city": "Medellín"
                  }
                },
                "cityCode": {
                  "summary": "Ubicación por código DANE (deriva city, state y stateCode).",
                  "value": {
                    "email": "ana@example.com",
                    "first_name": "Ana",
                    "cityCode": "76001"
                  }
                },
                "cityWithState": {
                  "summary": "Ciudad homónima desambiguada con el departamento.",
                  "value": {
                    "email": "ana@example.com",
                    "city": "Rionegro",
                    "state": "Santander"
                  }
                },
                "linkedConversation": {
                  "summary": "Lead de Instagram/Messenger sin teléfono ni email, enlazado a su conversación.",
                  "value": {
                    "first_name": "ana.perez",
                    "linkedConversationId": "66f1d0a2b3c4d5e6f7a80912"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Contacto existente actualizado; envelope distinto del 201.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactUpsertResult"
                },
                "examples": {
                  "updated": {
                    "summary": "Contacto existente actualizado, conservando el formato legacy de respuesta.",
                    "value": {
                      "message": "Contact updated successfully",
                      "contact": {
                        "_id": "507f1f77bcf86cd799439011",
                        "organizationId": "507f1f77bcf86cd799439012",
                        "properties": [
                          {
                            "key": "firstName",
                            "value": "Ana",
                            "isVisible": true
                          },
                          {
                            "key": "email",
                            "value": "ana@example.com",
                            "isVisible": true
                          }
                        ],
                        "leadScore": 0,
                        "createdAt": "2026-09-05T15:00:00.000Z",
                        "updatedAt": "2026-09-05T16:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Nuevo contacto creado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactCreatedResult"
                },
                "examples": {
                  "created": {
                    "value": {
                      "success": true,
                      "message": "Contact created successfully",
                      "data": {
                        "id": "507f1f77bcf86cd799439011",
                        "firstName": "Ana",
                        "lastName": "Ejemplo",
                        "fullName": "Ana Ejemplo",
                        "company": "",
                        "companyId": null,
                        "email": "ana@example.com",
                        "mobile": "573000000000",
                        "city": "Medellín",
                        "cityCode": "05001",
                        "state": "Antioquia",
                        "stateCode": "05",
                        "leadScore": 0,
                        "attribution": null,
                        "tags": [],
                        "createdAt": "2026-09-05T15:00:00.000Z",
                        "updatedAt": "2026-09-05T15:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Falta email/móvil, la empresa indicada es inválida/no pertenece a la organización, o cityCode no existe en el catálogo DANE (INVALID_CITY_CODE, cuerpo de CityUnresolvedError), o idNumber inválido para su tipo (INVALID_ID_NUMBER, cuerpo de InvalidIdNumberError).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactError"
                },
                "examples": {
                  "missingIdentity": {
                    "value": {
                      "error": "Either whatsapp_phone",
                      "mobile": null,
                      "email or linkedConversationId is required": null
                    }
                  },
                  "invalidCompany": {
                    "value": {
                      "success": false,
                      "error": "Invalid companyId"
                    }
                  },
                  "companyNotFound": {
                    "value": {
                      "success": false,
                      "error": "Company not found or does not belong to this organization"
                    }
                  },
                  "invalidCityCode": {
                    "summary": "cityCode que no existe en el catálogo DANE (400).",
                    "value": {
                      "success": false,
                      "error": "INVALID_CITY_CODE",
                      "code": "INVALID_CITY_CODE",
                      "message": "cityCode \"99999\" no existe en el catálogo DANE",
                      "input": "99999",
                      "status": "invalid_code",
                      "candidates": []
                    }
                  },
                  "invalidIdNumber": {
                    "summary": "idNumber inválido para su tipo (400 INVALID_ID_NUMBER, Épica Z).",
                    "value": {
                      "success": false,
                      "error": "INVALID_ID_NUMBER",
                      "code": "INVALID_ID_NUMBER",
                      "message": "El número de documento \"1.020.304.050-1\" no es válido: solo el NIT lleva dígito de verificación.",
                      "field": "idNumber"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "description": "Permiso insuficiente, módulo API no habilitado o límite de contactos del plan alcanzado (PLAN_LIMIT_REACHED).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message",
                    "code"
                  ],
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "INSUFFICIENT_API_PERMISSIONS",
                        "PLAN_MODULE_REQUIRED",
                        "PLAN_LIMIT_REACHED"
                      ]
                    },
                    "requiredPermission": {
                      "type": "string"
                    },
                    "availablePermissions": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "module": {
                      "type": "string"
                    },
                    "details": {
                      "type": "object",
                      "additionalProperties": true
                    }
                  }
                },
                "example": {
                  "message": "Has alcanzado el límite de tu plan. Actualiza tu plan para continuar.",
                  "code": "PLAN_LIMIT_REACHED"
                }
              }
            }
          },
          "404": {
            "description": "linkedConversationId no existe o es de otra organización (CONVERSATION_NOT_FOUND). No se escribe nada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactError"
                },
                "example": {
                  "success": false,
                  "error": "CONVERSATION_NOT_FOUND",
                  "code": "CONVERSATION_NOT_FOUND",
                  "message": "La conversación no existe o no pertenece a la organización",
                  "field": "linkedConversationId"
                }
              }
            }
          },
          "409": {
            "description": "La conversación de linkedConversationId ya tiene otro contacto enlazado (CONVERSATION_ALREADY_LINKED). No se escribe nada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactError"
                },
                "example": {
                  "success": false,
                  "error": "CONVERSATION_ALREADY_LINKED",
                  "code": "CONVERSATION_ALREADY_LINKED",
                  "message": "La conversación ya tiene otro contacto enlazado",
                  "field": "linkedConversationId",
                  "contactId": "507f1f77bcf86cd799439011"
                }
              }
            }
          },
          "422": {
            "description": "La ciudad o el tipo de documento no resuelven sin ambigüedad. CITY_UNRESOLVED (Épica Y): reenviar con cityCode eligiendo uno de candidates (GET /catalogs/municipalities?q=). ID_TYPE_UNRESOLVED (Épica Z): reenviar idType con el código DIAN (candidates, si viene, trae los posibles). No se escribe nada.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "description": "Ubicación no resuelta contra el catálogo DANE (Épica Y, modo estricto de la API pública, empresas y formularios). error y code llevan el mismo valor: CITY_UNRESOLVED (422) cuando city es ambigua, está mal escrita o no existe; INVALID_CITY_CODE (400) cuando cityCode no está en el catálogo. candidates trae los municipios posibles (vacío si no hay ninguno parecido); el cliente debe reenviar con cityCode. Nunca se asigna un \"mejor candidato\".",
                      "required": [
                        "success",
                        "error",
                        "code",
                        "message",
                        "input",
                        "status",
                        "candidates"
                      ],
                      "properties": {
                        "success": {
                          "type": "boolean",
                          "const": false
                        },
                        "error": {
                          "type": "string",
                          "enum": [
                            "CITY_UNRESOLVED",
                            "INVALID_CITY_CODE"
                          ]
                        },
                        "code": {
                          "type": "string",
                          "enum": [
                            "CITY_UNRESOLVED",
                            "INVALID_CITY_CODE"
                          ]
                        },
                        "message": {
                          "type": "string"
                        },
                        "input": {
                          "type": "string",
                          "description": "El city o cityCode recibido."
                        },
                        "status": {
                          "type": "string",
                          "description": "Resultado del resolver; ambiguous = varios municipios, fuzzy = parecido pero no exacto, none = nada parecido, invalid_code = cityCode inexistente.",
                          "enum": [
                            "ambiguous",
                            "fuzzy",
                            "none",
                            "invalid_code"
                          ]
                        },
                        "candidates": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/FormError/properties/candidates/items"
                          }
                        }
                      }
                    },
                    {
                      "type": "object",
                      "description": "Tipo de documento no traducible a un código DIAN (Épica Z, contrato §e.1; modo estricto de la API pública, formularios y agente de IA). Solo se evalúa el idType que trae la petición: un valor viejo ya guardado no tumba un PUT que no lo envía. candidates trae los códigos DIAN posibles cuando el valor es ambiguo (\"1\" → [\"11\",\"13\"], \"11\" → [\"11\",\"47\"], \"12\" → [\"12\",\"48\"]) y se omite cuando no hay pista. El cliente debe reenviar con el código DIAN.",
                      "required": [
                        "success",
                        "error",
                        "code",
                        "message",
                        "input"
                      ],
                      "properties": {
                        "success": {
                          "type": "boolean",
                          "const": false
                        },
                        "error": {
                          "type": "string",
                          "const": "ID_TYPE_UNRESOLVED"
                        },
                        "code": {
                          "type": "string",
                          "const": "ID_TYPE_UNRESOLVED"
                        },
                        "message": {
                          "type": "string"
                        },
                        "input": {
                          "type": "string",
                          "description": "El idType recibido."
                        },
                        "candidates": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "description": "Código DIAN posible (13 cédula",
                            "31 NIT": null,
                            "…).": null
                          }
                        }
                      }
                    }
                  ]
                },
                "examples": {
                  "cityAmbiguous": {
                    "summary": "Rionegro existe en Antioquia y en Santander.",
                    "value": {
                      "success": false,
                      "error": "CITY_UNRESOLVED",
                      "code": "CITY_UNRESOLVED",
                      "message": "La ciudad \"Rionegro\" corresponde a varios municipios; indique cityCode o el departamento",
                      "input": "Rionegro",
                      "status": "ambiguous",
                      "candidates": [
                        {
                          "code": "05615",
                          "shortName": "Rionegro",
                          "stateName": "Antioquia",
                          "stateCode": "05"
                        },
                        {
                          "code": "68615",
                          "shortName": "Rionegro",
                          "stateName": "Santander",
                          "stateCode": "68"
                        }
                      ]
                    }
                  },
                  "idTypeAmbiguous": {
                    "summary": "\"1\" puede ser Registro civil (11) o el CC casero de una org (13).",
                    "value": {
                      "success": false,
                      "error": "ID_TYPE_UNRESOLVED",
                      "code": "ID_TYPE_UNRESOLVED",
                      "message": "El tipo de documento \"1\" es ambiguo: puede ser 11 o 13. Envía el código DIAN (13 cédula, 31 NIT, …).",
                      "input": "1",
                      "candidates": [
                        "11",
                        "13"
                      ]
                    }
                  },
                  "idTypeUnknown": {
                    "summary": "Valor sin pista; se omite candidates.",
                    "value": {
                      "success": false,
                      "error": "ID_TYPE_UNRESOLVED",
                      "code": "ID_TYPE_UNRESOLVED",
                      "message": "No se reconoce el tipo de documento \"carnet\". Envía el código DIAN (13 cédula, 31 NIT, 41 pasaporte, …).",
                      "input": "carnet"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/contacts/search": {
      "get": {
        "operationId": "searchContactByPhone",
        "tags": [
          "Contacts"
        ],
        "summary": "Buscar un contacto por teléfono",
        "description": "Requiere contacts:read o *. Normaliza phone a dígitos y busca coincidencia exacta o parcial en mobile y phone; devuelve un solo contacto, sin ordenar coincidencias. La búsqueda se hace siempre en la organización del token. El parámetro organizationId es opcional y se conserva por compatibilidad: si se envía y no coincide con la del token responde 403 ORGANIZATION_ID_MISMATCH. Su respuesta usa contact y properties como objeto, no data ni properties como array.",
        "x-required-permissions": [
          "contacts:read"
        ],
        "parameters": [
          {
            "name": "organizationId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Opcional. Si se envía, debe ser la organización del token.",
            "example": "507f1f77bcf86cd799439012"
          },
          {
            "name": "phone",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 3
            },
            "description": "Teléfono con al menos tres dígitos después de normalizar; se permiten espacios y símbolos.",
            "example": "+57 300 000-0000"
          }
        ],
        "responses": {
          "200": {
            "description": "Primer contacto coincidente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactSearchResult"
                },
                "examples": {
                  "found": {
                    "value": {
                      "success": true,
                      "contact": {
                        "id": "507f1f77bcf86cd799439011",
                        "organizationId": "507f1f77bcf86cd799439012",
                        "properties": {
                          "firstName": "Ana",
                          "email": "ana@example.com",
                          "mobile": "573000000000"
                        },
                        "firstName": "Ana",
                        "lastName": "",
                        "fullName": "Ana",
                        "email": "ana@example.com",
                        "mobile": "573000000000",
                        "phone": "",
                        "company": "",
                        "position": "",
                        "leadScore": 0,
                        "attribution": {
                          "firstTouchAt": "2026-09-14T14:20:00.000Z",
                          "channel": "web",
                          "utmSource": "google",
                          "utmMedium": "cpc",
                          "utmCampaign": "manillas-eventos",
                          "utmTerm": null,
                          "utmContent": null,
                          "referrer": "https://www.google.com/",
                          "landingPage": "https://www.manillasdecontrol.com/manillas-sublimadas",
                          "gclid": "Cj0KCQjw-ejemplo",
                          "fbclid": null,
                          "formId": "507f1f77bcf86cd799439020",
                          "landingPageId": null,
                          "landingPageName": null
                        },
                        "tags": [
                          "web",
                          "cotizacion"
                        ],
                        "createdAt": "2026-09-05T15:00:00.000Z",
                        "updatedAt": "2026-09-05T15:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Falta phone o contiene menos de tres dígitos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactError"
                },
                "examples": {
                  "phone": {
                    "value": {
                      "success": false,
                      "error": "Phone number is required"
                    }
                  },
                  "shortPhone": {
                    "value": {
                      "success": false,
                      "error": "Phone number must have at least 3 characters"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Ningún contacto coincide con el teléfono.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactError"
                },
                "example": {
                  "success": false,
                  "error": "Contact not found",
                  "message": "No contact found with phone number: 573000000000"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/contacts/advanced-filter": {
      "post": {
        "operationId": "advancedFilterContacts",
        "tags": [
          "Contacts"
        ],
        "summary": "Filtrar contactos por properties y negocios asociados",
        "description": "Requiere contacts:read o *. Consulta de solo lectura aunque usa POST. Contacts es obligatorio (puede estar vacío). Devuelve _id, properties y hasDeals, con un envelope sin success. Los filtros de negocio regulares determinan hasDeals; añade el filtro hasDeals=true para excluir contactos sin negocio coincidente. La paginación se aplica después de calcular los resultados en memoria.",
        "x-required-permissions": [
          "contacts:read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AdvancedFilterRequest"
              },
              "examples": {
                "cityAndDeals": {
                  "value": {
                    "Contacts": [
                      {
                        "field": "city",
                        "condition": "equals",
                        "value": "Medellín"
                      }
                    ],
                    "Deals": [
                      {
                        "field": "amount",
                        "condition": "greater_than",
                        "value": 100000
                      },
                      {
                        "field": "hasDeals",
                        "value": true
                      }
                    ],
                    "page": 1,
                    "limit": 50
                  }
                },
                "all": {
                  "value": {
                    "Contacts": [],
                    "page": 1,
                    "limit": 50
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Contactos filtrados y paginación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdvancedFilterResult"
                },
                "examples": {
                  "filtered": {
                    "value": {
                      "data": [
                        {
                          "_id": "507f1f77bcf86cd799439011",
                          "properties": {
                            "firstName": "Ana",
                            "email": "ana@example.com",
                            "city": "Medellín"
                          },
                          "hasDeals": true
                        }
                      ],
                      "pagination": {
                        "total": 1,
                        "page": 1,
                        "limit": 50,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Contacts falta o no es un array.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message"
                  ],
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "message": "Faltan los filtros de contactos o formato incorrecto"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "description": "Error aplicando los filtros o durante la autenticación.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message"
                  ],
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "error": {
                      "description": "Error serializado por el handler; normalmente un objeto vacío.",
                      "type": [
                        "object",
                        "string"
                      ]
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "message": "Error en el servidor",
                  "error": {}
                }
              }
            }
          }
        }
      }
    },
    "/contacts/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB del contacto.",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "507f1f77bcf86cd799439011"
        }
      ],
      "get": {
        "operationId": "getContactById",
        "tags": [
          "Contacts"
        ],
        "summary": "Consultar un contacto por ID",
        "description": "Requiere contacts:read o *. El contacto debe pertenecer a la organización del token.",
        "x-required-permissions": [
          "contacts:read"
        ],
        "responses": {
          "200": {
            "description": "Contacto encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactResult"
                },
                "examples": {
                  "found": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439011",
                        "firstName": "Ana",
                        "lastName": "Ejemplo",
                        "fullName": "Ana Ejemplo",
                        "company": "",
                        "companyId": null,
                        "email": "ana@example.com",
                        "city": "Medellín",
                        "leadScore": 0,
                        "attribution": null,
                        "tags": [],
                        "createdAt": "2026-09-05T15:00:00.000Z",
                        "updatedAt": "2026-09-05T16:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID de contacto inválido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactError"
                },
                "example": {
                  "success": false,
                  "error": "Invalid contact ID"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Contacto inexistente o no perteneciente a la organización del token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactError"
                },
                "example": {
                  "success": false,
                  "error": "Contact not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "put": {
        "operationId": "updateContactById",
        "tags": [
          "Contacts"
        ],
        "summary": "Actualizar parcialmente un contacto por ID",
        "description": "Requiere contacts:write o *. Actualiza un contacto de la organización del token. Ubicación en modo estricto como en POST: city/cityCode deben resolver contra el catálogo DANE (422 CITY_UNRESOLVED con candidates / 400 INVALID_CITY_CODE); sin ellos en el body la ubicación guardada no cambia.",
        "x-required-permissions": [
          "contacts:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactUpdate"
              },
              "examples": {
                "city": {
                  "value": {
                    "city": "Medellín",
                    "notes": "Contactar la próxima semana"
                  }
                },
                "cityCode": {
                  "summary": "Corregir la ubicación por código DANE.",
                  "value": {
                    "cityCode": "05615"
                  }
                },
                "unlink": {
                  "summary": "Desvincular la empresa sin borrar su texto.",
                  "value": {
                    "companyId": null
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Contacto actualizado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactResult"
                },
                "examples": {
                  "updated": {
                    "$ref": "#/paths/~1contacts~1{id}/get/responses/200/content/application~1json/examples/found"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID de contacto inválido, empresa inválida/no perteneciente a la organización, cityCode inexistente (INVALID_CITY_CODE) o idNumber inválido para su tipo (INVALID_ID_NUMBER).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactError"
                },
                "examples": {
                  "invalidContact": {
                    "value": {
                      "success": false,
                      "error": "Invalid contact ID"
                    }
                  },
                  "invalidCompany": {
                    "value": {
                      "success": false,
                      "error": "Invalid companyId"
                    }
                  },
                  "missingCompany": {
                    "value": {
                      "success": false,
                      "error": "Company not found or does not belong to this organization"
                    }
                  },
                  "invalidCityCode": {
                    "$ref": "#/paths/~1contacts/post/responses/400/content/application~1json/examples/invalidCityCode"
                  },
                  "invalidIdNumber": {
                    "$ref": "#/paths/~1contacts/post/responses/400/content/application~1json/examples/invalidIdNumber"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1contacts~1{id}/get/responses/404"
          },
          "422": {
            "$ref": "#/paths/~1contacts/post/responses/422"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "delete": {
        "operationId": "deleteContactById",
        "tags": [
          "Contacts"
        ],
        "summary": "Eliminar un contacto",
        "description": "Requiere contacts:delete o *. Elimina el documento del contacto dentro de la organización del token.",
        "x-required-permissions": [
          "contacts:delete"
        ],
        "responses": {
          "200": {
            "description": "Contacto eliminado; no responde 204.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactDeleteResult"
                },
                "examples": {
                  "deleted": {
                    "value": {
                      "success": true,
                      "message": "Contact deleted successfully",
                      "data": {
                        "id": "507f1f77bcf86cd799439011"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1contacts~1{id}/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1contacts~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/companies": {
      "get": {
        "operationId": "listCompanies",
        "tags": [
          "Companies"
        ],
        "summary": "Listar empresas",
        "description": "Requiere companies:read o *. Filtra por la organización del API Token. Los parámetros de query no reservados se interpretan como filtros de properties (industry=Tecnología, city=Medellín, cityCode=05001 con matchMode=exact, stateCode=05 para todo un departamento). Repetir una clave produce OR entre sus valores; claves diferentes se combinan con AND. Búsquedas literales (regex escapada).",
        "x-required-permissions": [
          "companies:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            },
            "description": "El servidor acota entre 1 y 1000."
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Coincidencia parcial sin distinguir mayúsculas en name, industry, email, phone, website, city e idNumber.",
            "example": "Microm"
          },
          {
            "name": "matchMode",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "contains",
                "exact"
              ],
              "default": "contains"
            },
            "description": "Modo de los filtros por property; exact distingue mayúsculas. No afecta search."
          },
          {
            "name": "sortBy",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "createdAt",
                "updatedAt"
              ],
              "default": "createdAt"
            }
          },
          {
            "name": "sortOrder",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          },
          {
            "name": "createdFrom",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Fecha mínima inclusiva de creación (ISO 8601 o YYYY-MM-DD)."
          },
          {
            "name": "createdTo",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Fecha máxima inclusiva; YYYY-MM-DD se interpreta a medianoche UTC, no al fin del día."
          },
          {
            "name": "industry",
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "style": "form",
            "explode": true,
            "description": "Ejemplo de filtro por property; admite cualquier otra clave no reservada.",
            "example": [
              "Tecnología"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Empresas y paginación; data puede estar vacío.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyListResult"
                },
                "examples": {
                  "list": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "507f1f77bcf86cd799439013",
                          "name": "Microm S.A.S",
                          "idNumber": "900123456",
                          "industry": "Tecnología",
                          "createdAt": "2026-09-05T15:00:00.000Z",
                          "updatedAt": "2026-09-05T15:00:00.000Z"
                        },
                        {
                          "id": "507f1f77bcf86cd799439014",
                          "name": "Acme (Corp)",
                          "createdAt": "2026-09-04T15:00:00.000Z",
                          "updatedAt": "2026-09-04T15:00:00.000Z"
                        }
                      ],
                      "pagination": {
                        "total": 2,
                        "page": 1,
                        "limit": 100,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "post": {
        "operationId": "createCompany",
        "tags": [
          "Companies"
        ],
        "summary": "Crear o actualizar una empresa por idNumber o nombre",
        "description": "Requiere companies:write o *. Upsert: busca primero por idNumber exacto (NIT) y después por name exacto sin distinguir mayúsculas. Si existe, hace merge de las properties recibidas y responde 200; si no, crea y responde 201. Ambas ramas devuelven el mismo envelope aplanado. El límite de empresas del plan se verifica antes del dedupe y puede impedir incluso actualizar una existente. La creación emite el evento company.created (trigger de automatizaciones); la actualización no.",
        "x-required-permissions": [
          "companies:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompanyCreate"
              },
              "examples": {
                "nit": {
                  "summary": "Dedupe por NIT (escenario de companiesApi.test.ts con datos ficticios).",
                  "value": {
                    "name": "Microm S.A.S",
                    "nit": "900123456",
                    "phone": "+57 300 000-0000",
                    "industry": "Tecnología"
                  }
                },
                "byName": {
                  "summary": "Solo nombre; aliases en español.",
                  "value": {
                    "companyName": "Acme (Corp)",
                    "ciudad": "Bogotá",
                    "pais": "Colombia"
                  }
                },
                "cityCode": {
                  "summary": "Ubicación por código DANE (deriva city, state y stateCode).",
                  "value": {
                    "name": "Acme Cali",
                    "cityCode": "76001"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Empresa existente actualizada (merge por key).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyResult"
                },
                "examples": {
                  "updated": {
                    "summary": "Coincidencia por idNumber; se hizo merge de city y se devolvió 200.",
                    "value": {
                      "success": true,
                      "message": "Company updated successfully",
                      "data": {
                        "id": "507f1f77bcf86cd799439013",
                        "name": "Microm S.A.S",
                        "idNumber": "900123456",
                        "phone": "573000000000",
                        "industry": "Tecnología",
                        "city": "Medellín",
                        "createdAt": "2026-09-05T15:00:00.000Z",
                        "updatedAt": "2026-09-05T16:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Empresa nueva creada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyResult"
                },
                "examples": {
                  "created": {
                    "value": {
                      "success": true,
                      "message": "Company created successfully",
                      "data": {
                        "id": "507f1f77bcf86cd799439013",
                        "name": "Microm S.A.S",
                        "idNumber": "900123456",
                        "phone": "573000000000",
                        "industry": "Tecnología",
                        "createdAt": "2026-09-05T15:00:00.000Z",
                        "updatedAt": "2026-09-05T15:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Falta name e idNumber, cityCode inexistente (INVALID_CITY_CODE) o idNumber inválido para su tipo (INVALID_ID_NUMBER).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyError"
                },
                "examples": {
                  "missingIdentity": {
                    "value": {
                      "success": false,
                      "error": "Either name or idNumber is required"
                    }
                  },
                  "invalidCityCode": {
                    "summary": "cityCode que no existe en el catálogo DANE (400).",
                    "value": {
                      "success": false,
                      "error": "INVALID_CITY_CODE",
                      "code": "INVALID_CITY_CODE",
                      "message": "cityCode \"99999\" no existe en el catálogo DANE",
                      "input": "99999",
                      "status": "invalid_code",
                      "candidates": []
                    }
                  },
                  "invalidIdNumber": {
                    "summary": "idNumber inválido para su tipo (400 INVALID_ID_NUMBER, Épica Z).",
                    "value": {
                      "success": false,
                      "error": "INVALID_ID_NUMBER",
                      "code": "INVALID_ID_NUMBER",
                      "message": "El número de documento \"1.020.304.050-1\" no es válido: solo el NIT lleva dígito de verificación.",
                      "field": "idNumber"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "description": "Permiso insuficiente, módulo API no habilitado o límite de empresas del plan alcanzado (PLAN_LIMIT_REACHED).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "example": {
                  "message": "Has alcanzado el límite de tu plan. Actualiza tu plan para continuar.",
                  "code": "PLAN_LIMIT_REACHED"
                }
              }
            }
          },
          "422": {
            "$ref": "#/paths/~1contacts/post/responses/422"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/companies/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB de la empresa.",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "507f1f77bcf86cd799439013"
        }
      ],
      "get": {
        "operationId": "getCompanyById",
        "tags": [
          "Companies"
        ],
        "summary": "Consultar una empresa con sus contactos, negocios y resumen",
        "description": "Requiere companies:read o *. Devuelve la empresa aplanada más contacts (vinculados por companyId), deals (associatedCompanyId, ordenados por closingDate descendente) y un resume con totalRevenue (suma de amount de todos los negocios, ganados o no), totalDeals, totalContacts y lastDeal (mayor createdAt).",
        "x-required-permissions": [
          "companies:read"
        ],
        "responses": {
          "200": {
            "description": "Empresa encontrada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyDetailResult"
                },
                "examples": {
                  "found": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439013",
                        "name": "Microm S.A.S",
                        "idNumber": "900123456",
                        "createdAt": "2026-09-05T15:00:00.000Z",
                        "updatedAt": "2026-09-05T15:00:00.000Z",
                        "contacts": [
                          {
                            "id": "507f1f77bcf86cd799439011",
                            "firstName": "Ana",
                            "lastName": "Ejemplo",
                            "fullName": "Ana Ejemplo",
                            "email": "ana@example.com",
                            "company": "Microm S.A.S",
                            "leadScore": 0,
                            "createdAt": "2026-09-05T15:00:00.000Z",
                            "updatedAt": "2026-09-05T15:00:00.000Z"
                          }
                        ],
                        "deals": [
                          {
                            "id": "507f1f77bcf86cd799439021",
                            "title": "Manillas evento",
                            "amount": 250000,
                            "closingDate": "2026-09-30T00:00:00.000Z",
                            "status": {
                              "id": "507f1f77bcf86cd799439031",
                              "name": "Oportunidad"
                            },
                            "createdAt": "2026-09-05T15:00:00.000Z"
                          }
                        ],
                        "resume": {
                          "totalRevenue": 250000,
                          "totalDeals": 1,
                          "totalContacts": 1,
                          "lastDeal": {
                            "id": "507f1f77bcf86cd799439021",
                            "title": "Manillas evento",
                            "amount": 250000,
                            "closingDate": "2026-09-30T00:00:00.000Z"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID de empresa inválido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyError"
                },
                "example": {
                  "success": false,
                  "error": "Invalid company ID"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Empresa inexistente o no perteneciente a la organización del token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyError"
                },
                "example": {
                  "success": false,
                  "error": "Company not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "put": {
        "operationId": "updateCompanyById",
        "tags": [
          "Companies"
        ],
        "summary": "Actualizar parcialmente una empresa",
        "description": "Requiere companies:write o *. Merge de properties por key: las claves enviadas se actualizan o agregan, las demás se conservan. Si cambia name, se sincroniza la property de texto company de los contactos vinculados.",
        "x-required-permissions": [
          "companies:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompanyWrite"
              },
              "examples": {
                "city": {
                  "value": {
                    "city": "Medellín",
                    "website": "https://example.com"
                  }
                },
                "cityCode": {
                  "summary": "Corregir la ubicación por código DANE.",
                  "value": {
                    "cityCode": "05615"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Empresa actualizada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyResult"
                },
                "examples": {
                  "updated": {
                    "$ref": "#/paths/~1companies/post/responses/200/content/application~1json/examples/updated"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID de empresa inválido, cityCode inexistente (INVALID_CITY_CODE) o idNumber inválido para su tipo (INVALID_ID_NUMBER).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyError"
                },
                "examples": {
                  "invalidId": {
                    "value": {
                      "success": false,
                      "error": "Invalid company ID"
                    }
                  },
                  "invalidCityCode": {
                    "$ref": "#/paths/~1companies/post/responses/400/content/application~1json/examples/invalidCityCode"
                  },
                  "invalidIdNumber": {
                    "$ref": "#/paths/~1companies/post/responses/400/content/application~1json/examples/invalidIdNumber"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1companies~1{id}/get/responses/404"
          },
          "422": {
            "$ref": "#/paths/~1contacts/post/responses/422"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "delete": {
        "operationId": "deleteCompanyById",
        "tags": [
          "Companies"
        ],
        "summary": "Eliminar una empresa",
        "description": "Requiere companies:delete o *. Borra el documento y desvincula sus contactos y negocios (quita companyId / associatedCompanyId). Nunca borra en cascada.",
        "x-required-permissions": [
          "companies:delete"
        ],
        "responses": {
          "200": {
            "description": "Empresa eliminada; no responde 204.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyDeleteResult"
                },
                "examples": {
                  "deleted": {
                    "value": {
                      "success": true,
                      "message": "Company deleted successfully",
                      "data": {
                        "id": "507f1f77bcf86cd799439013"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1companies~1{id}/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1companies~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/companies/{id}/contacts": {
      "parameters": [
        {
          "$ref": "#/paths/~1companies~1{id}/parameters/0"
        }
      ],
      "get": {
        "operationId": "listCompanyContacts",
        "tags": [
          "Companies"
        ],
        "summary": "Listar los contactos vinculados a una empresa",
        "description": "Requiere companies:read o *. Sin paginación; más recientes primero.",
        "x-required-permissions": [
          "companies:read"
        ],
        "responses": {
          "200": {
            "description": "Contactos vinculados; data puede estar vacío.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyContactsResult"
                },
                "examples": {
                  "contacts": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "507f1f77bcf86cd799439011",
                          "firstName": "Ana",
                          "lastName": "Ejemplo",
                          "fullName": "Ana Ejemplo",
                          "email": "ana@example.com",
                          "company": "Microm S.A.S",
                          "leadScore": 0,
                          "createdAt": "2026-09-05T15:00:00.000Z",
                          "updatedAt": "2026-09-05T15:00:00.000Z"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1companies~1{id}/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1companies~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "post": {
        "operationId": "linkContactsToCompany",
        "tags": [
          "Companies"
        ],
        "summary": "Vincular contactos a una empresa en bloque",
        "description": "Requiere companies:write o *. Asigna companyId a los contactos indicados que pertenezcan a la organización y sincroniza su property de texto company con el nombre de la empresa. Los IDs inválidos o ajenos no fallan: se cuentan en skipped. Nunca desvincula (para eso, PUT /contacts/{id} con companyId null).",
        "x-required-permissions": [
          "companies:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LinkContactsRequest"
              },
              "example": {
                "contactIds": [
                  "507f1f77bcf86cd799439011",
                  "507f1f77bcf86cd799439015"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resultado de la vinculación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LinkContactsResult"
                },
                "examples": {
                  "linked": {
                    "summary": "Dos IDs válidos de la organización, uno inválido y uno ajeno.",
                    "value": {
                      "success": true,
                      "linked": 2,
                      "skipped": 2
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID de empresa inválido o contactIds vacío/no array.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyError"
                },
                "examples": {
                  "invalidId": {
                    "value": {
                      "success": false,
                      "error": "Invalid company ID"
                    }
                  },
                  "emptyIds": {
                    "value": {
                      "success": false,
                      "error": "contactIds must be a non-empty array"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1companies~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/deals/config": {
      "get": {
        "operationId": "getDealsConfig",
        "tags": [
          "Deals"
        ],
        "summary": "Pipelines y etapas de negocios de la organización",
        "description": "Requiere deals:read o *. Devuelve los IDs que necesitan POST y PUT /deals (pipeline y status). Las etapas van ordenadas por order; isWon/isLost/probability pueden faltar en etapas sin configurar.",
        "x-required-permissions": [
          "deals:read"
        ],
        "responses": {
          "200": {
            "description": "Pipelines con sus etapas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealsConfigResult"
                },
                "examples": {
                  "config": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "507f1f77bcf86cd799439030",
                          "name": "Ventas",
                          "stages": [
                            {
                              "id": "507f1f77bcf86cd799439031",
                              "name": "Oportunidad",
                              "color": "#3b82f6",
                              "order": 1,
                              "probability": 40
                            },
                            {
                              "id": "507f1f77bcf86cd799439032",
                              "name": "Cerrado Ganado",
                              "color": "#22c55e",
                              "order": 2,
                              "isWon": true,
                              "probability": 100
                            },
                            {
                              "id": "507f1f77bcf86cd799439033",
                              "name": "Cerrado Perdido",
                              "color": "#ef4444",
                              "order": 3,
                              "isLost": true,
                              "probability": 0
                            }
                          ]
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/deals": {
      "get": {
        "operationId": "listDeals",
        "tags": [
          "Deals"
        ],
        "summary": "Listar negocios",
        "description": "Requiere deals:read o *. Filtra por la organización del API Token. Los parámetros con prefijo contact. filtran por properties del contacto asociado (contact.city=Medellín, coincidencia parcial sin distinguir mayúsculas; valor vacío = property ausente o vacía) y se ignoran si también se envía contactId. Las fechas se interpretan como días completos en UTC. El parámetro search NO escapa la expresión regular: un valor con símbolos como + o ( puede responder 500.",
        "x-required-permissions": [
          "deals:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Coincidencia parcial en title (regex sin escapar)."
          },
          {
            "name": "pipeline",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "ID de pipeline."
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "ID de etapa."
          },
          {
            "name": "contactId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "companyId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amountMin",
            "in": "query",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "amountMax",
            "in": "query",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "createdFrom",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "YYYY-MM-DD; inicio del día en UTC."
          },
          {
            "name": "createdTo",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "YYYY-MM-DD; fin del día en UTC."
          },
          {
            "name": "closingFrom",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "YYYY-MM-DD sobre closingDate (real al cerrar, estimada mientras está abierto)."
          },
          {
            "name": "closingTo",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "wonFrom",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "YYYY-MM-DD sobre wonAt (instante en que se ganó); inicio del día en UTC."
          },
          {
            "name": "wonTo",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "lostFrom",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "YYYY-MM-DD sobre lostAt (instante en que se perdió)."
          },
          {
            "name": "lostTo",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "sortBy",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "createdAt",
                "updatedAt",
                "amount",
                "closingDate"
              ],
              "default": "createdAt"
            }
          },
          {
            "name": "sortOrder",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          },
          {
            "name": "contact.city",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Ejemplo de filtro por property del contacto; admite cualquier clave con prefijo contact.",
            "example": "Medellín"
          }
        ],
        "responses": {
          "200": {
            "description": "Negocios y paginación; data puede estar vacío.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealListResult"
                },
                "examples": {
                  "list": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "507f1f77bcf86cd799439021",
                          "title": "Manillas evento",
                          "amount": 250000,
                          "closingDate": "2026-09-30T00:00:00.000Z",
                          "pipeline": {
                            "id": "507f1f77bcf86cd799439030",
                            "name": "Ventas"
                          },
                          "status": {
                            "id": "507f1f77bcf86cd799439031",
                            "name": "Oportunidad",
                            "color": "#3b82f6",
                            "probability": 40
                          },
                          "contact": {
                            "id": "507f1f77bcf86cd799439011",
                            "fullName": "Ana Ejemplo",
                            "company": "Microm S.A.S",
                            "leadScore": 0
                          },
                          "company": {
                            "id": "507f1f77bcf86cd799439013",
                            "name": "Microm S.A.S"
                          },
                          "createdAt": "2026-09-05T15:00:00.000Z",
                          "updatedAt": "2026-09-05T15:00:00.000Z",
                          "customFields": []
                        }
                      ],
                      "pagination": {
                        "total": 1,
                        "page": 1,
                        "limit": 100,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "post": {
        "operationId": "createDeal",
        "tags": [
          "Deals"
        ],
        "summary": "Crear un negocio",
        "description": "Requiere deals:write o *. Valida que pipeline pertenezca a la organización y que status (si viene) sea una etapa de ese pipeline. El contacto se resuelve por id o por teléfono (creándolo si no existe). Si no se envía companyId se hereda el del contacto. Los customFields se validan contra los campos del pipeline: deben pertenecerle y no puede faltar ninguno de los `required` (omitirlo y mandarlo vacío son el mismo 400, que los nombra y devuelve sus ids en `missingFields`). Los `required` con `enforcement: \"warn\"` no bloquean: salen en `warnings` del 201. Responde el negocio poblado.",
        "x-required-permissions": [
          "deals:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DealCreate"
              },
              "examples": {
                "byPhone": {
                  "summary": "Contacto por teléfono (se crea si no existe) y empresa heredada del contacto.",
                  "value": {
                    "title": "Manillas evento",
                    "amount": 250000,
                    "pipeline": "507f1f77bcf86cd799439030",
                    "status": "507f1f77bcf86cd799439031",
                    "closingDate": "2026-09-30T00:00:00.000Z",
                    "contact": {
                      "mobile": "573000000000",
                      "name": "Ana",
                      "lastName": "Ejemplo",
                      "email": "ana@example.com",
                      "company": "Microm S.A.S"
                    }
                  }
                },
                "byIds": {
                  "summary": "Contacto y empresa existentes, con campo personalizado.",
                  "value": {
                    "title": "Manillas evento",
                    "amount": "250000",
                    "pipeline": "507f1f77bcf86cd799439030",
                    "contact": {
                      "id": "507f1f77bcf86cd799439011"
                    },
                    "companyId": "507f1f77bcf86cd799439013",
                    "customFields": [
                      {
                        "field": "507f1f77bcf86cd799439040",
                        "value": "500"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Negocio creado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealResult"
                },
                "examples": {
                  "created": {
                    "value": {
                      "success": true,
                      "message": "Deal created successfully",
                      "data": {
                        "id": "507f1f77bcf86cd799439021",
                        "title": "Manillas evento",
                        "amount": 250000,
                        "closingDate": "2026-09-30T00:00:00.000Z",
                        "pipeline": {
                          "id": "507f1f77bcf86cd799439030",
                          "name": "Ventas"
                        },
                        "wonAt": null,
                        "lostAt": null,
                        "expectedCloseDate": null,
                        "status": null,
                        "contact": {
                          "id": "507f1f77bcf86cd799439011",
                          "fullName": "Ana Ejemplo",
                          "company": "Microm S.A.S",
                          "mobile": "573000000000",
                          "leadScore": 0
                        },
                        "company": {
                          "id": "507f1f77bcf86cd799439013",
                          "name": "Microm S.A.S"
                        },
                        "createdAt": "2026-09-05T15:00:00.000Z",
                        "updatedAt": "2026-09-05T15:00:00.000Z",
                        "customFields": []
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Faltan campos, amount no numérico, pipeline/etapa/empresa/campo inválidos o error de validación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "success": false,
                      "error": "Missing required fields",
                      "required": [
                        "title",
                        "amount",
                        "pipeline"
                      ]
                    }
                  },
                  "missingCustomFields": {
                    "summary": "Falta un campo personalizado obligatorio del pipeline.",
                    "value": {
                      "success": false,
                      "error": "Faltan campos obligatorios: Cantidad de manillas, Despacho desde",
                      "missingFields": [
                        {
                          "id": "507f1f77bcf86cd799439040",
                          "name": "Cantidad de manillas",
                          "type": "text"
                        },
                        {
                          "id": "507f1f77bcf86cd799439041",
                          "name": "Despacho desde",
                          "type": "select"
                        }
                      ]
                    }
                  },
                  "amount": {
                    "value": {
                      "success": false,
                      "error": "'amount' must be a valid number"
                    }
                  },
                  "pipeline": {
                    "value": {
                      "success": false,
                      "error": "Pipeline not found or does not belong to this organization"
                    }
                  },
                  "status": {
                    "value": {
                      "success": false,
                      "error": "Status not found or does not belong to this pipeline"
                    }
                  },
                  "customField": {
                    "value": {
                      "success": false,
                      "error": "Custom field '507f...' does not exist in this pipeline"
                    }
                  },
                  "requiredField": {
                    "value": {
                      "success": false,
                      "error": "Field 'Cantidad' is required"
                    }
                  },
                  "company": {
                    "value": {
                      "success": false,
                      "error": "Company not found or does not belong to this organization"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/deals/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB del negocio.",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "507f1f77bcf86cd799439021"
        }
      ],
      "get": {
        "operationId": "getDealById",
        "tags": [
          "Deals"
        ],
        "summary": "Consultar un negocio por ID",
        "description": "Requiere deals:read o *. El negocio debe pertenecer a la organización del token.",
        "x-required-permissions": [
          "deals:read"
        ],
        "responses": {
          "200": {
            "description": "Negocio encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealResult"
                },
                "examples": {
                  "found": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439021",
                        "title": "Manillas evento",
                        "amount": 250000,
                        "closingDate": "2026-09-30T00:00:00.000Z",
                        "pipeline": {
                          "id": "507f1f77bcf86cd799439030",
                          "name": "Ventas"
                        },
                        "wonAt": null,
                        "lostAt": null,
                        "expectedCloseDate": null,
                        "status": {
                          "id": "507f1f77bcf86cd799439031",
                          "name": "Oportunidad",
                          "color": "#3b82f6",
                          "probability": 40
                        },
                        "contact": {
                          "id": "507f1f77bcf86cd799439011",
                          "fullName": "Ana Ejemplo",
                          "company": "Microm S.A.S",
                          "email": "ana@example.com",
                          "mobile": "573000000000",
                          "leadScore": 0
                        },
                        "company": {
                          "id": "507f1f77bcf86cd799439013",
                          "name": "Microm S.A.S"
                        },
                        "createdAt": "2026-09-05T15:00:00.000Z",
                        "updatedAt": "2026-09-05T15:00:00.000Z",
                        "customFields": [
                          {
                            "field": "Cantidad",
                            "type": "number",
                            "value": "500"
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID de negocio inválido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealError"
                },
                "example": {
                  "success": false,
                  "error": "Invalid deal ID"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Negocio inexistente o no perteneciente a la organización del token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealError"
                },
                "example": {
                  "success": false,
                  "error": "Deal not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "put": {
        "operationId": "updateDealById",
        "tags": [
          "Deals"
        ],
        "summary": "Actualizar parcialmente un negocio",
        "description": "Requiere deals:write o *. Solo se tocan los campos enviados. Cambiar status pasa por el gate de Blueprints (requisitos por etapa): si faltan, responde 422 y no guarda nada. Un cambio de etapa real emite deals.status_changed (automatizaciones y lead scoring). Cambiar pipeline no revalida que la etapa actual pertenezca al nuevo.",
        "x-required-permissions": [
          "deals:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DealUpdate"
              },
              "examples": {
                "moveStage": {
                  "value": {
                    "status": "507f1f77bcf86cd799439032",
                    "customFields": [
                      {
                        "field": "507f1f77bcf86cd799439040",
                        "value": "500"
                      }
                    ]
                  }
                },
                "unlink": {
                  "summary": "Quitar contacto y empresa.",
                  "value": {
                    "contact": null,
                    "companyId": null
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Negocio actualizado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealResult"
                },
                "examples": {
                  "updated": {
                    "$ref": "#/paths/~1deals~1{id}/get/responses/200/content/application~1json/examples/found"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID inválido, amount no numérico, pipeline/etapa/empresa/contacto inexistentes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealError"
                },
                "examples": {
                  "invalidId": {
                    "value": {
                      "success": false,
                      "error": "Invalid deal ID"
                    }
                  },
                  "pipeline": {
                    "value": {
                      "success": false,
                      "error": "Pipeline not found"
                    }
                  },
                  "status": {
                    "value": {
                      "success": false,
                      "error": "Status not found"
                    }
                  },
                  "contact": {
                    "value": {
                      "success": false,
                      "error": "Contact not found or does not belong to this organization"
                    }
                  },
                  "company": {
                    "value": {
                      "success": false,
                      "error": "Company not found or does not belong to this organization"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1deals~1{id}/get/responses/404"
          },
          "422": {
            "description": "Requisitos de la etapa destino sin cumplir (Blueprints). Nada se guarda.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StageRequirementsError"
                },
                "examples": {
                  "unmet": {
                    "value": {
                      "success": false,
                      "error": "Para pasar a \"Cerrado Ganado\" faltan: Cantidad",
                      "code": "STAGE_REQUIREMENTS_UNMET",
                      "message": "Para pasar a \"Cerrado Ganado\" faltan: Cantidad",
                      "missing": [
                        {
                          "kind": "field",
                          "id": "507f1f77bcf86cd799439040",
                          "label": "Cantidad",
                          "stageId": "507f1f77bcf86cd799439032",
                          "stageName": "Cerrado Ganado"
                        }
                      ],
                      "warnings": []
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "delete": {
        "operationId": "deleteDealById",
        "tags": [
          "Deals"
        ],
        "summary": "Eliminar un negocio",
        "description": "Requiere deals:delete o *. Borra el negocio y los valores de sus campos personalizados.",
        "x-required-permissions": [
          "deals:delete"
        ],
        "responses": {
          "200": {
            "description": "Negocio eliminado; no responde 204.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealDeleteResult"
                },
                "examples": {
                  "deleted": {
                    "value": {
                      "success": true,
                      "message": "Deal deleted successfully",
                      "data": {
                        "id": "507f1f77bcf86cd799439021"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1deals~1{id}/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1deals~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/forms": {
      "get": {
        "operationId": "listForms",
        "tags": [
          "Forms"
        ],
        "summary": "Listar formularios de captación",
        "description": "Requiere forms:read o *. Más recientes primero. El listado no incluye share.",
        "x-required-permissions": [
          "forms:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          },
          {
            "name": "active",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            },
            "description": "Filtra activos o pausados; sin el parámetro devuelve todos."
          }
        ],
        "responses": {
          "200": {
            "description": "Formularios y paginación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FormListResult"
                },
                "examples": {
                  "list": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "66f1000000000000000000c2",
                          "name": "Cotización manillas",
                          "description": null,
                          "slug": "cotizacion-manillas",
                          "active": true,
                          "fields": [
                            {
                              "key": "email",
                              "label": "Correo",
                              "type": "email",
                              "required": true
                            }
                          ],
                          "hiddenFields": [
                            "utm_source",
                            "utm_medium",
                            "utm_campaign"
                          ],
                          "createContact": true,
                          "defaultSource": "website",
                          "tags": [
                            "web"
                          ],
                          "deal": null,
                          "stats": {
                            "submissions": 12
                          },
                          "createdAt": "2026-09-05T10:00:00.000Z",
                          "updatedAt": "2026-09-05T10:00:00.000Z"
                        }
                      ],
                      "pagination": {
                        "total": 1,
                        "page": 1,
                        "limit": 100,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/forms/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB del formulario.",
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "operationId": "getFormById",
        "tags": [
          "Forms"
        ],
        "summary": "Detalle de un formulario (con share)",
        "description": "Requiere forms:read o *. Incluye share (link público, snippets de embed y URL del webhook con la clave pública) cuando el form tiene slug y clave. Un ID que no es ObjectId responde 404, no 400.",
        "x-required-permissions": [
          "forms:read"
        ],
        "responses": {
          "200": {
            "description": "Formulario encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FormResult"
                },
                "examples": {
                  "found": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "66f1000000000000000000c2",
                        "name": "Cotización manillas",
                        "description": null,
                        "slug": "cotizacion-manillas",
                        "active": true,
                        "fields": [
                          {
                            "key": "firstName",
                            "label": "Nombre",
                            "type": "text",
                            "required": true
                          },
                          {
                            "key": "email",
                            "label": "Correo",
                            "type": "email",
                            "required": true
                          },
                          {
                            "key": "mobile",
                            "label": "Celular",
                            "type": "tel",
                            "required": false
                          },
                          {
                            "key": "quantity",
                            "label": "Cantidad",
                            "type": "number",
                            "required": false
                          }
                        ],
                        "hiddenFields": [
                          "utm_source",
                          "utm_medium",
                          "utm_campaign",
                          "utm_term",
                          "utm_content",
                          "gclid",
                          "fbclid",
                          "referrer",
                          "page_url"
                        ],
                        "createContact": true,
                        "defaultSource": "website",
                        "tags": [
                          "web"
                        ],
                        "deal": {
                          "pipelineId": "507f1f77bcf86cd799439030",
                          "stageId": "507f1f77bcf86cd799439031"
                        },
                        "stats": {
                          "submissions": 1482,
                          "lastSubmissionAt": "2026-09-05T14:41:00.000Z"
                        },
                        "share": {
                          "publicUrl": "https://forms.fusioncol.com/f/cotizacion-manillas",
                          "embedScript": "<script src=\"https://forms.fusioncol.com/embed.js\" data-form=\"cotizacion-manillas\"></script>",
                          "embedIframe": "<iframe src=\"https://forms.fusioncol.com/f/cotizacion-manillas?embed=1\" style=\"width:100%;border:0;min-height:520px\" loading=\"lazy\" title=\"Formulario\"></iframe>",
                          "webhookUrl": "https://forms.fusioncol.com/api/v1/forms/submit/66f1000000000000000000c2?key=fk_xxx"
                        },
                        "createdAt": "2026-09-05T10:00:00.000Z",
                        "updatedAt": "2026-09-05T10:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Formulario inexistente, ID inválido o de otra organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FormError"
                },
                "example": {
                  "success": false,
                  "error": "Form not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "put": {
        "operationId": "updateFormById",
        "tags": [
          "Forms"
        ],
        "summary": "Actualizar la configuración de un formulario",
        "description": "Requiere forms:write o *. Misma lista blanca que el editor del CRM. Nunca cambia slug ni publicKey (la clave se regenera solo desde el panel). Responde el form con share.",
        "x-required-permissions": [
          "forms:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FormUpdate"
              },
              "examples": {
                "pause": {
                  "value": {
                    "active": false
                  }
                },
                "deal": {
                  "summary": "Activar creación de negocio y aviso por correo.",
                  "value": {
                    "deal": {
                      "enabled": true,
                      "pipelineId": "507f1f77bcf86cd799439030",
                      "stageId": "507f1f77bcf86cd799439031",
                      "titleTemplate": "Cotización {{nombre}}"
                    },
                    "notify": {
                      "emails": [
                        "ventas@example.com"
                      ],
                      "inApp": true
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Formulario actualizado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FormResult"
                },
                "examples": {
                  "updated": {
                    "$ref": "#/paths/~1forms~1{id}/get/responses/200/content/application~1json/examples/found"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1forms~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/forms/{id}/responses": {
      "parameters": [
        {
          "$ref": "#/paths/~1forms~1{id}/parameters/0"
        }
      ],
      "get": {
        "operationId": "listFormResponses",
        "tags": [
          "Forms"
        ],
        "summary": "Listar envíos de un formulario",
        "description": "Requiere forms:read o *. Más recientes primero. Por defecto excluye los envíos marcados como sospechosos (honeypot, envío demasiado rápido, dominio no permitido).",
        "x-required-permissions": [
          "forms:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          },
          {
            "name": "outcome",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "created",
                "updated",
                "not_created"
              ]
            }
          },
          {
            "name": "since",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Solo envíos con receivedAt igual o posterior; un valor inválido se ignora."
          },
          {
            "name": "includeSuspicious",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "true"
              ]
            },
            "description": "Incluye los envíos sospechosos (se conservan 30 días)."
          }
        ],
        "responses": {
          "200": {
            "description": "Envíos y paginación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FormResponseListResult"
                },
                "examples": {
                  "responses": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "66f2000000000000000000d1",
                          "formId": "66f1000000000000000000c2",
                          "contactId": "507f1f77bcf86cd799439011",
                          "dealId": "507f1f77bcf86cd799439021",
                          "outcome": "created",
                          "responses": {
                            "firstName": "Laura",
                            "email": "laura@example.com",
                            "quantity": "2000"
                          },
                          "context": {
                            "referrer": "https://www.google.com/",
                            "pageUrl": "https://manillasdecontrol.com/manillas-tyvek",
                            "utm": {
                              "source": "meta_ads",
                              "campaign": "manillas-eventos-sep"
                            },
                            "gclid": null,
                            "fbclid": null,
                            "embed": true
                          },
                          "consent": {
                            "accepted": true,
                            "text": "Autorizo el tratamiento de mis datos.",
                            "at": "2026-09-05T14:41:00.000Z"
                          },
                          "suspicious": false,
                          "suspiciousReason": null,
                          "receivedAt": "2026-09-05T14:41:00.000Z"
                        }
                      ],
                      "pagination": {
                        "total": 1,
                        "page": 1,
                        "limit": 100,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1forms~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/forms/{id}/submit": {
      "parameters": [
        {
          "$ref": "#/paths/~1forms~1{id}/parameters/0"
        }
      ],
      "post": {
        "operationId": "submitForm",
        "tags": [
          "Forms"
        ],
        "summary": "Enviar un formulario (server-to-server)",
        "description": "Requiere forms:write o *. Pasa por el mismo procesador que la página pública: dedupe del contacto por email/móvil, merge solo de campos vacíos si ya existía, atribución first-touch, etiquetas, lista, negocio, aviso al dueño y trigger form_submitted. Cuenta para el límite de contactos del plan. Documento (idType/idNumber/dv, Épica Z, Z-D16): modo lenient — se normaliza lo que se pueda (código DIAN, número sin puntos, DV del NIT en dv) y lo que no se reconoce se guarda como llegó; nunca responde error por el documento. La ciudad sí va en modo estricto (400 CITY_UNRESOLVED / INVALID_CITY_CODE). Para forms externos sin API token usa la URL del webhook de share (con ?key=).",
        "x-required-permissions": [
          "forms:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FormSubmitRequest"
              },
              "example": {
                "firstName": "Laura",
                "email": "laura@example.com",
                "mobile": "3001234567",
                "quantity": "2000",
                "utm_source": "meta_ads",
                "utm_campaign": "manillas-eventos-sep",
                "page_url": "https://manillasdecontrol.com/manillas-tyvek"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Envío registrado (aunque el contacto no se haya creado, ver outcome).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FormSubmitResult"
                },
                "examples": {
                  "submitted": {
                    "value": {
                      "success": true,
                      "data": {
                        "responseId": "66f2000000000000000000d1",
                        "outcome": "created",
                        "contactId": "507f1f77bcf86cd799439011",
                        "dealId": "507f1f77bcf86cd799439021",
                        "suspicious": false
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Body vacío, campo requerido faltante, consentimiento no aceptado, o ciudad no resuelta contra el catálogo DANE (code CITY_UNRESOLVED con candidates / INVALID_CITY_CODE; los campos city/cityCode del formulario van en modo estricto y el contacto no se crea).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FormError"
                },
                "examples": {
                  "empty": {
                    "value": {
                      "success": false,
                      "error": "Body is empty"
                    }
                  },
                  "required": {
                    "value": {
                      "success": false,
                      "error": "Campo Correo es requerido."
                    }
                  },
                  "consent": {
                    "value": {
                      "success": false,
                      "error": "Debes aceptar el tratamiento de datos para continuar."
                    }
                  },
                  "cityUnresolved": {
                    "value": {
                      "success": false,
                      "error": "La ciudad \"Rionegro\" corresponde a varios municipios; indique cityCode o el departamento",
                      "code": "CITY_UNRESOLVED",
                      "input": "Rionegro",
                      "candidates": [
                        {
                          "code": "05615",
                          "shortName": "Rionegro",
                          "stateName": "Antioquia",
                          "stateCode": "05"
                        },
                        {
                          "code": "68615",
                          "shortName": "Rionegro",
                          "stateName": "Santander",
                          "stateCode": "68"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "description": "Permiso insuficiente, módulo API no habilitado o límite de contactos del plan alcanzado (PLAN_LIMIT_REACHED).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "example": {
                  "message": "Has alcanzado el límite de tu plan. Actualiza tu plan para continuar.",
                  "code": "PLAN_LIMIT_REACHED"
                }
              }
            }
          },
          "404": {
            "$ref": "#/paths/~1forms~1{id}/get/responses/404"
          },
          "410": {
            "description": "El formulario está pausado (active false).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FormError"
                },
                "example": {
                  "success": false,
                  "error": "Form is paused"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/landing-pages": {
      "get": {
        "operationId": "listLandingPages",
        "tags": [
          "Landing pages"
        ],
        "summary": "Listar landing pages",
        "description": "Requiere forms:read o *. Más recientes (por actualización) primero. El listado no incluye las secciones; pídelas en el detalle.",
        "x-required-permissions": [
          "forms:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "published"
              ]
            },
            "description": "Filtra por estado; sin el parámetro devuelve todas."
          }
        ],
        "responses": {
          "200": {
            "description": "Páginas y paginación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LandingPageListResult"
                },
                "examples": {
                  "list": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "66f1000000000000000000d1",
                          "name": "Manillas para eventos",
                          "slug": "manillas-para-eventos",
                          "status": "published",
                          "template": "cotizacion",
                          "formId": "66f1000000000000000000c2",
                          "publicUrl": "https://forms.fusioncol.com/p/manillas-para-eventos",
                          "sectionsCount": 6,
                          "stats": {
                            "views": 1240,
                            "uniques": 980,
                            "submissions": 41,
                            "whatsappClicks": 97,
                            "lastViewAt": "2026-09-05T20:11:00.000Z"
                          },
                          "publishedAt": "2026-09-05T15:00:00.000Z",
                          "createdAt": "2026-09-05T14:00:00.000Z",
                          "updatedAt": "2026-09-05T15:00:00.000Z"
                        }
                      ],
                      "pagination": {
                        "total": 1,
                        "page": 1,
                        "limit": 100,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "post": {
        "operationId": "createLandingPage",
        "tags": [
          "Landing pages"
        ],
        "summary": "Crear una landing page (siempre en borrador)",
        "description": "Requiere landings:write o *. La página nace en borrador (status draft): no es pública hasta POST /landing-pages/{id}/publish. Parte de una plantilla (templateId, default blank; ver GET /landing-pages/templates) o de las secciones enviadas (sections reemplaza las de la plantilla). El slug se genera del nombre, único global. formId debe ser un formulario de la organización. Las imágenes van como URLs http(s); no hay subida de archivos por API. Cuenta contra el límite de páginas del plan (limits.landingPages): lleno → 403 PLAN_LIMIT_REACHED. En plan Free theme.showBranding queda siempre en true.",
        "x-required-permissions": [
          "landings:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LandingPageCreate"
              },
              "examples": {
                "template": {
                  "summary": "Desde plantilla",
                  "value": {
                    "name": "Manillas para eventos",
                    "templateId": "cotizacion",
                    "formId": "66f1000000000000000000c2"
                  }
                },
                "sections": {
                  "summary": "Con secciones propias",
                  "value": {
                    "name": "Promo tyvek",
                    "sections": [
                      {
                        "type": "hero",
                        "data": {
                          "title": "Manillas tyvek desde 500 unidades",
                          "cta": {
                            "label": "Cotizar por WhatsApp",
                            "action": "whatsapp"
                          }
                        }
                      },
                      {
                        "type": "whatsapp_cta",
                        "data": {
                          "title": "¿Dudas? Escríbenos"
                        }
                      }
                    ],
                    "whatsapp": {
                      "phone": "573001112233",
                      "sticky": true
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Página creada en borrador, con el detalle completo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LandingPageResult"
                },
                "examples": {
                  "created": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "66f1000000000000000000d1",
                        "name": "Manillas para eventos",
                        "slug": "manillas-para-eventos",
                        "status": "published",
                        "template": "cotizacion",
                        "formId": "66f1000000000000000000c2",
                        "publicUrl": "https://forms.fusioncol.com/p/manillas-para-eventos",
                        "sectionsCount": 2,
                        "sections": [
                          {
                            "id": "a1b2c3d4e5f6",
                            "type": "hero",
                            "enabled": true,
                            "order": 0,
                            "data": {
                              "title": "Manillas para eventos",
                              "subtitle": "Tyvek, tela sublimada y vinilo. Cotiza en minutos.",
                              "layout": "split",
                              "cta": {
                                "label": "Cotizar por WhatsApp",
                                "action": "whatsapp"
                              }
                            }
                          },
                          {
                            "id": "b2c3d4e5f6a1",
                            "type": "form",
                            "enabled": true,
                            "order": 1,
                            "data": {
                              "title": "Pide tu cotización",
                              "thanks": {
                                "title": "¡Listo!",
                                "showWhatsapp": true
                              }
                            }
                          }
                        ],
                        "theme": {
                          "primaryColor": "#1f2a48",
                          "fontPreset": "system",
                          "showBranding": true
                        },
                        "seo": {
                          "title": "Manillas para eventos",
                          "indexable": true
                        },
                        "whatsapp": {
                          "phone": "573001112233",
                          "sticky": true
                        },
                        "stats": {
                          "views": 1240,
                          "uniques": 980,
                          "submissions": 41,
                          "whatsappClicks": 97,
                          "lastViewAt": "2026-09-05T20:11:00.000Z"
                        },
                        "publishedAt": "2026-09-05T15:00:00.000Z",
                        "createdAt": "2026-09-05T14:00:00.000Z",
                        "updatedAt": "2026-09-05T15:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Payload inválido (nombre vacío, plantilla desconocida, sección inválida, formulario de otra organización, ID de Pixel/GA4 mal formado, nada que actualizar).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1landing-pages~1{id}~1publish/post/responses/400/content/application~1json/schema"
                },
                "examples": {
                  "name": {
                    "value": {
                      "success": false,
                      "error": "Ponle un nombre a la página."
                    }
                  },
                  "section": {
                    "value": {
                      "success": false,
                      "error": "Tipo de sección desconocido: \"video\"."
                    }
                  },
                  "form": {
                    "value": {
                      "success": false,
                      "error": "Ese formulario no existe en tu organización."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "description": "Permiso insuficiente, módulo API no habilitado o límite de páginas del plan alcanzado (PLAN_LIMIT_REACHED).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "example": {
                  "message": "Has alcanzado el límite de tu plan. Actualiza tu plan para continuar.",
                  "code": "PLAN_LIMIT_REACHED"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/landing-pages/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB de la landing page.",
          "schema": {
            "type": "string"
          },
          "example": "66f1000000000000000000c2"
        }
      ],
      "get": {
        "operationId": "getLandingPageById",
        "tags": [
          "Landing pages"
        ],
        "summary": "Detalle de una landing page (con secciones)",
        "description": "Requiere forms:read o *. Incluye las secciones con su contenido, tema, SEO y la configuración de WhatsApp. Un ID que no es ObjectId responde 404, no 400.",
        "x-required-permissions": [
          "forms:read"
        ],
        "responses": {
          "200": {
            "description": "Página encontrada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LandingPageResult"
                },
                "examples": {
                  "found": {
                    "$ref": "#/paths/~1landing-pages/post/responses/201/content/application~1json/examples/created"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Página inexistente, ID inválido o de otra organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1landing-pages~1{id}~1publish/post/responses/400/content/application~1json/schema"
                },
                "example": {
                  "success": false,
                  "error": "Landing page not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "put": {
        "operationId": "updateLandingPage",
        "tags": [
          "Landing pages"
        ],
        "summary": "Editar una landing page",
        "description": "Requiere landings:write o *. Solo cambia los campos enviados; theme, seo, tracking, whatsapp y legal se fusionan campo a campo, sections REEMPLAZA la lista completa. Si la página está publicada los cambios salen EN VIVO de inmediato (igual que el guardado de la app; no hace falta republicar). Si la edición la deja sin poder publicarse (sin secciones activas, bloque form sin formulario o con el formulario pausado) vuelve a borrador y la respuesta trae unpublished true con el motivo. status no se edita aquí (usa publish / unpublish). Cambiar slug cambia la URL pública (409 si ya existe).",
        "x-required-permissions": [
          "landings:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LandingPageUpdate"
              },
              "examples": {
                "seo": {
                  "value": {
                    "seo": {
                      "title": "Manillas para eventos en Medellín",
                      "indexable": true
                    },
                    "tracking": {
                      "metaPixelId": "123456789012345"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Página guardada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LandingPageUpdateResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1landing-pages/post/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1landing-pages~1{id}/get/responses/404"
          },
          "409": {
            "description": "El slug ya lo usa otra página (los slugs son únicos entre todas las organizaciones).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1landing-pages~1{id}~1publish/post/responses/400/content/application~1json/schema"
                },
                "example": {
                  "success": false,
                  "error": "Ese slug ya está en uso. Elige otro.",
                  "code": "SLUG_TAKEN"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/landing-pages/{id}/stats": {
      "parameters": [
        {
          "$ref": "#/paths/~1landing-pages~1{id}/parameters/0"
        }
      ],
      "get": {
        "operationId": "getLandingPageStats",
        "tags": [
          "Landing pages"
        ],
        "summary": "Estadísticas de una landing page",
        "description": "Requiere forms:read o *. Analítica propia de FusionCRM (no depende de GA4 ni del pixel del cliente): totales, dos conversiones (envíos/únicos y WhatsApp/únicos), serie diaria, embudo por sesión, top utm_source, clics por sección, dispositivo y últimas respuestas. Fechas en America/Bogota; por defecto los últimos 30 días. Las vistas previas y el modo prueba nunca cuentan.",
        "x-required-permissions": [
          "forms:read"
        ],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "YYYY-MM-DD. Default 29 días antes de to."
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "YYYY-MM-DD. Default hoy. Rango máximo 366 días."
          }
        ],
        "responses": {
          "200": {
            "description": "Estadísticas del rango.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LandingPageStatsResult"
                },
                "examples": {
                  "stats": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "66f1000000000000000000d1",
                        "name": "Manillas para eventos",
                        "slug": "manillas-para-eventos",
                        "status": "published",
                        "range": {
                          "from": "2026-08-07",
                          "to": "2026-09-05"
                        },
                        "totals": {
                          "views": 1240,
                          "uniques": 980,
                          "sessions": 1010,
                          "scroll50": 620,
                          "ctaClicks": 140,
                          "whatsappClicks": 97,
                          "formStarts": 88,
                          "submissions": 41,
                          "outboundClicks": 3,
                          "conversationsStarted": 0,
                          "dealsWon": 4,
                          "revenueWon": 2850000,
                          "timeOnPageSum": 61200,
                          "timeOnPageCount": 900,
                          "avgTimeOnPage": 68,
                          "conversionForm": 4.2,
                          "conversionWhatsapp": 9.9
                        },
                        "series": [
                          {
                            "date": "2026-09-04",
                            "views": 60,
                            "uniques": 48,
                            "submissions": 2,
                            "whatsappClicks": 5,
                            "ctaClicks": 7
                          },
                          {
                            "date": "2026-09-05",
                            "views": 72,
                            "uniques": 55,
                            "submissions": 3,
                            "whatsappClicks": 6,
                            "ctaClicks": 9
                          }
                        ],
                        "funnel": {
                          "sessions": 1010,
                          "scrolled": 620,
                          "engaged": 210,
                          "converted": 138,
                          "won": 4
                        },
                        "bySource": [
                          {
                            "source": "meta_ads",
                            "views": 800,
                            "submissions": 30,
                            "whatsappClicks": 70
                          },
                          {
                            "source": "direct",
                            "views": 300,
                            "submissions": 8,
                            "whatsappClicks": 20
                          },
                          {
                            "source": "qr",
                            "views": 140,
                            "submissions": 3,
                            "whatsappClicks": 7
                          }
                        ],
                        "bySection": [
                          {
                            "section": "hero",
                            "ctaClicks": 90,
                            "whatsappClicks": 40
                          },
                          {
                            "section": "sticky",
                            "ctaClicks": 0,
                            "whatsappClicks": 35
                          }
                        ],
                        "byDevice": {
                          "mobile": 900,
                          "desktop": 300,
                          "tablet": 40
                        },
                        "lastResponses": [
                          {
                            "id": "66f1000000000000000000e1",
                            "contactId": "66f1000000000000000000a1",
                            "dealId": null,
                            "outcome": "created",
                            "utmSource": "meta_ads",
                            "receivedAt": "2026-09-05T19:20:00.000Z"
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1landing-pages~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/landing-pages/templates": {
      "get": {
        "operationId": "listLandingPageTemplates",
        "tags": [
          "Landing pages"
        ],
        "summary": "Plantillas y catálogo de secciones",
        "description": "Requiere forms:read o *. Catálogo fijo (vive en código): las plantillas con su objetivo, si necesitan formulario y los tipos de sección que traen; los tipos de sección con un ejemplo del objeto data (los valores por defecto) y las claves de ícono permitidas en benefits. Úsalo para armar sections válidas antes de crear o editar.",
        "x-required-permissions": [
          "forms:read"
        ],
        "responses": {
          "200": {
            "description": "Catálogo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LandingTemplatesResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/landing-pages/{id}/duplicate": {
      "parameters": [
        {
          "$ref": "#/paths/~1landing-pages~1{id}/parameters/0"
        }
      ],
      "post": {
        "operationId": "duplicateLandingPage",
        "tags": [
          "Landing pages"
        ],
        "summary": "Duplicar una landing page",
        "description": "Requiere landings:write o *. Copia en borrador con \"(copia)\" en el nombre, slug nuevo terminado en -copia, las mismas secciones y configuración, estadísticas en cero. Cuenta contra el límite de páginas del plan (403 PLAN_LIMIT_REACHED).",
        "x-required-permissions": [
          "landings:write"
        ],
        "responses": {
          "201": {
            "description": "Copia creada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LandingPageResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "description": "Permiso insuficiente, módulo API no habilitado o límite de páginas del plan alcanzado (PLAN_LIMIT_REACHED).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "example": {
                  "message": "Has alcanzado el límite de tu plan. Actualiza tu plan para continuar.",
                  "code": "PLAN_LIMIT_REACHED"
                }
              }
            }
          },
          "404": {
            "$ref": "#/paths/~1landing-pages~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/landing-pages/{id}/publish": {
      "parameters": [
        {
          "$ref": "#/paths/~1landing-pages~1{id}/parameters/0"
        }
      ],
      "post": {
        "operationId": "publishLandingPage",
        "tags": [
          "Landing pages"
        ],
        "summary": "Publicar una landing page",
        "description": "Requiere landings:write o *. EFECTO EXTERNO: la página queda pública en internet en publicUrl (<PAGES_PUBLIC_BASE_URL>/p/<slug>), indexable por buscadores salvo seo.indexable false, y empieza a recibir visitas y leads. Exige al menos una sección activa y, si hay bloque form, un formulario activo de la organización; si no cumple → 400 LANDING_NOT_PUBLISHABLE con el motivo. Guarda una copia de las secciones publicadas. Republicar una página ya publicada es válido (actualiza publishedAt).",
        "x-required-permissions": [
          "landings:write"
        ],
        "responses": {
          "200": {
            "description": "Página publicada (publicUrl con valor).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LandingPageResult"
                }
              }
            }
          },
          "400": {
            "description": "La página no cumple las reglas de publicación.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": false
                    },
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string",
                      "description": "SLUG_TAKEN (409) o LANDING_NOT_PUBLISHABLE (400 al publicar)."
                    }
                  }
                },
                "example": {
                  "success": false,
                  "error": "Activa al menos una sección antes de publicar.",
                  "code": "LANDING_NOT_PUBLISHABLE"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1landing-pages~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/landing-pages/{id}/unpublish": {
      "parameters": [
        {
          "$ref": "#/paths/~1landing-pages~1{id}/parameters/0"
        }
      ],
      "post": {
        "operationId": "unpublishLandingPage",
        "tags": [
          "Landing pages"
        ],
        "summary": "Despublicar una landing page",
        "description": "Requiere landings:write o *. Vuelve a borrador: la URL pública responde 404 de inmediato (anuncios, QR y enlaces quedan rotos hasta republicar). No borra secciones ni estadísticas. Idempotente.",
        "x-required-permissions": [
          "landings:write"
        ],
        "responses": {
          "200": {
            "description": "Página en borrador.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LandingPageResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1landing-pages~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/campaigns": {
      "get": {
        "operationId": "listCampaigns",
        "tags": [
          "Campaigns"
        ],
        "summary": "Listar campañas de WhatsApp con resumen de entrega",
        "description": "Requiere contacts:read o *. Solo campañas de tipo WhatsApp Marketing (las que se crean en /mass-whatsapp), más recientes primero. `summary` cuenta contactos únicos por su ÚLTIMO estado reportado por Meta: `accepted` (Meta aceptó el envío), `delivered`, `read` y `failed`. Para saber quién respondió y el motivo de cada fallo, pedir `/campaigns/{id}/stats`.",
        "x-required-permissions": [
          "contacts:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Campañas y paginación.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data",
                    "pagination"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name",
                          "description",
                          "state",
                          "templateName",
                          "contactListId",
                          "summary",
                          "createdAt",
                          "updatedAt"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "description": {
                            "type": "string"
                          },
                          "state": {
                            "type": "string",
                            "enum": [
                              "planeacion",
                              "en_curso",
                              "terminada"
                            ]
                          },
                          "templateName": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Plantilla aprobada en Meta que envía la campaña."
                          },
                          "contactListId": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "summary": {
                            "type": "object",
                            "description": "Contactos únicos por su último estado reportado por Meta.",
                            "required": [
                              "accepted",
                              "delivered",
                              "read",
                              "failed",
                              "lastSentAt"
                            ],
                            "properties": {
                              "accepted": {
                                "type": "integer",
                                "minimum": 0,
                                "description": "Meta aceptó el envío (HTTP 200). No implica entrega."
                              },
                              "delivered": {
                                "type": "integer",
                                "minimum": 0
                              },
                              "read": {
                                "type": "integer",
                                "minimum": 0
                              },
                              "failed": {
                                "type": "integer",
                                "minimum": 0
                              },
                              "lastSentAt": {
                                "type": [
                                  "string",
                                  "null"
                                ],
                                "format": "date-time"
                              }
                            }
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "updatedAt": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                },
                "examples": {
                  "list": {
                    "summary": "Dos campañas de WhatsApp",
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "6aaac4526e2bf21326089c55",
                          "name": "Santander 2026",
                          "description": "Parques, hoteles y turismo con control de acceso",
                          "state": "en_curso",
                          "templateName": "prospeccion_manillas_v2",
                          "contactListId": "6aaa8f5ffbdd6317ec16c089",
                          "summary": {
                            "accepted": 43,
                            "delivered": 39,
                            "read": 24,
                            "failed": 4,
                            "lastSentAt": "2026-09-16T16:31:36.000Z"
                          },
                          "createdAt": "2026-09-16T16:31:14.720Z",
                          "updatedAt": "2026-09-16T16:31:36.600Z"
                        },
                        {
                          "id": "6aa8d1f2e2bf21326089a001",
                          "name": "Hoteles Meta sep-2026",
                          "description": "",
                          "state": "terminada",
                          "templateName": "prospeccion_manillas_v2",
                          "contactListId": "6aa54ccd72676a4a35dd0465",
                          "summary": {
                            "accepted": 203,
                            "delivered": 180,
                            "read": 96,
                            "failed": 36,
                            "lastSentAt": "2026-09-15T14:09:12.000Z"
                          },
                          "createdAt": "2026-09-15T13:40:00.000Z",
                          "updatedAt": "2026-09-15T14:09:12.000Z"
                        }
                      ],
                      "pagination": {
                        "total": 2,
                        "page": 1,
                        "limit": 50,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "post": {
        "operationId": "createCampaignDraft",
        "tags": [
          "Campaigns"
        ],
        "summary": "Crear el borrador de una campaña de WhatsApp (sin ejecutarla)",
        "description": "Requiere automations:write o *. Crea una campaña de WhatsApp Marketing en estado `planeacion` y SIN fecha de envío: no envía, no programa y no ejecuta nada (ejecutar se hace desde la app; no está en la API). `state`, `startDate`, `schedule` o `notes` en el body se ignoran. `contactListId` tiene que ser una lista de la organización del token (si no, 400 `LIST_NOT_FOUND`). La plantilla no se verifica contra Meta: un nombre inexistente falla al ejecutar. Después, `GET /campaigns/{id}/preflight` dice a cuántos les llegaría.",
        "x-required-permissions": [
          "automations:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "state, startDate, schedule y notes se ignoran; el borrador nace en planeacion sin fecha.",
                "required": [
                  "name",
                  "contactListId",
                  "template"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 2000
                  },
                  "contactListId": {
                    "type": "string",
                    "description": "Lista de contactos de la organización del token."
                  },
                  "template": {
                    "type": "object",
                    "required": [
                      "name"
                    ],
                    "properties": {
                      "name": {
                        "type": "string",
                        "pattern": "^[a-z0-9_]+$",
                        "maxLength": 512,
                        "description": "Nombre exacto de la plantilla aprobada en Meta."
                      },
                      "language": {
                        "type": "string",
                        "pattern": "^[a-z]{2,3}(_[A-Z]{2})?$",
                        "default": "es"
                      },
                      "variableMapping": {
                        "$ref": "#/paths/~1campaigns~1{id}/put/requestBody/content/application~1json/schema/properties/template/properties/variableMapping/oneOf/0"
                      },
                      "headerMediaUrl": {
                        "type": "string",
                        "format": "uri",
                        "maxLength": 2048,
                        "description": "URL https de la imagen/video/documento de cabecera."
                      }
                    }
                  }
                }
              },
              "examples": {
                "hoteles": {
                  "value": {
                    "name": "Hoteles Santander oct-2026",
                    "description": "Prospección de hoteles con control de acceso",
                    "contactListId": "6aaa8f5ffbdd6317ec16c089",
                    "template": {
                      "name": "prospeccion_manillas_v2",
                      "language": "es",
                      "variableMapping": {
                        "1": {
                          "source": "field",
                          "key": "firstName",
                          "fallback": "cliente"
                        }
                      },
                      "headerMediaUrl": "https://cdn.example.com/manillas.jpg"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Borrador creado.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "id",
                        "name",
                        "description",
                        "state",
                        "startDate",
                        "campaignType",
                        "contactListId",
                        "contactListName",
                        "template",
                        "createdAt",
                        "updatedAt"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "description": {
                          "type": "string"
                        },
                        "state": {
                          "type": "string",
                          "enum": [
                            "planeacion",
                            "en_curso",
                            "terminada"
                          ],
                          "description": "Un borrador creado por API siempre es planeacion."
                        },
                        "startDate": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        },
                        "campaignType": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "const": "whatsapp_marketing"
                        },
                        "contactListId": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "contactListName": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "template": {
                          "type": "object",
                          "required": [
                            "name",
                            "language",
                            "variableMapping",
                            "headerMediaUrl"
                          ],
                          "properties": {
                            "name": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "language": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "variableMapping": {
                              "oneOf": [
                                {
                                  "$ref": "#/paths/~1campaigns~1{id}/put/requestBody/content/application~1json/schema/properties/template/properties/variableMapping/oneOf/0"
                                },
                                {
                                  "type": "null"
                                }
                              ]
                            },
                            "headerMediaUrl": {
                              "type": [
                                "string",
                                "null"
                              ]
                            }
                          }
                        },
                        "createdAt": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updatedAt": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "created": {
                    "summary": "Borrador recién creado",
                    "value": {
                      "success": true,
                      "data": {
                        "id": "6ab10000000000000000c001",
                        "name": "Hoteles Santander oct-2026",
                        "description": "Prospección de hoteles con control de acceso",
                        "state": "planeacion",
                        "startDate": null,
                        "campaignType": "whatsapp_marketing",
                        "contactListId": "6aaa8f5ffbdd6317ec16c089",
                        "contactListName": "Hoteles Santander",
                        "template": {
                          "name": "prospeccion_manillas_v2",
                          "language": "es",
                          "variableMapping": {
                            "1": {
                              "source": "field",
                              "key": "firstName",
                              "fallback": "cliente"
                            }
                          },
                          "headerMediaUrl": "https://cdn.example.com/manillas.jpg"
                        },
                        "createdAt": "2026-09-25T15:00:00.000Z",
                        "updatedAt": "2026-09-25T15:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Payload inválido (el mensaje dice qué campo) o lista de contactos que no es de la organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/campaigns/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "ID de la campaña (ObjectId)."
        }
      ],
      "put": {
        "operationId": "updateCampaignDraft",
        "tags": [
          "Campaigns"
        ],
        "summary": "Editar el borrador de una campaña que nunca se ha ejecutado",
        "description": "Requiere automations:write o *. Parcial: `name`, `description` (null la borra), `contactListId` y `template` (solo los campos enviados; `variableMapping` o `headerMediaUrl` en null los quitan). Nunca cambia `state` ni `startDate`. Solo se edita si la campaña NUNCA se ha ejecutado: si hay una ejecución en cola o corriendo, o está programada y su fecha ya llegó, responde 409 `CAMPAIGN_RUNNING`; si ya salió (ejecución previa, mensajes enviados o estado `terminada`), 409 `CAMPAIGN_ALREADY_SENT`. Una campaña que no es de WhatsApp Marketing responde 409 `NOT_WHATSAPP_CAMPAIGN`. El body se valida antes de buscar la campaña; un ID que no es ObjectId o de otra organización responde 404.",
        "x-required-permissions": [
          "automations:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Parcial; al menos un campo. Nunca cambia state ni startDate.",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "description": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 2000,
                    "description": "null la borra."
                  },
                  "contactListId": {
                    "type": "string"
                  },
                  "template": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string",
                        "pattern": "^[a-z0-9_]+$",
                        "maxLength": 512
                      },
                      "language": {
                        "type": "string",
                        "pattern": "^[a-z]{2,3}(_[A-Z]{2})?$"
                      },
                      "variableMapping": {
                        "description": "Reemplaza el mapeo completo; null lo quita.",
                        "oneOf": [
                          {
                            "type": "object",
                            "description": "Una entrada por variable: clave = número de la variable (\"1\" para {{1}}). Máximo 30.",
                            "propertyNames": {
                              "pattern": "^[1-9][0-9]?$"
                            },
                            "additionalProperties": {
                              "type": "object",
                              "description": "De dónde sale el valor de una variable {{n}} del cuerpo de la plantilla.",
                              "required": [
                                "source"
                              ],
                              "additionalProperties": false,
                              "properties": {
                                "source": {
                                  "type": "string",
                                  "enum": [
                                    "field",
                                    "text"
                                  ],
                                  "description": "field = propiedad del contacto; text = texto fijo."
                                },
                                "key": {
                                  "type": "string",
                                  "maxLength": 100,
                                  "description": "Con source=field (obligatorio): propiedad del contacto, p. ej. firstName."
                                },
                                "value": {
                                  "type": "string",
                                  "maxLength": 1000,
                                  "description": "Con source=text (obligatorio): el texto fijo."
                                },
                                "fallback": {
                                  "type": "string",
                                  "maxLength": 500,
                                  "description": "Valor si el contacto no tiene la propiedad (Meta rechaza variables vacías)."
                                }
                              }
                            }
                          },
                          {
                            "type": "null"
                          }
                        ]
                      },
                      "headerMediaUrl": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "uri",
                        "maxLength": 2048,
                        "description": "null la quita."
                      }
                    }
                  }
                }
              },
              "examples": {
                "cambiarLista": {
                  "value": {
                    "contactListId": "6aaa8f5ffbdd6317ec16c089",
                    "template": {
                      "headerMediaUrl": null
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Borrador actualizado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1campaigns/post/responses/201/content/application~1json/schema"
                },
                "examples": {
                  "updated": {
                    "$ref": "#/paths/~1campaigns/post/responses/201/content/application~1json/examples/created"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1campaigns/post/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Campaña no encontrada en esta organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "La campaña ya salió, se está ejecutando o no es de WhatsApp Marketing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/campaigns/{id}/preflight": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "ID de la campaña (ObjectId)."
        }
      ],
      "get": {
        "operationId": "getCampaignPreflight",
        "tags": [
          "Campaigns"
        ],
        "summary": "A quién le llegaría la campaña hoy y por qué se excluye al resto",
        "description": "Requiere contacts:read o *. Aplica a la lista de la campaña, TAL COMO ESTÁ HOY, la misma selección que usa el envío real: solo el celular (`mobile`) cuenta, y se excluyen `no_mobile`, `landline` (fijo colombiano 57 60X…), `invalid_phone`, `opted_out` (pidió no ser contactado por WhatsApp) y `whatsapp_invalid` (marcado como número sin WhatsApp). Devuelve conteos por motivo y hasta 10 ejemplos repartidos entre motivos, con el teléfono enmascarado (últimos 4 dígitos). Solo lectura. Sin lista configurada → 409 `CAMPAIGN_NOT_CONFIGURED`; lista borrada → 409 `LIST_NOT_FOUND`.",
        "x-required-permissions": [
          "contacts:read"
        ],
        "responses": {
          "200": {
            "description": "Resultado del preflight.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "campaignId",
                        "contactListId",
                        "totalContacts",
                        "willSend",
                        "excludedCount",
                        "excludedByReason",
                        "examples"
                      ],
                      "properties": {
                        "campaignId": {
                          "type": "string"
                        },
                        "contactListId": {
                          "type": "string"
                        },
                        "totalContacts": {
                          "type": "integer",
                          "minimum": 0,
                          "description": "Contactos de la lista hoy."
                        },
                        "willSend": {
                          "type": "integer",
                          "minimum": 0,
                          "description": "Contactos con celular válido que recibirían la plantilla."
                        },
                        "excludedCount": {
                          "type": "integer",
                          "minimum": 0
                        },
                        "excludedByReason": {
                          "type": "array",
                          "description": "Solo los motivos con al menos un contacto.",
                          "items": {
                            "type": "object",
                            "required": [
                              "reason",
                              "label",
                              "count"
                            ],
                            "properties": {
                              "reason": {
                                "$ref": "#/paths/~1campaigns~1{id}~1preflight/get/responses/200/content/application~1json/schema/properties/data/properties/examples/items/properties/reason"
                              },
                              "label": {
                                "type": "string"
                              },
                              "count": {
                                "type": "integer",
                                "minimum": 1
                              }
                            }
                          }
                        },
                        "examples": {
                          "type": "array",
                          "maxItems": 10,
                          "description": "Hasta 10 excluidos, repartidos por turnos entre motivos.",
                          "items": {
                            "type": "object",
                            "required": [
                              "contactId",
                              "name",
                              "phone",
                              "reason",
                              "label"
                            ],
                            "properties": {
                              "contactId": {
                                "type": "string"
                              },
                              "name": {
                                "type": "string"
                              },
                              "phone": {
                                "type": "string",
                                "description": "Enmascarado: solo los últimos 4 dígitos (vacío si no tiene)."
                              },
                              "reason": {
                                "type": "string",
                                "enum": [
                                  "no_mobile",
                                  "landline",
                                  "invalid_phone",
                                  "opted_out",
                                  "whatsapp_invalid"
                                ]
                              },
                              "label": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "preflight": {
                    "summary": "47 contactos, 44 reciben y 3 quedan fuera",
                    "value": {
                      "success": true,
                      "data": {
                        "campaignId": "6aaac4526e2bf21326089c55",
                        "contactListId": "6aaa8f5ffbdd6317ec16c089",
                        "totalContacts": 47,
                        "willSend": 44,
                        "excludedCount": 3,
                        "excludedByReason": [
                          {
                            "reason": "landline",
                            "label": "Número fijo, no es WhatsApp",
                            "count": 2
                          },
                          {
                            "reason": "no_mobile",
                            "label": "Sin celular (solo fijo o correo)",
                            "count": 1
                          }
                        ],
                        "examples": [
                          {
                            "contactId": "66c74b4166baaf700d2db69b",
                            "name": "Recrear La Victoria",
                            "phone": "••••••••2141",
                            "reason": "landline",
                            "label": "Número fijo, no es WhatsApp"
                          },
                          {
                            "contactId": "66c74b5666baaf700d2fd971",
                            "name": "Juan Carlos Gómez Delgado",
                            "phone": "",
                            "reason": "no_mobile",
                            "label": "Sin celular (solo fijo o correo)"
                          },
                          {
                            "contactId": "66c74b5666baaf700d2fd970",
                            "name": "CAJASAN",
                            "phone": "••••••••4444",
                            "reason": "landline",
                            "label": "Número fijo, no es WhatsApp"
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Campaña no encontrada en esta organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "La campaña no tiene lista de contactos o la lista ya no existe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/campaigns/{id}/stats": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "ID de la campaña (ObjectId)."
        }
      ],
      "get": {
        "operationId": "getCampaignStats",
        "tags": [
          "Campaigns"
        ],
        "summary": "Embudo real de una campaña y tabla por contacto",
        "description": "Requiere contacts:read o *. Todo sale de los eventos de marketing de la campaña (uno por contacto y envío, cuyo `status` actualiza el webhook de estados de Meta) cruzados con los mensajes entrantes: `funnel.accepted` es lo que Meta aceptó (HTTP 200) — NO lo entregado; `delivered`/`read` llegan por webhook; `failed` trae el motivo de Meta (número sin WhatsApp, plantilla pausada…); `replied` cuenta contactos con un mensaje entrante posterior al envío; `skipped` son los contactos que la selección ACTUAL de la lista dejaría fuera (sin celular, número fijo, opt-out, marcado como sin WhatsApp). Las filas `failed` son las accionables: depurarlas con `POST /api/v1/campanas/{id}/recipients/mark-invalid` (sesión de usuario). Un ID que no es ObjectId responde 404.",
        "x-required-permissions": [
          "contacts:read"
        ],
        "responses": {
          "200": {
            "description": "Estadísticas de la campaña.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "campaign",
                        "lastSentAt",
                        "funnel",
                        "rates",
                        "failures",
                        "recipients",
                        "executions"
                      ],
                      "properties": {
                        "campaign": {
                          "type": "object",
                          "required": [
                            "id",
                            "name"
                          ],
                          "properties": {
                            "id": {
                              "type": "string"
                            },
                            "name": {
                              "type": "string"
                            },
                            "state": {
                              "type": "string"
                            },
                            "templateName": {
                              "type": "string"
                            },
                            "contactListId": {
                              "type": "string"
                            },
                            "createdAt": {
                              "type": "string",
                              "format": "date-time"
                            }
                          }
                        },
                        "lastSentAt": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        },
                        "funnel": {
                          "type": "object",
                          "required": [
                            "listSize",
                            "attempted",
                            "accepted",
                            "delivered",
                            "read",
                            "failed",
                            "replied",
                            "skipped"
                          ],
                          "properties": {
                            "listSize": {
                              "type": "integer",
                              "minimum": 0,
                              "description": "Contactos en la lista hoy."
                            },
                            "attempted": {
                              "type": "integer",
                              "minimum": 0,
                              "description": "Contactos con al menos un intento (aceptado o fallido)."
                            },
                            "accepted": {
                              "type": "integer",
                              "minimum": 0,
                              "description": "Meta aceptó el envío."
                            },
                            "delivered": {
                              "type": "integer",
                              "minimum": 0
                            },
                            "read": {
                              "type": "integer",
                              "minimum": 0
                            },
                            "failed": {
                              "type": "integer",
                              "minimum": 0
                            },
                            "replied": {
                              "type": "integer",
                              "minimum": 0,
                              "description": "Contactos con un mensaje entrante posterior al envío."
                            },
                            "skipped": {
                              "type": "integer",
                              "minimum": 0,
                              "description": "Contactos que la selección actual de la lista deja fuera."
                            }
                          }
                        },
                        "rates": {
                          "type": "object",
                          "description": "Porcentajes con un decimal; null cuando el denominador es 0.",
                          "required": [
                            "deliveryRate",
                            "readRate",
                            "replyRate",
                            "failureRate"
                          ],
                          "properties": {
                            "deliveryRate": {
                              "type": [
                                "number",
                                "null"
                              ],
                              "description": "delivered / accepted"
                            },
                            "readRate": {
                              "type": [
                                "number",
                                "null"
                              ],
                              "description": "read / delivered"
                            },
                            "replyRate": {
                              "type": [
                                "number",
                                "null"
                              ],
                              "description": "replied / accepted"
                            },
                            "failureRate": {
                              "type": [
                                "number",
                                "null"
                              ],
                              "description": "failed / attempted"
                            }
                          }
                        },
                        "failures": {
                          "type": "array",
                          "description": "Motivos de fallo agrupados, de mayor a menor.",
                          "items": {
                            "type": "object",
                            "required": [
                              "reason",
                              "count"
                            ],
                            "properties": {
                              "reason": {
                                "type": "string"
                              },
                              "count": {
                                "type": "integer",
                                "minimum": 1
                              }
                            }
                          }
                        },
                        "recipients": {
                          "type": "array",
                          "description": "Una fila por contacto; fallidos y excluidos primero.",
                          "items": {
                            "type": "object",
                            "required": [
                              "contactId",
                              "name",
                              "phone",
                              "status"
                            ],
                            "properties": {
                              "contactId": {
                                "type": "string"
                              },
                              "name": {
                                "type": "string"
                              },
                              "phone": {
                                "type": "string",
                                "description": "Celular en dígitos; vacío si el contacto no tiene."
                              },
                              "status": {
                                "type": "string",
                                "enum": [
                                  "sent",
                                  "delivered",
                                  "read",
                                  "failed",
                                  "skipped"
                                ]
                              },
                              "reason": {
                                "type": "string",
                                "description": "Motivo de exclusión (skipped) o error de Meta (failed)."
                              },
                              "errorCode": {
                                "type": [
                                  "integer",
                                  "string"
                                ],
                                "description": "Código de error de Meta, si lo hubo."
                              },
                              "sentAt": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "deliveredAt": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "readAt": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "repliedAt": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "conversationId": {
                                "type": "string",
                                "description": "Conversación del número en el CRM, si existe."
                              }
                            }
                          }
                        },
                        "executions": {
                          "type": "array",
                          "description": "Últimas 10 ejecuciones (lo que la cola hizo, no lo que Meta entregó).",
                          "items": {
                            "type": "object",
                            "required": [
                              "executionId",
                              "status",
                              "errors"
                            ],
                            "properties": {
                              "executionId": {
                                "type": "string"
                              },
                              "status": {
                                "type": "string",
                                "enum": [
                                  "queued",
                                  "running",
                                  "completed",
                                  "failed",
                                  "partial"
                                ]
                              },
                              "stats": {
                                "type": "object",
                                "additionalProperties": true
                              },
                              "startedAt": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "completedAt": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "errors": {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "stats": {
                    "summary": "Embudo de una campaña con un fallo y un excluido",
                    "value": {
                      "success": true,
                      "data": {
                        "campaign": {
                          "id": "6aaac4526e2bf21326089c55",
                          "name": "Santander 2026",
                          "state": "en_curso",
                          "templateName": "prospeccion_manillas_v2",
                          "contactListId": "6aaa8f5ffbdd6317ec16c089",
                          "createdAt": "2026-09-16T16:31:14.720Z"
                        },
                        "lastSentAt": "2026-09-16T16:31:36.000Z",
                        "funnel": {
                          "listSize": 47,
                          "attempted": 44,
                          "accepted": 43,
                          "delivered": 39,
                          "read": 24,
                          "failed": 1,
                          "replied": 18,
                          "skipped": 3
                        },
                        "rates": {
                          "deliveryRate": 90.7,
                          "readRate": 61.5,
                          "replyRate": 41.9,
                          "failureRate": 2.3
                        },
                        "failures": [
                          {
                            "reason": "Message Undeliverable.",
                            "count": 1
                          }
                        ],
                        "recipients": [
                          {
                            "contactId": "66c74b5666baaf700d2fd970",
                            "name": "CAJASAN",
                            "phone": "576076434444",
                            "status": "failed",
                            "reason": "Message Undeliverable.",
                            "errorCode": 131026,
                            "sentAt": "2026-09-16T16:31:30.000Z"
                          },
                          {
                            "contactId": "66c74b4166baaf700d2db69b",
                            "name": "Recrear La Victoria",
                            "phone": "576076472141",
                            "status": "skipped",
                            "reason": "Número fijo, no es WhatsApp"
                          },
                          {
                            "contactId": "6aaa9158fbdd6317ec175051",
                            "name": "Club Acuarela Mesa de los Santos",
                            "phone": "573160237777",
                            "status": "read",
                            "sentAt": "2026-09-16T16:31:30.000Z",
                            "deliveredAt": "2026-09-16T16:31:33.000Z",
                            "readAt": "2026-09-16T16:31:38.000Z",
                            "repliedAt": "2026-09-16T16:31:39.000Z",
                            "conversationId": "6aaac46c6e2bf2132608a8a9"
                          }
                        ],
                        "executions": [
                          {
                            "executionId": "6aaac45c6e2bf21326089ea5",
                            "status": "completed",
                            "stats": {
                              "totalContacts": 47,
                              "validContacts": 44,
                              "sentMessages": 43,
                              "failedMessages": 1,
                              "skippedContacts": 3
                            },
                            "startedAt": "2026-09-16T16:31:24.475Z",
                            "completedAt": "2026-09-16T16:31:36.597Z",
                            "errors": [
                              "3 omitido(s) — Sin celular (solo fijo o correo): Recrear La Victoria (576076472141), Juan Carlos Gómez Delgado, Club Campestre de Bucaramanga"
                            ]
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Campaña no encontrada en esta organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/conversations/config": {
      "get": {
        "operationId": "getConversationsConfig",
        "tags": [
          "Conversations"
        ],
        "summary": "Pipelines de conversaciones y sus etapas",
        "description": "Requiere conversations:read o *. Devuelve todos los pipelines de conversaciones de la organización con sus etapas ordenadas por order: son los IDs que necesitan POST /conversations (pipeline, stage), PUT /conversations/{id}/stage y PUT /conversations/{id}/pipeline. Aquí los identificadores se llaman id; en /conversations/pipelines y /conversations/pipelines/stages se llaman _id.",
        "x-required-permissions": [
          "conversations:read"
        ],
        "responses": {
          "200": {
            "description": "Pipelines con sus etapas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationsConfigResult"
                },
                "examples": {
                  "config": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "507f1f77bcf86cd799439060",
                          "name": "Pipeline de Conversaciones",
                          "isDefault": true,
                          "stages": [
                            {
                              "id": "507f1f77bcf86cd799439061",
                              "name": "Lead",
                              "order": 0,
                              "color": "#4287f5",
                              "autoAssign": false,
                              "webhookEnabled": false
                            },
                            {
                              "id": "507f1f77bcf86cd799439062",
                              "name": "Oportunidad",
                              "order": 2,
                              "color": "#3b82f6",
                              "autoAssign": false,
                              "webhookEnabled": true
                            },
                            {
                              "id": "507f1f77bcf86cd799439063",
                              "name": "Cerrado Ganado",
                              "order": 6,
                              "color": "#22c55e",
                              "autoAssign": false,
                              "webhookEnabled": false
                            }
                          ]
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/conversations/pipelines": {
      "get": {
        "operationId": "listConversationPipelines",
        "tags": [
          "Conversations"
        ],
        "summary": "Listar pipelines de conversaciones",
        "description": "Requiere conversations:read o *. Documentos crudos (_id, name, isDefault), sin etapas.",
        "x-required-permissions": [
          "conversations:read"
        ],
        "responses": {
          "200": {
            "description": "Pipelines de la organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PipelineListResult"
                },
                "examples": {
                  "pipelines": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "_id": "507f1f77bcf86cd799439060",
                          "name": "Pipeline de Conversaciones",
                          "isDefault": true
                        },
                        {
                          "_id": "507f1f77bcf86cd799439064",
                          "name": "Soporte",
                          "isDefault": false
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/conversations/pipelines/stages": {
      "get": {
        "operationId": "listConversationPipelineStages",
        "tags": [
          "Conversations"
        ],
        "summary": "Etapas de un pipeline de conversaciones",
        "description": "Requiere conversations:read o *. Sin pipelineId usa el pipeline default de la organización; si no hay default responde 404 Pipeline not found.",
        "x-required-permissions": [
          "conversations:read"
        ],
        "parameters": [
          {
            "name": "pipelineId",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^[a-fA-F0-9]{24}$"
            },
            "description": "ID del pipeline; debe pertenecer a la organización."
          }
        ],
        "responses": {
          "200": {
            "description": "Pipeline y sus etapas ordenadas por order.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PipelineStagesResult"
                },
                "examples": {
                  "stages": {
                    "value": {
                      "success": true,
                      "data": {
                        "pipeline": {
                          "_id": "507f1f77bcf86cd799439060",
                          "name": "Pipeline de Conversaciones",
                          "isDefault": true
                        },
                        "stages": [
                          {
                            "_id": "507f1f77bcf86cd799439061",
                            "name": "Lead",
                            "order": 0,
                            "color": "#4287f5",
                            "autoAssign": false,
                            "webhookEnabled": false
                          },
                          {
                            "_id": "507f1f77bcf86cd799439062",
                            "name": "Oportunidad",
                            "order": 2,
                            "color": "#3b82f6",
                            "autoAssign": false,
                            "webhookEnabled": true
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "pipelineId no es un ObjectId.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationError"
                },
                "example": {
                  "success": false,
                  "error": "'pipelineId' must be a valid ObjectId"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Pipeline inexistente, de otra organización, o sin default configurado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationError"
                },
                "example": {
                  "success": false,
                  "error": "Pipeline not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/conversations": {
      "get": {
        "operationId": "listConversations",
        "tags": [
          "Conversations"
        ],
        "summary": "Listar conversaciones",
        "description": "Requiere conversations:read o *. Filtra por la organización del API Token. Cada conversación incluye sus últimos messagesLimit mensajes (orden cronológico ascendente) y, si el móvil del contacto existe en el CRM, participants.contact.contactData. Las fechas aceptan dd/mm/yyyy o ISO 8601 y se interpretan como día completo en la zona indicada por timezone (offset en horas; 0 por defecto). Los parámetros search y mobile NO escapan la expresión regular: un valor con + o ( puede responder 500.",
        "x-required-permissions": [
          "conversations:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          },
          {
            "name": "pipeline",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "ID de pipeline de conversaciones."
          },
          {
            "name": "stage",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^[a-fA-F0-9]{24}$"
            },
            "description": "ID de etapa (PipelineStage). Un valor que no sea ObjectId responde 400."
          },
          {
            "name": "assignedTo",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "ID del usuario asignado."
          },
          {
            "name": "priority",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "low",
                "medium",
                "high"
              ]
            }
          },
          {
            "name": "source",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "whatsapp",
                "facebook",
                "instagram"
              ]
            }
          },
          {
            "name": "tags",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Etiquetas separadas por coma; coincide si tiene alguna.",
            "example": "venta,soporte"
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Coincidencia parcial SOLO en title (regex sin escapar). Para buscar por contacto usar /conversations/search."
          },
          {
            "name": "mobile",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Coincidencia parcial en participants.contact.displayInfo.mobile (regex sin escapar)."
          },
          {
            "name": "dateFrom",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "dd/mm/yyyy o ISO 8601; inicio del día en la zona de timezone.",
            "example": "01/09/2026"
          },
          {
            "name": "dateTo",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "dd/mm/yyyy o ISO 8601; fin del día (23:59:59.999) en la zona de timezone.",
            "example": "05/09/2026"
          },
          {
            "name": "dateField",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "createdAt",
                "updatedAt",
                "lastMessageTimestamp"
              ],
              "default": "createdAt"
            },
            "description": "Campo sobre el que aplica el rango de fechas."
          },
          {
            "name": "timezone",
            "in": "query",
            "schema": {
              "type": "number",
              "default": 0
            },
            "description": "Offset UTC en horas de la zona del cliente (-5 para Colombia).",
            "example": -5
          },
          {
            "name": "sortBy",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "lastMessageTimestamp",
                "createdAt",
                "updatedAt"
              ],
              "default": "lastMessageTimestamp"
            }
          },
          {
            "name": "sortOrder",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          },
          {
            "name": "messagesLimit",
            "in": "query",
            "description": "Cantidad de mensajes recientes incluidos por conversación (orden cronológico ascendente).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Conversaciones con mensajes y paginación; data puede estar vacío.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationListResult"
                },
                "examples": {
                  "list": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "507f1f77bcf86cd799439051",
                          "title": "Ana Ejemplo",
                          "pipeline": {
                            "_id": "507f1f77bcf86cd799439060",
                            "name": "Pipeline de Conversaciones",
                            "isDefault": true
                          },
                          "currentStage": {
                            "_id": "507f1f77bcf86cd799439061",
                            "name": "Lead",
                            "order": 0,
                            "color": "#4287f5"
                          },
                          "assignedTo": {
                            "_id": "507f1f77bcf86cd799439001",
                            "firstName": "Alejandro",
                            "lastName": "Cabrejo",
                            "email": "ventas@example.com",
                            "profilePicture": null
                          },
                          "priority": "medium",
                          "tags": [
                            "venta"
                          ],
                          "source": "whatsapp",
                          "unreadCount": 1,
                          "lastMessage": {
                            "_id": "507f1f77bcf86cd799439072",
                            "message": "Gracias por la info",
                            "direction": "incoming",
                            "timestamp": "2026-09-05T15:00:00.000Z",
                            "type": "text",
                            "from": "573000000000",
                            "to": "573100000000",
                            "isRead": false,
                            "source": "whatsapp"
                          },
                          "lastMessageTimestamp": "2026-09-05T15:00:00.000Z",
                          "participants": {
                            "user": {
                              "type": "User",
                              "reference": "507f1f77bcf86cd799439001"
                            },
                            "contact": {
                              "type": "Contact",
                              "reference": "573000000000",
                              "displayInfo": {
                                "mobile": "573000000000",
                                "name": "Ana"
                              }
                            }
                          },
                          "metadata": [],
                          "isArchived": false,
                          "createdAt": "2026-09-01T08:00:00.000Z",
                          "updatedAt": "2026-09-05T15:00:00.000Z",
                          "messages": [
                            {
                              "_id": "507f1f77bcf86cd799439072",
                              "user": "507f1f77bcf86cd799439001",
                              "organization": "507f1f77bcf86cd799439000",
                              "conversation": "507f1f77bcf86cd799439051",
                              "from": "573000000000",
                              "to": "573100000000",
                              "message": "Gracias por la info",
                              "timestamp": "2026-09-05T15:00:00.000Z",
                              "type": "text",
                              "direction": "incoming",
                              "isRead": false,
                              "reactions": [],
                              "source": "whatsapp",
                              "__v": 0
                            }
                          ]
                        }
                      ],
                      "pagination": {
                        "total": 1,
                        "page": 1,
                        "limit": 100,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "stage no es un ObjectId.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationError"
                },
                "example": {
                  "success": false,
                  "error": "El parámetro 'stage' debe ser un ObjectId válido (antes se usaba un índice numérico — ahora se requiere el _id del stage)."
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "post": {
        "operationId": "createConversation",
        "tags": [
          "Conversations"
        ],
        "summary": "Crear una conversación",
        "description": "Requiere conversations:write o *. Sin pipeline usa el default de la organización; sin stage usa la etapa de menor order del pipeline. La conversación nace sin participants ni mensajes (los agrega POST /conversations/{id}/messages), así que /conversations/find-by-phone no la encuentra. Ojo, pipeline y assignedTo NO se validan contra la organización del token. Responde la conversación poblada, sin messages.",
        "x-required-permissions": [
          "conversations:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConversationCreate"
              },
              "examples": {
                "minimal": {
                  "summary": "Solo título; pipeline default y primera etapa.",
                  "value": {
                    "title": "Pedido feria Corferias"
                  }
                },
                "full": {
                  "summary": "Pipeline, etapa y metadata explícitos.",
                  "value": {
                    "title": "Pedido feria Corferias",
                    "pipeline": "507f1f77bcf86cd799439060",
                    "stage": "507f1f77bcf86cd799439061",
                    "assignedTo": "507f1f77bcf86cd799439001",
                    "priority": "high",
                    "tags": [
                      "feria"
                    ],
                    "source": "whatsapp",
                    "metadata": [
                      {
                        "key": "origen",
                        "value": "landing-feria"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Conversación creada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationResult"
                },
                "examples": {
                  "created": {
                    "summary": "Creada por la API — sin participants, sin lastMessage y sin messages.",
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439052",
                        "title": "Pedido feria Corferias",
                        "pipeline": {
                          "_id": "507f1f77bcf86cd799439060",
                          "name": "Pipeline de Conversaciones",
                          "isDefault": true
                        },
                        "currentStage": {
                          "_id": "507f1f77bcf86cd799439061",
                          "name": "Lead",
                          "order": 0,
                          "color": "#4287f5"
                        },
                        "priority": "high",
                        "tags": [
                          "feria"
                        ],
                        "source": "whatsapp",
                        "unreadCount": 0,
                        "metadata": [
                          {
                            "_id": "507f1f77bcf86cd799439080",
                            "key": "origen",
                            "value": "landing-feria"
                          }
                        ],
                        "isArchived": false,
                        "createdAt": "2026-09-05T16:00:00.000Z",
                        "updatedAt": "2026-09-05T16:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Falta title, IDs inválidos, etapa fuera del pipeline, o pipeline sin default/etapas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationError"
                },
                "examples": {
                  "title": {
                    "value": {
                      "success": false,
                      "error": "'title' is required"
                    }
                  },
                  "pipeline": {
                    "value": {
                      "success": false,
                      "error": "'pipeline' must be a valid ObjectId"
                    }
                  },
                  "noDefault": {
                    "value": {
                      "success": false,
                      "error": "No default pipeline configured for this organization. Pass 'pipeline' explicitly."
                    }
                  },
                  "stage": {
                    "value": {
                      "success": false,
                      "error": "'stage' must be a valid ObjectId"
                    }
                  },
                  "stageNotInPipeline": {
                    "value": {
                      "success": false,
                      "error": "'stage' does not belong to the specified pipeline"
                    }
                  },
                  "noStages": {
                    "value": {
                      "success": false,
                      "error": "The pipeline has no stages configured"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/conversations/search": {
      "get": {
        "operationId": "searchConversations",
        "tags": [
          "Conversations"
        ],
        "summary": "Buscar conversaciones por título o contacto",
        "description": "Requiere conversations:read o *. Coincidencia parcial sin distinguir mayúsculas (regex escapada) en title, nombre, apellido, móvil y referencia del contacto. NO busca dentro del contenido de los mensajes. Ordena por lastMessageTimestamp descendente e incluye mensajes como el listado.",
        "x-required-permissions": [
          "conversations:read"
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "example": "Ana"
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          },
          {
            "$ref": "#/paths/~1conversations/get/parameters/16"
          }
        ],
        "responses": {
          "200": {
            "description": "Conversaciones que coinciden, con mensajes y paginación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationListResult"
                },
                "examples": {
                  "list": {
                    "$ref": "#/paths/~1conversations/get/responses/200/content/application~1json/examples/list"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Falta q (o está vacío).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationError"
                },
                "example": {
                  "success": false,
                  "error": "Query param 'q' is required"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/conversations/find-by-phone": {
      "get": {
        "operationId": "findConversationByPhone",
        "tags": [
          "Conversations"
        ],
        "summary": "Conversación más reciente de un teléfono",
        "description": "Requiere conversations:read o *. Busca por coincidencia parcial (regex escapada, con distinción de mayúsculas) en participants.contact.reference y devuelve la de lastMessageTimestamp más reciente, con sus mensajes. Solo encuentra conversaciones con participants (las que entran por un canal), no las creadas por esta API.",
        "x-required-permissions": [
          "conversations:read"
        ],
        "parameters": [
          {
            "name": "phone",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "description": "Dígitos del número; una subcadena también coincide.",
            "example": "573000000000"
          },
          {
            "$ref": "#/paths/~1conversations/get/parameters/16"
          }
        ],
        "responses": {
          "200": {
            "description": "Conversación encontrada con mensajes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationDetailResult"
                },
                "examples": {
                  "found": {
                    "$ref": "#/paths/~1conversations~1{id}/get/responses/200/content/application~1json/examples/found"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Falta phone.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationError"
                },
                "example": {
                  "success": false,
                  "error": "Query param 'phone' is required"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Ningún participante de la organización coincide con ese teléfono.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationError"
                },
                "example": {
                  "success": false,
                  "error": "No active conversation found for that phone"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/conversations/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB de la conversación.",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "507f1f77bcf86cd799439051"
        }
      ],
      "get": {
        "operationId": "getConversationById",
        "tags": [
          "Conversations"
        ],
        "summary": "Consultar una conversación por ID",
        "description": "Requiere conversations:read o *. La conversación debe pertenecer a la organización del token. Incluye los últimos messagesLimit mensajes en orden cronológico ascendente. Para leer historial más antiguo, repetir con messagesBefore = messagesPagination.nextBefore mientras messagesPagination.hasMore sea true.",
        "x-required-permissions": [
          "conversations:read"
        ],
        "parameters": [
          {
            "$ref": "#/paths/~1conversations/get/parameters/16"
          },
          {
            "name": "messagesBefore",
            "in": "query",
            "description": "Cursor hacia atrás: timestamp ISO 8601 o _id de un mensaje de la conversación. Devuelve los messagesLimit mensajes estrictamente anteriores a ese punto. Para seguir paginando, usar el valor de messagesPagination.nextBefore de la respuesta previa.",
            "schema": {
              "type": "string"
            },
            "example": "2026-09-18T20:05:00.000Z"
          }
        ],
        "responses": {
          "200": {
            "description": "Conversación encontrada con mensajes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationDetailResult"
                },
                "examples": {
                  "found": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439051",
                        "title": "Ana Ejemplo",
                        "pipeline": {
                          "_id": "507f1f77bcf86cd799439060",
                          "name": "Pipeline de Conversaciones",
                          "isDefault": true
                        },
                        "currentStage": {
                          "_id": "507f1f77bcf86cd799439061",
                          "name": "Lead",
                          "order": 0,
                          "color": "#4287f5"
                        },
                        "assignedTo": {
                          "_id": "507f1f77bcf86cd799439001",
                          "firstName": "Alejandro",
                          "lastName": "Cabrejo",
                          "email": "ventas@example.com",
                          "profilePicture": null
                        },
                        "priority": "medium",
                        "tags": [
                          "venta"
                        ],
                        "source": "whatsapp",
                        "unreadCount": 1,
                        "lastMessage": {
                          "_id": "507f1f77bcf86cd799439072",
                          "message": "Gracias por la info",
                          "direction": "incoming",
                          "timestamp": "2026-09-05T15:00:00.000Z",
                          "type": "text",
                          "from": "573000000000",
                          "to": "573100000000",
                          "isRead": false,
                          "source": "whatsapp"
                        },
                        "lastMessageTimestamp": "2026-09-05T15:00:00.000Z",
                        "participants": {
                          "user": {
                            "type": "User",
                            "reference": "507f1f77bcf86cd799439001"
                          },
                          "contact": {
                            "type": "Contact",
                            "reference": "573000000000",
                            "displayInfo": {
                              "mobile": "573000000000",
                              "name": "Ana"
                            },
                            "contactData": {
                              "id": "507f1f77bcf86cd799439011",
                              "firstName": "Ana",
                              "lastName": "Ejemplo",
                              "fullName": "Ana Ejemplo",
                              "email": "ana@example.com",
                              "mobile": "573000000000",
                              "company": "Microm S.A.S",
                              "position": "",
                              "leadScore": 12
                            }
                          }
                        },
                        "metadata": [],
                        "isArchived": false,
                        "createdAt": "2026-09-01T08:00:00.000Z",
                        "updatedAt": "2026-09-05T15:00:00.000Z",
                        "messages": [
                          {
                            "_id": "507f1f77bcf86cd799439071",
                            "user": "507f1f77bcf86cd799439001",
                            "organization": "507f1f77bcf86cd799439000",
                            "conversation": "507f1f77bcf86cd799439051",
                            "from": "573100000000",
                            "to": "573000000000",
                            "message": "Hola Ana, ¿en qué te ayudo?",
                            "timestamp": "2026-09-05T14:58:00.000Z",
                            "type": "text",
                            "direction": "outgoing",
                            "isRead": true,
                            "messageId": "wamid.HBgMNTczMDAwMDAwMDAwFQIAERgSMUExQjJDM0Q0RTVGNkc3SDhJAA==",
                            "reactions": [],
                            "source": "whatsapp",
                            "deliveryStatus": "delivered",
                            "__v": 0
                          },
                          {
                            "_id": "507f1f77bcf86cd799439072",
                            "user": "507f1f77bcf86cd799439001",
                            "organization": "507f1f77bcf86cd799439000",
                            "conversation": "507f1f77bcf86cd799439051",
                            "from": "573000000000",
                            "to": "573100000000",
                            "message": "Gracias por la info",
                            "timestamp": "2026-09-05T15:00:00.000Z",
                            "type": "text",
                            "direction": "incoming",
                            "isRead": false,
                            "possibleName": "Ana",
                            "messageId": "wamid.HBgMNTczMDAwMDAwMDAwFQIAEhgUM0E5QjhDN0Q2RTVGNEczSDJJMUsA",
                            "reactions": [],
                            "source": "whatsapp",
                            "__v": 0
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID de conversación inválido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationError"
                },
                "example": {
                  "success": false,
                  "error": "Invalid conversation ID"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Conversación inexistente o no perteneciente a la organización del token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationError"
                },
                "example": {
                  "success": false,
                  "error": "Conversation not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "put": {
        "operationId": "updateConversationById",
        "tags": [
          "Conversations"
        ],
        "summary": "Actualizar parcialmente una conversación",
        "description": "Requiere conversations:write o *. Solo title, assignedTo, assignedAgentId, priority, tags y metadata; el resto del body se ignora y un body sin ninguno responde 400. assignedTo (humano) y assignedAgentId (agente IA) son excluyentes: asignar uno limpia el otro, y asignar el agente reanuda sus automatizaciones. No mueve de etapa (usar /conversations/{id}/stage). Responde la conversación poblada sin messages y con lastMessage como ID.",
        "x-required-permissions": [
          "conversations:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConversationUpdate"
              },
              "examples": {
                "assignHuman": {
                  "value": {
                    "assignedTo": "507f1f77bcf86cd799439001",
                    "priority": "high",
                    "tags": [
                      "venta",
                      "urgente"
                    ]
                  }
                },
                "assignAgent": {
                  "summary": "Pasar la conversación a un agente IA.",
                  "value": {
                    "assignedAgentId": "507f1f77bcf86cd7994390a1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Conversación actualizada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationResult"
                },
                "examples": {
                  "updated": {
                    "summary": "Respuesta de PUT — lastMessage llega como ID (no se puebla) y no incluye messages.",
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439051",
                        "title": "Ana Ejemplo",
                        "pipeline": {
                          "_id": "507f1f77bcf86cd799439060",
                          "name": "Pipeline de Conversaciones",
                          "isDefault": true
                        },
                        "currentStage": {
                          "_id": "507f1f77bcf86cd799439062",
                          "name": "Oportunidad",
                          "order": 2,
                          "color": "#3b82f6"
                        },
                        "assignedTo": {
                          "_id": "507f1f77bcf86cd799439001",
                          "firstName": "Alejandro",
                          "lastName": "Cabrejo",
                          "email": "ventas@example.com",
                          "profilePicture": null
                        },
                        "priority": "high",
                        "tags": [
                          "venta",
                          "urgente"
                        ],
                        "source": "whatsapp",
                        "unreadCount": 1,
                        "lastMessage": "507f1f77bcf86cd799439072",
                        "lastMessageTimestamp": "2026-09-05T15:00:00.000Z",
                        "participants": {
                          "user": {
                            "type": "User",
                            "reference": "507f1f77bcf86cd799439001"
                          },
                          "contact": {
                            "type": "Contact",
                            "reference": "573000000000",
                            "displayInfo": {
                              "mobile": "573000000000",
                              "name": "Ana"
                            }
                          }
                        },
                        "metadata": [],
                        "isArchived": false,
                        "createdAt": "2026-09-01T08:00:00.000Z",
                        "updatedAt": "2026-09-05T16:30:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID inválido, body sin campos válidos, ambos asignados a la vez, o assignedAgentId inválido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationError"
                },
                "examples": {
                  "invalidId": {
                    "value": {
                      "success": false,
                      "error": "Invalid conversation ID"
                    }
                  },
                  "noFields": {
                    "value": {
                      "success": false,
                      "error": "No valid fields to update"
                    }
                  },
                  "both": {
                    "value": {
                      "success": false,
                      "error": "No puedes asignar assignedTo y assignedAgentId al mismo tiempo. Son mutuamente excluyentes."
                    }
                  },
                  "agentId": {
                    "value": {
                      "success": false,
                      "error": "assignedAgentId inválido"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Conversación inexistente (o de otra organización) o agente IA no encontrado en la organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationError"
                },
                "examples": {
                  "conversation": {
                    "value": {
                      "success": false,
                      "error": "Conversation not found"
                    }
                  },
                  "agent": {
                    "value": {
                      "success": false,
                      "error": "Agente no encontrado en esta organización"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "delete": {
        "operationId": "deleteConversationById",
        "tags": [
          "Conversations"
        ],
        "summary": "Eliminar una conversación",
        "description": "Requiere conversations:delete o *. Borra la conversación y TODOS sus mensajes. No responde 204.",
        "x-required-permissions": [
          "conversations:delete"
        ],
        "responses": {
          "200": {
            "description": "Conversación y mensajes eliminados.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationDeleteResult"
                },
                "examples": {
                  "deleted": {
                    "value": {
                      "success": true,
                      "message": "Conversation deleted successfully",
                      "data": {
                        "id": "507f1f77bcf86cd799439051"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1conversations~1{id}/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1conversations~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/conversations/{id}/stage-history": {
      "parameters": [
        {
          "$ref": "#/paths/~1conversations~1{id}/parameters/0"
        }
      ],
      "get": {
        "operationId": "getConversationStageHistory",
        "tags": [
          "Conversations"
        ],
        "summary": "Historial de cambios de etapa",
        "description": "Requiere conversations:read o *. Lee los logs CONVERSATION_STAGE_CHANGE de la conversación, más reciente primero. Los cambios de pipeline NO aparecen (se registran como CONVERSATION_PIPELINE_CHANGE). La paginación va plana dentro de data.",
        "x-required-permissions": [
          "conversations:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Historial paginado; items puede estar vacío.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StageHistoryResult"
                },
                "examples": {
                  "history": {
                    "value": {
                      "success": true,
                      "data": {
                        "conversationId": "507f1f77bcf86cd799439051",
                        "total": 1,
                        "page": 1,
                        "limit": 100,
                        "items": [
                          {
                            "_id": "507f1f77bcf86cd799439090",
                            "timestamp": "2026-09-05T16:30:00.000Z",
                            "triggeredBy": "system",
                            "user": {
                              "userId": "507f1f77bcf86cd799439001",
                              "email": "ventas@example.com"
                            },
                            "fromStage": {
                              "_id": "507f1f77bcf86cd799439061",
                              "name": "Lead",
                              "order": 0
                            },
                            "toStage": {
                              "_id": "507f1f77bcf86cd799439062",
                              "name": "Oportunidad",
                              "order": 2
                            },
                            "pipeline": {
                              "_id": "507f1f77bcf86cd799439060",
                              "name": "Pipeline de Conversaciones"
                            },
                            "conversationTitle": "Ana Ejemplo"
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1conversations~1{id}/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1conversations~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/conversations/{id}/stage": {
      "parameters": [
        {
          "$ref": "#/paths/~1conversations~1{id}/parameters/0"
        }
      ],
      "put": {
        "operationId": "moveConversationStage",
        "tags": [
          "Conversations"
        ],
        "summary": "Mover la conversación a otra etapa",
        "description": "Requiere conversations:write o *. La etapa debe pertenecer al pipeline ACTUAL de la conversación. Si la etapa destino tiene autoAssign con un usuario, reasigna la conversación. Si la etapa cambió de verdad, registra el log de historial y dispara el webhook configurado en la etapa destino (fire-and-forget). Mover a la misma etapa responde 200 sin registrar nada. Responde la conversación poblada sin messages.",
        "x-required-permissions": [
          "conversations:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MoveStageRequest"
              },
              "examples": {
                "move": {
                  "value": {
                    "stageId": "507f1f77bcf86cd799439062"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Conversación en la etapa destino.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationResult"
                },
                "examples": {
                  "moved": {
                    "$ref": "#/paths/~1conversations~1{id}/put/responses/200/content/application~1json/examples/updated"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID inválido, stageId inválido o etapa de otro pipeline.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationError"
                },
                "examples": {
                  "invalidId": {
                    "value": {
                      "success": false,
                      "error": "Invalid conversation ID"
                    }
                  },
                  "stageId": {
                    "value": {
                      "success": false,
                      "error": "'stageId' must be a valid ObjectId"
                    }
                  },
                  "otherPipeline": {
                    "value": {
                      "success": false,
                      "error": "Stage does not belong to the conversation's pipeline"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1conversations~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/conversations/{id}/pipeline": {
      "parameters": [
        {
          "$ref": "#/paths/~1conversations~1{id}/parameters/0"
        }
      ],
      "put": {
        "operationId": "changeConversationPipeline",
        "tags": [
          "Conversations"
        ],
        "summary": "Cambiar la conversación de pipeline",
        "description": "Requiere conversations:write o *. El pipeline debe pertenecer a la organización. Sin initialStageId cae en la etapa de menor order del pipeline destino. Aplica el autoAssign de la etapa destino, registra el log CONVERSATION_PIPELINE_CHANGE y dispara el webhook de la etapa destino SIEMPRE (aunque sea el mismo pipeline y etapa). Responde la conversación poblada sin messages.",
        "x-required-permissions": [
          "conversations:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChangePipelineRequest"
              },
              "examples": {
                "firstStage": {
                  "summary": "Al pipeline de soporte, en su primera etapa.",
                  "value": {
                    "pipelineId": "507f1f77bcf86cd799439064"
                  }
                },
                "explicitStage": {
                  "value": {
                    "pipelineId": "507f1f77bcf86cd799439064",
                    "initialStageId": "507f1f77bcf86cd799439065"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Conversación en el pipeline destino.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationResult"
                },
                "examples": {
                  "moved": {
                    "$ref": "#/paths/~1conversations~1{id}/put/responses/200/content/application~1json/examples/updated"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID inválido, pipelineId/initialStageId inválidos, etapa fuera del pipeline o pipeline sin etapas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationError"
                },
                "examples": {
                  "invalidId": {
                    "value": {
                      "success": false,
                      "error": "Invalid conversation ID"
                    }
                  },
                  "pipelineId": {
                    "value": {
                      "success": false,
                      "error": "'pipelineId' must be a valid ObjectId"
                    }
                  },
                  "initialStageId": {
                    "value": {
                      "success": false,
                      "error": "'initialStageId' must be a valid ObjectId"
                    }
                  },
                  "stageNotInPipeline": {
                    "value": {
                      "success": false,
                      "error": "'initialStageId' does not belong to the specified pipeline"
                    }
                  },
                  "noStages": {
                    "value": {
                      "success": false,
                      "error": "The target pipeline has no stages configured"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Pipeline destino inexistente o de otra organización, o conversación inexistente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationError"
                },
                "examples": {
                  "pipeline": {
                    "value": {
                      "success": false,
                      "error": "Pipeline not found"
                    }
                  },
                  "conversation": {
                    "value": {
                      "success": false,
                      "error": "Conversation not found"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/conversations/{id}/messages": {
      "parameters": [
        {
          "$ref": "#/paths/~1conversations~1{id}/parameters/0"
        }
      ],
      "post": {
        "operationId": "addConversationMessage",
        "tags": [
          "Conversations"
        ],
        "summary": "Agregar un mensaje a la conversación",
        "description": "Requiere conversations:write o *. Solo registra el mensaje en el CRM: NO lo envía por WhatsApp ni por ningún canal (para enviar usar /whatsapp). Un incoming incrementa unreadCount; un outgoing nace leído. Actualiza lastMessage y lastMessageTimestamp. Responde el documento del mensaje tal como quedó guardado, con user = dueño del API Token. from y to son opcionales: si faltan se derivan del contacto de la conversación (participants.contact.reference) y del número de la integración de WhatsApp de la organización (fallback: el contacto en ambos lados) según direction (incoming: contacto → negocio; outgoing: negocio → contacto). Si aun así no se pueden derivar (conversación sin contacto) responde 400. Arreglado el 05-sep-2026: antes respondía 500 siempre por user/from/to vacíos.",
        "x-required-permissions": [
          "conversations:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MessageCreate"
              },
              "examples": {
                "outgoing": {
                  "summary": "from/to explícitos.",
                  "value": {
                    "message": "Hola, con gusto te ayudo",
                    "direction": "outgoing",
                    "from": "573100000000",
                    "to": "573000000000"
                  }
                },
                "outgoingDerived": {
                  "summary": "Sin from/to — se derivan del contacto y de la integración.",
                  "value": {
                    "message": "Tu pedido ya salió",
                    "direction": "outgoing"
                  }
                },
                "incomingMedia": {
                  "summary": "Entrante con imagen.",
                  "value": {
                    "message": "Foto del pedido",
                    "direction": "incoming",
                    "from": "573000000000",
                    "to": "573100000000",
                    "type": "image",
                    "mediaUrl": "https://example.com/pedido.jpg"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Mensaje registrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResult"
                },
                "examples": {
                  "created": {
                    "value": {
                      "success": true,
                      "data": {
                        "_id": "507f1f77bcf86cd799439073",
                        "user": "507f1f77bcf86cd799439011",
                        "organization": "507f1f77bcf86cd799439000",
                        "conversation": "507f1f77bcf86cd799439051",
                        "message": "Hola, con gusto te ayudo",
                        "direction": "outgoing",
                        "from": "573100000000",
                        "to": "573000000000",
                        "type": "text",
                        "timestamp": "2026-09-05T16:40:00.000Z",
                        "isRead": true,
                        "source": "whatsapp",
                        "reactions": [],
                        "__v": 0
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID inválido, falta message, direction no es incoming/outgoing, o no se pudo derivar from/to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationError"
                },
                "examples": {
                  "invalidId": {
                    "value": {
                      "success": false,
                      "error": "Invalid conversation ID"
                    }
                  },
                  "message": {
                    "value": {
                      "success": false,
                      "error": "'message' is required"
                    }
                  },
                  "direction": {
                    "value": {
                      "success": false,
                      "error": "'direction' must be 'incoming' or 'outgoing'"
                    }
                  },
                  "fromTo": {
                    "value": {
                      "success": false,
                      "error": "'from' and 'to' are required (could not be derived from the conversation contact)"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Token inválido, o token sin usuario asociado (User not found in token).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationError"
                },
                "example": {
                  "success": false,
                  "error": "User not found in token"
                }
              }
            }
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1conversations~1{id}/get/responses/404"
          },
          "500": {
            "description": "Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationError"
                },
                "example": {
                  "success": false,
                  "error": "Internal server error"
                }
              }
            }
          }
        }
      }
    },
    "/quotations": {
      "get": {
        "operationId": "listQuotations",
        "tags": [
          "Quotations"
        ],
        "summary": "Listar cotizaciones",
        "description": "Requiere quotations:read o *. Filtra por la organización del API Token y devuelve el contacto aplanado en cada cotización. createdFrom/createdTo se comparan contra creationDate como instantes exactos (new Date(valor)): un createdTo=2026-09-05 es la medianoche UTC de ese día y EXCLUYE el resto del día, a diferencia de /deals. search NO escapa la expresión regular (un + o ( pueden responder 500); contactId no se valida como ObjectId (un valor inválido responde 500).",
        "x-required-permissions": [
          "quotations:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Coincidencia parcial literal en name, sin distinguir mayúsculas (el texto se escapa)."
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "draft",
                  "ready",
                  "sent",
                  "accepted",
                  "rejected",
                  "expired",
                  "invoiced",
                  "canceled",
                  "sent_unviewed"
                ],
                "description": "Un estado de cotización o el pseudo-filtro sent_unviewed (enviadas sin ver: sent con viewCount 0 o ausente). sent_unviewed no es un estado: solo filtra."
              }
            },
            "style": "form",
            "explode": true,
            "description": "Uno o varios estados (OR). Acepta separados por coma (status=sent,accepted), repetidos (status=sent&status=accepted) o status[]=sent&status[]=accepted. Las cotizaciones viejas sin estado filtran como draft. sent_unviewed = enviadas sin ver (sent con viewCount 0). Un valor desconocido responde 400 INVALID_STATUS_FILTER.",
            "example": [
              "sent",
              "accepted"
            ]
          },
          {
            "name": "contactId",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "ID del contacto."
          },
          {
            "name": "createdFrom",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Instante mínimo de creationDate (inclusive). Acepta cualquier fecha parseable por Date.",
            "example": "2026-09-01T00:00:00.000Z"
          },
          {
            "name": "createdTo",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Instante máximo de creationDate (inclusive). Para cubrir un día completo enviar su fin (T23:59:59.999Z).",
            "example": "2026-09-30T23:59:59.999Z"
          },
          {
            "name": "sortBy",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "creationDate",
                "lastModified",
                "total",
                "quotationNumber"
              ],
              "default": "creationDate"
            },
            "description": "Un valor fuera del enum cae a creationDate sin error."
          },
          {
            "name": "sortOrder",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cotizaciones y paginación; data puede estar vacío.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuotationListResult"
                },
                "examples": {
                  "list": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "507f1f77bcf86cd799439051",
                          "quotationNumber": 42,
                          "name": "42 - Ana Ejemplo",
                          "status": "draft",
                          "contact": {
                            "id": "507f1f77bcf86cd799439011",
                            "fullName": "Ana Ejemplo",
                            "company": "Microm S.A.S",
                            "email": "ana@example.com",
                            "mobile": "573000000000"
                          },
                          "items": [
                            {
                              "_id": "507f1f77bcf86cd799439061",
                              "name": "Manilla Tyvek 19mm",
                              "quantity": 500,
                              "unitPrice": 400,
                              "taxes": 19,
                              "total": 238000
                            }
                          ],
                          "subtotal": 200000,
                          "taxes": 38000,
                          "discounts": 0,
                          "total": 238000,
                          "paymentTerms": "50% anticipo, 50% contra entrega",
                          "shippingTerms": "5 días hábiles",
                          "expirationDate": "2026-10-05T15:00:00.000Z",
                          "creationDate": "2026-09-05T15:00:00.000Z",
                          "lastModified": "2026-09-05T15:00:00.000Z"
                        }
                      ],
                      "pagination": {
                        "total": 1,
                        "page": 1,
                        "limit": 100,
                        "pages": 1
                      },
                      "statusCounts": {
                        "draft": 1,
                        "ready": 0,
                        "sent": 0,
                        "accepted": 0,
                        "rejected": 0,
                        "expired": 0,
                        "invoiced": 0,
                        "canceled": 0,
                        "sent_unviewed": 0,
                        "total": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Algún valor de status no es un estado ni sent_unviewed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Filtro de listado inválido.",
                  "required": [
                    "success",
                    "error",
                    "code"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": false
                    },
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "INVALID_STATUS_FILTER"
                      ]
                    }
                  }
                },
                "example": {
                  "success": false,
                  "error": "Estado inválido: pending. Valores admitidos: draft, ready, sent, accepted, rejected, expired, invoiced, canceled, sent_unviewed",
                  "code": "INVALID_STATUS_FILTER"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "description": "Error interno (incluye fallos de validación del modelo Mongoose y filtros inválidos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuotationError"
                },
                "example": {
                  "success": false,
                  "error": "Internal server error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createQuotation",
        "tags": [
          "Quotations"
        ],
        "summary": "Crear una cotización",
        "description": "Requiere quotations:write o *. Aplica el límite de cotizaciones del plan (PLAN_LIMIT_REACHED → 403). Contrato legacy con claves en español: Cliente, cotizacion y totals. Busca el contacto por la property mobile exacta y lo crea si no existe. El número lo asigna el servidor (correlativo por organización; si la creación falla NO se revierte: un hueco es preferible a un duplicado). Todo el body se valida ANTES de consumir número o tocar el contacto: cliente, ítems, totales, términos y fecha inválidos responden 400 con `fields`. QUOTATION_VALIDATION_FAILED es la red de seguridad de Mongoose y sí puede llegar con el número ya consumido. Responde el documento crudo con el contacto poblado, NO el formato aplanado de GET.",
        "x-required-permissions": [
          "quotations:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuotationCreate"
              },
              "examples": {
                "spanish": {
                  "summary": "Claves en español (formato original de n8n/Elementor).",
                  "value": {
                    "Cliente": {
                      "Nombre": "Ana Ejemplo",
                      "Mobile": "573000000000",
                      "Email": "ana@example.com",
                      "Nit": "900123456-1",
                      "Empresa": "Microm S.A.S",
                      "Ciudad": "Medellín"
                    },
                    "cotizacion": [
                      {
                        "producto": "Manilla Tyvek 19mm",
                        "descripcion": "Impresa a 1 tinta",
                        "cantidad": 500,
                        "precio": 400,
                        "iva": 19,
                        "total": 238000
                      }
                    ],
                    "totals": {
                      "subtotal": 200000,
                      "iva": 38000,
                      "total": 238000,
                      "forma_De_Pago": "50% anticipo, 50% contra entrega",
                      "tiempo_de_entrega": "5 días hábiles"
                    }
                  }
                },
                "english": {
                  "summary": "Alias en inglés para Cliente y montos como cadena.",
                  "value": {
                    "Cliente": {
                      "name": "Ana Ejemplo",
                      "mobile": "573000000000",
                      "email": "ana@example.com"
                    },
                    "cotizacion": [
                      {
                        "producto": "Manilla Tyvek 19mm",
                        "cantidad": "500",
                        "precio": "400",
                        "iva": "19",
                        "total": "238000",
                        "imagen": "https://cdn.example.com/tyvek.png"
                      }
                    ],
                    "totals": {
                      "subtotal": "200000",
                      "iva": "38000",
                      "total": "238000",
                      "forma_de_pago": "Contado",
                      "tiempo_De_Entrega": "5 días hábiles"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Cotización creada (documento crudo).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuotationCreatedResult"
                },
                "examples": {
                  "created": {
                    "summary": "Documento crudo con el contacto poblado (formato legacy del POST).",
                    "value": {
                      "message": "Cotización creada exitosamente",
                      "data": {
                        "_id": "507f1f77bcf86cd799439051",
                        "quotationNumber": 42,
                        "name": "42 - Ana Ejemplo",
                        "observaciones": "Forma de pago: 50% anticipo, 50% contra entrega. Tiempo de entrega: 5 días hábiles",
                        "contactId": {
                          "_id": "507f1f77bcf86cd799439011",
                          "organizationId": "507f1f77bcf86cd799439012",
                          "properties": [
                            {
                              "key": "firstName",
                              "value": "Ana Ejemplo",
                              "isVisible": true
                            },
                            {
                              "key": "lastName",
                              "value": "",
                              "isVisible": true
                            },
                            {
                              "key": "email",
                              "value": "ana@example.com",
                              "isVisible": false
                            },
                            {
                              "key": "mobile",
                              "value": "573000000000",
                              "isVisible": true
                            },
                            {
                              "key": "idType",
                              "value": "NIT",
                              "isVisible": false
                            },
                            {
                              "key": "idNumber",
                              "value": "900123456-1",
                              "isVisible": false
                            },
                            {
                              "key": "companyName",
                              "value": "Microm S.A.S",
                              "isVisible": false
                            }
                          ],
                          "leadScore": 0,
                          "createdAt": "2026-09-05T15:00:00.000Z",
                          "updatedAt": "2026-09-05T15:00:00.000Z"
                        },
                        "organizationId": "507f1f77bcf86cd799439012",
                        "userId": "507f1f77bcf86cd799439001",
                        "creationDate": "2026-09-05T15:00:00.000Z",
                        "expirationDate": "2026-10-05T15:00:00.000Z",
                        "lastModified": "2026-09-05T15:00:00.000Z",
                        "paymentTerms": "50% anticipo, 50% contra entrega",
                        "shippingTerms": "5 días hábiles",
                        "items": [
                          {
                            "_id": "507f1f77bcf86cd799439061",
                            "name": "Manilla Tyvek 19mm",
                            "description": "Impresa a 1 tinta",
                            "quantity": 500,
                            "unitPrice": 400,
                            "taxes": 19,
                            "total": 238000,
                            "imageUrl": ""
                          }
                        ],
                        "optionalItems": [],
                        "subtotal": 200000,
                        "taxes": 38000,
                        "discounts": 0,
                        "total": 238000,
                        "status": "draft",
                        "viewCount": 0,
                        "createdAt": "2026-09-05T15:00:00.000Z",
                        "updatedAt": "2026-09-05T15:00:00.000Z",
                        "__v": 0
                      },
                      "quotationNumber": 42
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Body inválido, faltan Cliente/cotizacion/totals, cliente sin nombre o móvil, lista de ítems vacía, ítem o totales no numéricos, sin términos, fecha inválida u organización inexistente. Ningún 400 consume número, salvo QUOTATION_VALIDATION_FAILED (red de seguridad de Mongoose, llega después de reservar).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuotationCodedError"
                },
                "examples": {
                  "body": {
                    "value": {
                      "message": "El body debe ser un objeto JSON válido",
                      "code": "INVALID_BODY_FORMAT"
                    }
                  },
                  "missing": {
                    "value": {
                      "message": "Faltan datos requeridos: Cliente, cotizacion y totals son obligatorios",
                      "code": "MISSING_REQUIRED_FIELDS"
                    }
                  },
                  "client": {
                    "value": {
                      "message": "El cliente debe tener mobile y name (o Mobile y Nombre)",
                      "code": "INVALID_CLIENT_DATA"
                    }
                  },
                  "items": {
                    "value": {
                      "message": "La cotización debe tener al menos un item",
                      "code": "EMPTY_QUOTATION_ITEMS"
                    }
                  },
                  "organization": {
                    "value": {
                      "message": "Organización no encontrada",
                      "code": "ORGANIZATION_NOT_FOUND"
                    }
                  },
                  "badItems": {
                    "value": {
                      "message": "Cada ítem necesita name (o producto), quantity (o cantidad), unitPrice (o precio) y total numéricos",
                      "code": "INVALID_QUOTATION_ITEMS",
                      "fields": [
                        "cotizacion[0].unitPrice"
                      ]
                    }
                  },
                  "totals": {
                    "value": {
                      "message": "totals.subtotal y totals.total deben ser numéricos",
                      "code": "INVALID_QUOTATION_TOTALS",
                      "fields": [
                        "totals.total"
                      ]
                    }
                  },
                  "terms": {
                    "value": {
                      "message": "Faltan los términos de la cotización: manda paymentTerms y shippingTerms, o configúralos en Ajustes > Cotizaciones",
                      "code": "MISSING_QUOTATION_TERMS",
                      "fields": [
                        "paymentTerms",
                        "shippingTerms"
                      ]
                    }
                  },
                  "expiration": {
                    "value": {
                      "message": "expirationDate debe ser una fecha (yyyy-MM-dd o ISO)",
                      "code": "INVALID_EXPIRATION_DATE",
                      "fields": [
                        "expirationDate"
                      ]
                    }
                  },
                  "validation": {
                    "value": {
                      "message": "La cotización no es válida",
                      "code": "QUOTATION_VALIDATION_FAILED",
                      "fields": [
                        "items.0.name"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "La organización desapareció entre la lectura y el incremento del correlativo (carrera improbable).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuotationCodedError"
                },
                "example": {
                  "message": "Organización no encontrada durante la actualización",
                  "code": "ORGANIZATION_UPDATE_FAILED"
                }
              }
            }
          },
          "500": {
            "description": "Error interno (incluye fallos de validación del modelo; el correlativo consumido se revierte).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuotationCodedError"
                },
                "example": {
                  "message": "Error interno del servidor",
                  "code": "INTERNAL_SERVER_ERROR"
                }
              }
            }
          }
        }
      }
    },
    "/quotations/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB de la cotización (campo id de las respuestas). No confundir con quotationNumber.",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "507f1f77bcf86cd799439051"
        }
      ],
      "get": {
        "operationId": "getQuotationById",
        "tags": [
          "Quotations"
        ],
        "summary": "Consultar una cotización por ID",
        "description": "Requiere quotations:read o *. La cotización debe pertenecer a la organización del token.",
        "x-required-permissions": [
          "quotations:read"
        ],
        "responses": {
          "200": {
            "description": "Cotización encontrada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuotationResult"
                },
                "examples": {
                  "found": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439051",
                        "quotationNumber": 42,
                        "name": "42 - Ana Ejemplo",
                        "status": "draft",
                        "contact": {
                          "id": "507f1f77bcf86cd799439011",
                          "fullName": "Ana Ejemplo",
                          "company": "Microm S.A.S",
                          "email": "ana@example.com",
                          "mobile": "573000000000",
                          "idNumber": "900123456-1",
                          "city": "Medellín"
                        },
                        "items": [
                          {
                            "_id": "507f1f77bcf86cd799439061",
                            "name": "Manilla Tyvek 19mm",
                            "description": "Impresa a 1 tinta",
                            "quantity": 500,
                            "unitPrice": 400,
                            "taxes": 19,
                            "total": 238000,
                            "imageUrl": ""
                          }
                        ],
                        "subtotal": 200000,
                        "taxes": 38000,
                        "discounts": 0,
                        "total": 238000,
                        "paymentTerms": "50% anticipo, 50% contra entrega",
                        "shippingTerms": "5 días hábiles",
                        "observaciones": "Forma de pago: 50% anticipo, 50% contra entrega. Tiempo de entrega: 5 días hábiles",
                        "expirationDate": "2026-10-05T15:00:00.000Z",
                        "creationDate": "2026-09-05T15:00:00.000Z",
                        "lastModified": "2026-09-05T15:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID de cotización inválido (no es un ObjectId).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuotationError"
                },
                "example": {
                  "success": false,
                  "error": "Invalid quotation ID"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Cotización inexistente o no perteneciente a la organización del token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuotationError"
                },
                "example": {
                  "success": false,
                  "error": "Quotation not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1quotations/get/responses/500"
          }
        }
      },
      "put": {
        "operationId": "updateQuotationById",
        "tags": [
          "Quotations"
        ],
        "summary": "Actualizar parcialmente una cotización",
        "description": "Requiere quotations:write o *. Solo se tocan los campos enviados (status, name, paymentTerms, shippingTerms, observaciones, additionalNotes, expirationDate, items, subtotal, taxes, discounts, total); el resto del body se ignora. items reemplaza el array completo. status ya no es libre: si cambia, sigue la matriz de transiciones (mismo contrato que PATCH /quotations/{id}/status, 409 si no vale) y se aplica antes de los demás campos. Marcar sent aquí registra sentAt pero NO envía nada al cliente. Una expired con una expirationDate futura vuelve sola a sent (no cuenta como envío: sentAt no cambia); con status sent en el mismo body (desde expired, o desde draft/ready/rejected con la fecha ya pasada), la fecha se guarda primero. Una cotización invoiced o canceled no se edita (409 QUOTATION_LOCKED).",
        "x-required-permissions": [
          "quotations:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuotationUpdate"
              },
              "examples": {
                "status": {
                  "summary": "Marcar como aceptada (desde sent) y ajustar notas.",
                  "value": {
                    "status": "accepted",
                    "additionalNotes": "Incluye diseño sin costo"
                  }
                },
                "items": {
                  "summary": "Reemplazar ítems y totales (montos como cadena).",
                  "value": {
                    "items": [
                      {
                        "name": "Manilla Tyvek 19mm",
                        "quantity": 1000,
                        "unitPrice": 350,
                        "taxes": 19,
                        "total": 416500
                      }
                    ],
                    "subtotal": "350000",
                    "taxes": "66500",
                    "total": "416500",
                    "expirationDate": "2026-10-15T00:00:00.000Z"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cotización actualizada (formato aplanado, contacto poblado).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuotationResult"
                },
                "examples": {
                  "updated": {
                    "summary": "Tras PUT con status sent; lastModified avanza.",
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439051",
                        "quotationNumber": 42,
                        "name": "42 - Ana Ejemplo",
                        "status": "sent",
                        "contact": {
                          "id": "507f1f77bcf86cd799439011",
                          "fullName": "Ana Ejemplo",
                          "company": "Microm S.A.S",
                          "email": "ana@example.com",
                          "mobile": "573000000000"
                        },
                        "items": [
                          {
                            "_id": "507f1f77bcf86cd799439061",
                            "name": "Manilla Tyvek 19mm",
                            "quantity": 500,
                            "unitPrice": 400,
                            "taxes": 19,
                            "total": 238000
                          }
                        ],
                        "subtotal": 200000,
                        "taxes": 38000,
                        "discounts": 0,
                        "total": 238000,
                        "paymentTerms": "Contado",
                        "shippingTerms": "5 días hábiles",
                        "additionalNotes": "Incluye diseño sin costo",
                        "expirationDate": "2026-10-05T15:00:00.000Z",
                        "creationDate": "2026-09-05T15:00:00.000Z",
                        "lastModified": "2026-09-06T10:30:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1quotations~1{id}/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1quotations~1{id}/get/responses/404"
          },
          "409": {
            "description": "Transición inválida desde el estado actual (INVALID_TRANSITION, con allowed), el estado cambió entre tanto (STATUS_CONFLICT) o la cotización está facturada o anulada (QUOTATION_LOCKED).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1quotations~1{id}/delete/responses/409/content/application~1json/schema"
                },
                "example": {
                  "success": false,
                  "error": "No se puede pasar de Borrador a Aceptada",
                  "message": "No se puede pasar de Borrador a Aceptada",
                  "code": "INVALID_TRANSITION",
                  "currentStatus": "draft",
                  "allowed": [
                    "ready",
                    "sent",
                    "canceled"
                  ]
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1quotations/get/responses/500"
          }
        }
      },
      "delete": {
        "operationId": "deleteQuotationById",
        "tags": [
          "Quotations"
        ],
        "summary": "Eliminar una cotización",
        "description": "Requiere quotations:delete o *. Borrado físico; no libera el correlativo ni revoca nada más. Solo se borra lo que NUNCA se envió (draft o ready sin sentAt): lo demás responde 409 QUOTATION_NOT_DELETABLE y hay que anularlo (status canceled), porque el cliente ya tiene el link.",
        "x-required-permissions": [
          "quotations:delete"
        ],
        "responses": {
          "200": {
            "description": "Cotización eliminada; no responde 204.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuotationDeleteResult"
                },
                "examples": {
                  "deleted": {
                    "value": {
                      "success": true,
                      "message": "Quotation deleted successfully",
                      "data": {
                        "id": "507f1f77bcf86cd799439051"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1quotations~1{id}/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1quotations~1{id}/get/responses/404"
          },
          "409": {
            "description": "La cotización ya se envió; anúlala en vez de eliminarla.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Error de una transición de estado.",
                  "required": [
                    "success",
                    "error",
                    "code"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": false
                    },
                    "error": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "INVALID_STATUS",
                        "INVALID_TRANSITION",
                        "STATUS_CONFLICT",
                        "INVALID_REASON_CODE",
                        "INVALID_CHANNEL",
                        "QUOTATION_LOCKED",
                        "QUOTATION_NOT_DELETABLE",
                        "EXPIRATION_REQUIRED",
                        "QUOTATION_CANCELED",
                        "UNKNOWN_STATUS"
                      ]
                    },
                    "currentStatus": {
                      "type": "string",
                      "description": "Estado actual de la cotización."
                    },
                    "allowed": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Quotation/properties/status"
                      },
                      "description": "Destinos válidos desde el estado actual."
                    }
                  }
                },
                "example": {
                  "success": false,
                  "error": "Quotation was already sent: cancel it instead of deleting it",
                  "code": "QUOTATION_NOT_DELETABLE",
                  "currentStatus": "sent"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1quotations/get/responses/500"
          }
        }
      }
    },
    "/quotations/{id}/status": {
      "parameters": [
        {
          "$ref": "#/paths/~1quotations~1{id}/parameters/0"
        }
      ],
      "patch": {
        "operationId": "changeQuotationStatus",
        "tags": [
          "Quotations"
        ],
        "summary": "Cambiar el estado de una cotización",
        "description": "Requiere quotations:write o *. Único camino validado para mover el estado: sigue la matriz de transiciones y es atómico (si otro la movió entre tanto responde 409 STATUS_CONFLICT con el estado actual). Deja una entrada en el historial, registra la actividad en el contacto y dispara el trigger quotation_status_changed. Llegar a ready o sent por primera vez dispara además quotation_created.",
        "x-required-permissions": [
          "quotations:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Cambio de estado. to debe ser una transición válida desde el estado actual para quien llama: draft→ready|sent|canceled; ready→draft|sent|canceled; sent→accepted|rejected|canceled; accepted→sent|canceled; rejected→sent|canceled; expired→sent|canceled. expired e invoiced las escribe solo el sistema (cron y factura); canceled es terminal. Pedir el estado actual no cambia nada (changed false). Marcar sent a mano registra sentAt y el canal, pero NO envía nada al cliente. →sent (desde expired, o desde draft/ready/rejected con la fecha ya terminada en la zona de la organización) exige una fecha de vencimiento futura (409 EXPIRATION_REQUIRED); →ready no. Para enviar de verdad, POST /quotations/{id}/send-email o send-whatsapp.",
                "required": [
                  "to"
                ],
                "properties": {
                  "to": {
                    "$ref": "#/components/schemas/Quotation/properties/status"
                  },
                  "reason": {
                    "type": "string",
                    "maxLength": 1000,
                    "description": "Motivo libre (rechazo o anulación)."
                  },
                  "reasonCode": {
                    "$ref": "#/components/schemas/QuotationUpdate/properties/reasonCode"
                  },
                  "channel": {
                    "$ref": "#/components/schemas/Quotation/properties/sentChannel"
                  },
                  "expectedFrom": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/Quotation/properties/status"
                      }
                    ],
                    "description": "Estado que vio quien llama. Si ya no es el actual responde 409 STATUS_CONFLICT sin tocar nada."
                  }
                }
              },
              "examples": {
                "accept": {
                  "summary": "El cliente aceptó por WhatsApp.",
                  "value": {
                    "to": "accepted"
                  }
                },
                "reject": {
                  "summary": "Rechazada por precio.",
                  "value": {
                    "to": "rejected",
                    "reasonCode": "price",
                    "reason": "Le pareció caro el envío"
                  }
                },
                "markSent": {
                  "summary": "Se entregó en persona (no envía nada).",
                  "value": {
                    "to": "sent",
                    "channel": "in_person"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Estado cambiado (o ya estaba ahí, changed false).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data",
                    "from",
                    "to",
                    "changed"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Quotation"
                    },
                    "from": {
                      "$ref": "#/components/schemas/Quotation/properties/status"
                    },
                    "to": {
                      "$ref": "#/components/schemas/Quotation/properties/status"
                    },
                    "changed": {
                      "type": "boolean",
                      "description": "false si ya estaba en el estado pedido."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID inválido, estado, motivo o canal fuera del enum.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1quotations~1{id}/delete/responses/409/content/application~1json/schema"
                },
                "example": {
                  "success": false,
                  "error": "Estado inválido: pending",
                  "code": "INVALID_STATUS"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1quotations~1{id}/get/responses/404"
          },
          "409": {
            "$ref": "#/paths/~1quotations~1{id}/put/responses/409"
          },
          "500": {
            "$ref": "#/paths/~1quotations/get/responses/500"
          }
        }
      }
    },
    "/quotations/{id}/send-whatsapp": {
      "parameters": [
        {
          "$ref": "#/paths/~1quotations~1{id}/parameters/0"
        }
      ],
      "post": {
        "operationId": "sendQuotationWhatsApp",
        "tags": [
          "Quotations"
        ],
        "summary": "Enviar una cotización por WhatsApp",
        "description": "Requiere quotations:write y whatsapp:send (o *). Manda el PDF como documento con el link público en el pie y lo guarda en la conversación. Solo dentro de la ventana de 24 h de Meta: si el cliente no ha escrito en 24 h responde 409 WHATSAPP_WINDOW_CLOSED sin enviar nada. Regla de envío: draft, ready y rejected pasan a sent; sent, accepted e invoiced no cambian de estado (solo se actualizan sentAt y sentChannel); canceled no se envía (409 QUOTATION_CANCELED) y expired solo con fecha de vencimiento futura (409 EXPIRATION_REQUIRED); draft/ready/rejected cuya fecha de vencimiento ya terminó en la zona de la organización tampoco salen (409 EXPIRATION_REQUIRED). Es un envío real al cliente.",
        "x-required-permissions": [
          "quotations:write",
          "whatsapp:send"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuotationSendWhatsApp"
              },
              "examples": {
                "fromContact": {
                  "summary": "A la conversación del contacto de la cotización.",
                  "value": {}
                },
                "fromChat": {
                  "summary": "A una conversación concreta, con texto propio.",
                  "value": {
                    "conversationId": "6650f0c2a1b2c3d4e5f60718",
                    "message": "Te comparto la cotización que hablamos"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Enviada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuotationSendResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1quotations~1{id}~1send-email/post/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "description": "Sin permiso (quotations:write y whatsapp:send) o el contacto pidió no ser contactado por WhatsApp (WHATSAPP_OPTED_OUT).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuotationSendError"
                }
              }
            }
          },
          "404": {
            "$ref": "#/paths/~1quotations~1{id}~1send-email/post/responses/400"
          },
          "409": {
            "$ref": "#/paths/~1quotations~1{id}~1send-email/post/responses/400"
          },
          "422": {
            "$ref": "#/paths/~1quotations~1{id}~1send-email/post/responses/400"
          },
          "500": {
            "$ref": "#/paths/~1quotations/get/responses/500"
          },
          "502": {
            "$ref": "#/paths/~1quotations~1{id}~1send-email/post/responses/400"
          }
        }
      }
    },
    "/quotations/{id}/send-email": {
      "parameters": [
        {
          "$ref": "#/paths/~1quotations~1{id}/parameters/0"
        }
      ],
      "post": {
        "operationId": "sendQuotationEmail",
        "tags": [
          "Quotations"
        ],
        "summary": "Enviar una cotización por correo",
        "description": "Requiere quotations:write o *. Sale por la tubería de correo de la plataforma con el PDF adjunto y el link público. Misma regla de envío que WhatsApp (409 QUOTATION_CANCELED o EXPIRATION_REQUIRED sin enviar nada). Es un envío real al cliente.",
        "x-required-permissions": [
          "quotations:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuotationSendEmail"
              },
              "example": {
                "to": "compras@cliente.com"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Enviada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuotationSendResult"
                }
              }
            }
          },
          "400": {
            "description": "El envío no salió: regla de envío (QUOTATION_CANCELED, EXPIRATION_REQUIRED), ventana de 24 h cerrada (WHATSAPP_WINDOW_CLOSED), contacto que pidió no ser contactado (WHATSAPP_OPTED_OUT), sin celular, sin WhatsApp conectado o Meta lo rechazó (WHATSAPP_SEND_FAILED, 502).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuotationSendError"
                },
                "example": {
                  "success": false,
                  "error": "El cliente no ha escrito por WhatsApp en las últimas 24 h: Meta no deja enviarle la cotización hasta que escriba.",
                  "code": "WHATSAPP_WINDOW_CLOSED",
                  "lastInboundAt": "2026-10-03T15:20:00.000Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1quotations~1{id}~1send-email/post/responses/400"
          },
          "409": {
            "$ref": "#/paths/~1quotations~1{id}~1send-email/post/responses/400"
          },
          "500": {
            "$ref": "#/paths/~1quotations/get/responses/500"
          }
        }
      }
    },
    "/quotations/{number}/download": {
      "parameters": [
        {
          "name": "number",
          "in": "path",
          "required": true,
          "description": "Número correlativo de la cotización dentro de la organización (campo quotationNumber, el que aparece impreso en el PDF), NO el id de MongoDB. Se convierte con Number(): un valor no numérico responde 500, no 400.",
          "schema": {
            "type": "integer",
            "minimum": 1
          },
          "example": 42
        }
      ],
      "get": {
        "operationId": "downloadQuotationPdf",
        "tags": [
          "Quotations"
        ],
        "summary": "Descargar el PDF de una cotización por número",
        "description": "Requiere quotations:read o *. number es el quotationNumber (correlativo por organización, el impreso en el PDF), NO el id de Mongo. Genera el PDF con la misma plantilla que la app y lo devuelve como adjunto; puede tardar varios segundos (puppeteer) y el archivo pesa según las imágenes de los ítems. Los errores de esta operación usan el contrato message/code.",
        "x-required-permissions": [
          "quotations:read"
        ],
        "responses": {
          "200": {
            "description": "PDF de la cotización.",
            "headers": {
              "Content-Disposition": {
                "description": "attachment; filename=\"<numberPrefix de la organización o cotizacion_><number>.pdf\"",
                "schema": {
                  "type": "string"
                },
                "example": "attachment; filename=\"cotizacion_42.pdf\""
              },
              "Content-Length": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "No existe una cotización con ese número en la organización del token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuotationCodedError"
                },
                "example": {
                  "message": "Cotización no encontrada",
                  "code": "QUOTATION_NOT_FOUND"
                }
              }
            }
          },
          "500": {
            "description": "Error generando el PDF, o number no numérico (Number() da NaN y falla el cast).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuotationCodedError"
                },
                "example": {
                  "message": "Error interno del servidor",
                  "code": "INTERNAL_SERVER_ERROR"
                }
              }
            }
          }
        }
      }
    },
    "/products": {
      "get": {
        "operationId": "listProducts",
        "tags": [
          "Products"
        ],
        "summary": "Listar productos",
        "description": "Requiere products:read o *. Filtra por la organización del API Token. No incluye las variantes (solo GET /products/{id}). search NO escapa la expresión regular (un + o ( pueden responder 500). priceMin/priceMax se convierten con Number(): un valor no numérico rompe la consulta y responde 500. hasVariants se compara con la cadena \"true\": cualquier otro valor (false, 0, si) filtra los productos SIN variantes.",
        "x-required-permissions": [
          "products:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Coincidencia parcial en name, sku o description, sin distinguir mayúsculas (regex sin escapar)."
          },
          {
            "name": "categoryId",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Coincidencia exacta con el categoryId guardado (texto libre)."
          },
          {
            "name": "hasVariants",
            "in": "query",
            "schema": {
              "type": "boolean"
            },
            "description": "true = solo productos con variantes; cualquier otro valor = solo sin variantes. Omitir para no filtrar."
          },
          {
            "name": "priceMin",
            "in": "query",
            "schema": {
              "type": "number"
            },
            "description": "unitPrice mínimo (inclusive)."
          },
          {
            "name": "priceMax",
            "in": "query",
            "schema": {
              "type": "number"
            },
            "description": "unitPrice máximo (inclusive)."
          },
          {
            "name": "sortBy",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "createdAt",
                "updatedAt",
                "name",
                "unitPrice"
              ],
              "default": "createdAt"
            },
            "description": "Un valor fuera del enum cae a createdAt sin error."
          },
          {
            "name": "sortOrder",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Productos y paginación; data puede estar vacío.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductListResult"
                },
                "examples": {
                  "list": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "507f1f77bcf86cd799439071",
                          "name": "Manilla Tyvek 19mm",
                          "description": "Manilla de papel Tyvek 19mm impresa a 1 tinta",
                          "unitPrice": 400,
                          "taxes": 19,
                          "cost": 180,
                          "sku": "TYV-19",
                          "categoryId": "manillas",
                          "imageUrl": "https://cdn.example.com/tyvek-19.png",
                          "notes": "Pedido mínimo 100 unidades",
                          "dimensions": {
                            "width": 1.9,
                            "height": 0.1,
                            "length": 25
                          },
                          "hasVariants": true,
                          "variantAttributes": [
                            "Color"
                          ],
                          "createdAt": "2026-09-05T15:00:00.000Z",
                          "updatedAt": "2026-09-05T15:00:00.000Z"
                        },
                        {
                          "id": "507f1f77bcf86cd799439072",
                          "name": "Manilla sublimada",
                          "description": "",
                          "unitPrice": 2500,
                          "taxes": 0,
                          "cost": null,
                          "sku": "",
                          "categoryId": null,
                          "imageUrl": "",
                          "notes": "",
                          "dimensions": null,
                          "hasVariants": false,
                          "variantAttributes": [],
                          "createdAt": "2026-08-20T13:45:00.000Z",
                          "updatedAt": "2026-08-20T13:45:00.000Z"
                        }
                      ],
                      "pagination": {
                        "total": 2,
                        "page": 1,
                        "limit": 100,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "description": "Error interno (incluye fallos de validación del modelo en PUT y filtros inválidos en GET).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductError"
                },
                "example": {
                  "success": false,
                  "error": "Internal server error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createProduct",
        "tags": [
          "Products"
        ],
        "summary": "Crear un producto",
        "description": "Requiere products:write o *. NO aplica límite de plan (no hay planLimitsMiddleware en esta ruta, a diferencia de contactos, empresas y formularios). name y unitPrice son obligatorios; unitPrice acepta número o cadena numérica. El producto nace sin variantes (hasVariants false); las variantes solo se gestionan desde la app. Responde 201 con el producto en el mismo formato de GET (sin variants).",
        "x-required-permissions": [
          "products:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProductCreate"
              },
              "examples": {
                "full": {
                  "summary": "Producto completo con dimensiones.",
                  "value": {
                    "name": "Manilla Tyvek 19mm",
                    "description": "Manilla de papel Tyvek 19mm impresa a 1 tinta",
                    "unitPrice": 400,
                    "taxes": 19,
                    "cost": 180,
                    "sku": "TYV-19",
                    "categoryId": "manillas",
                    "imageUrl": "https://cdn.example.com/tyvek-19.png",
                    "notes": "Pedido mínimo 100 unidades",
                    "dimensions": {
                      "width": 1.9,
                      "height": 0.1,
                      "length": 25
                    }
                  }
                },
                "minimal": {
                  "summary": "Solo los obligatorios, con el precio como cadena.",
                  "value": {
                    "name": "Manilla sublimada",
                    "unitPrice": "2500"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Producto creado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductCreatedResult"
                },
                "examples": {
                  "created": {
                    "summary": "Respuesta al crear solo con name y unitPrice (defaults aplicados).",
                    "value": {
                      "success": true,
                      "message": "Product created successfully",
                      "data": {
                        "id": "507f1f77bcf86cd799439072",
                        "name": "Manilla sublimada",
                        "description": "",
                        "unitPrice": 2500,
                        "taxes": 0,
                        "cost": null,
                        "sku": "",
                        "categoryId": null,
                        "imageUrl": "",
                        "notes": "",
                        "dimensions": null,
                        "hasVariants": false,
                        "variantAttributes": [],
                        "createdAt": "2026-09-05T15:00:00.000Z",
                        "updatedAt": "2026-09-05T15:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Faltan name o unitPrice, unitPrice no numérico, o el modelo rechazó un valor (taxes/cost no numéricos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "success": false,
                      "error": "Missing required fields",
                      "required": [
                        "name",
                        "unitPrice"
                      ]
                    }
                  },
                  "invalidPrice": {
                    "value": {
                      "success": false,
                      "error": "'unitPrice' must be a valid number"
                    }
                  },
                  "validation": {
                    "value": {
                      "success": false,
                      "error": "Validation error",
                      "details": [
                        "Cast to Number failed for value \"abc\" (type string) at path \"cost\""
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1products/get/responses/500"
          }
        }
      }
    },
    "/products/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB del producto (campo id de las respuestas).",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "507f1f77bcf86cd799439071"
        }
      ],
      "get": {
        "operationId": "getProductById",
        "tags": [
          "Products"
        ],
        "summary": "Consultar un producto por ID",
        "description": "Requiere products:read o *. El producto debe pertenecer a la organización del token. Es la única operación que incluye variants: las variantes activas (isActive true) cuando hasVariants es true; [] en cualquier otro caso.",
        "x-required-permissions": [
          "products:read"
        ],
        "responses": {
          "200": {
            "description": "Producto encontrado, con sus variantes activas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductDetailResult"
                },
                "examples": {
                  "found": {
                    "summary": "Producto con dos variantes activas.",
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439071",
                        "name": "Manilla Tyvek 19mm",
                        "description": "Manilla de papel Tyvek 19mm impresa a 1 tinta",
                        "unitPrice": 400,
                        "taxes": 19,
                        "cost": 180,
                        "sku": "TYV-19",
                        "categoryId": "manillas",
                        "imageUrl": "https://cdn.example.com/tyvek-19.png",
                        "notes": "Pedido mínimo 100 unidades",
                        "dimensions": {
                          "width": 1.9,
                          "height": 0.1,
                          "length": 25
                        },
                        "hasVariants": true,
                        "variantAttributes": [
                          "Color"
                        ],
                        "createdAt": "2026-09-05T15:00:00.000Z",
                        "updatedAt": "2026-09-05T15:00:00.000Z",
                        "variants": [
                          {
                            "id": "507f1f77bcf86cd799439081",
                            "sku": "TYV-19-ROJO",
                            "price": 400,
                            "imageUrl": "https://cdn.example.com/tyvek-19-rojo.png",
                            "stock": 5000,
                            "attributeValues": [
                              {
                                "_id": "507f1f77bcf86cd799439091",
                                "attributeName": "Color",
                                "valueId": "rojo",
                                "value": "rojo",
                                "displayName": "Rojo"
                              }
                            ]
                          },
                          {
                            "id": "507f1f77bcf86cd799439082",
                            "sku": "TYV-19-AZUL",
                            "price": 400,
                            "imageUrl": "",
                            "stock": null,
                            "attributeValues": [
                              {
                                "_id": "507f1f77bcf86cd799439092",
                                "attributeName": "Color",
                                "valueId": "azul",
                                "value": "azul",
                                "displayName": "Azul"
                              }
                            ]
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID de producto inválido (no es un ObjectId).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductError"
                },
                "example": {
                  "success": false,
                  "error": "Invalid product ID"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Producto inexistente o no perteneciente a la organización del token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductError"
                },
                "example": {
                  "success": false,
                  "error": "Product not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1products/get/responses/500"
          }
        }
      },
      "put": {
        "operationId": "updateProductById",
        "tags": [
          "Products"
        ],
        "summary": "Actualizar parcialmente un producto",
        "description": "Requiere products:write o *. Solo se tocan los campos enviados; null limpia un campo. No valida los tipos antes de guardar: un monto no numérico falla en el modelo y responde 500, no 400. No permite tocar hasVariants ni variantAttributes. Responde el producto en el formato de GET lista (sin variants).",
        "x-required-permissions": [
          "products:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProductUpdate"
              },
              "examples": {
                "price": {
                  "summary": "Cambiar precio y costo.",
                  "value": {
                    "unitPrice": 450,
                    "cost": "200"
                  }
                },
                "clear": {
                  "summary": "Limpiar el costo y cambiar la categoría.",
                  "value": {
                    "cost": null,
                    "categoryId": "promocionales",
                    "notes": ""
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Producto actualizado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductResult"
                },
                "examples": {
                  "updated": {
                    "summary": "Tras PUT con unitPrice y cost nuevos; updatedAt avanza.",
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439071",
                        "name": "Manilla Tyvek 19mm",
                        "description": "Manilla de papel Tyvek 19mm impresa a 1 tinta",
                        "unitPrice": 450,
                        "taxes": 19,
                        "cost": 200,
                        "sku": "TYV-19",
                        "categoryId": "manillas",
                        "imageUrl": "https://cdn.example.com/tyvek-19.png",
                        "notes": "Pedido mínimo 100 unidades",
                        "dimensions": {
                          "width": 1.9,
                          "height": 0.1,
                          "length": 25
                        },
                        "hasVariants": true,
                        "variantAttributes": [
                          "Color"
                        ],
                        "createdAt": "2026-09-05T15:00:00.000Z",
                        "updatedAt": "2026-09-06T10:30:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1products~1{id}/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1products~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1products/get/responses/500"
          }
        }
      },
      "delete": {
        "operationId": "deleteProductById",
        "tags": [
          "Products"
        ],
        "summary": "Eliminar un producto y sus variantes",
        "description": "Requiere products:delete o *. IRREVERSIBLE: borra el producto y TODAS sus variantes (ProductVariant) de la organización del token; no hay papelera. Responde 409 PRODUCT_IN_OPEN_QUOTATIONS, sin borrar nada, si el producto aparece en una cotización abierta (status draft, ready o sent) de la organización. Las facturas no se revisan: sus líneas no guardan referencia al producto. A diferencia de GET y PUT, un ID que no es ObjectId responde 404, no 400 (igual que un producto de otra organización).",
        "x-required-permissions": [
          "products:delete"
        ],
        "responses": {
          "200": {
            "description": "Producto y variantes eliminados.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "message",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "message": {
                      "type": "string"
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "id",
                        "deletedVariants"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "deletedVariants": {
                          "type": "integer",
                          "minimum": 0,
                          "description": "Variantes borradas junto con el producto."
                        }
                      }
                    }
                  }
                },
                "example": {
                  "success": true,
                  "message": "Product deleted successfully",
                  "data": {
                    "id": "66f1000000000000000000a1",
                    "deletedVariants": 3
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1products~1{id}/get/responses/404"
          },
          "409": {
            "description": "El producto está en cotizaciones abiertas (borrador, lista o enviadas); no se borró nada.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "error",
                    "code",
                    "openQuotations"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": false
                    },
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string",
                      "const": "PRODUCT_IN_OPEN_QUOTATIONS"
                    },
                    "openQuotations": {
                      "type": "integer",
                      "minimum": 1,
                      "description": "Cotizaciones abiertas (draft",
                      "ready o sent) que lo usan.": null
                    }
                  }
                },
                "example": {
                  "success": false,
                  "error": "Product is used in open quotations",
                  "code": "PRODUCT_IN_OPEN_QUOTATIONS",
                  "openQuotations": 2
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1products/get/responses/500"
          }
        }
      }
    },
    "/invoices": {
      "get": {
        "operationId": "listInvoices",
        "tags": [
          "Invoices"
        ],
        "summary": "Listar facturas",
        "description": "Requiere invoices:read o *, y que el plan incluya el módulo de facturación electrónica (PLAN_MODULE_REQUIRED → 403). Lista la copia local de los documentos de la organización del API Token, con el contacto aplanado. Por defecto solo los emitidos (direction=outgoing); con direction=incoming se listan las facturas de proveedores recibidas. dateFrom/dateTo comparan el campo date como cadena YYYY-MM-DD (ambos inclusive). search NO escapa la expresión regular sobre prefix y customer.name (un + o ( pueden responder 500); si es numérico también compara number exacto y el número de identificación del cliente. contactId no se valida como ObjectId (un valor inválido responde 500). type_document_id no numérico devuelve lista vacía.",
        "x-required-permissions": [
          "invoices:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          },
          {
            "name": "direction",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/Invoice/properties/direction"
            },
            "description": "Por defecto outgoing. Un valor fuera del enum no falla, devuelve lista vacía."
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/Invoice/properties/status"
            },
            "description": "Coincidencia exacta; un valor fuera del enum devuelve lista vacía."
          },
          {
            "name": "type_document_id",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "1 factura, 91 nota crédito, 92 nota débito."
          },
          {
            "name": "contactId",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "ID del contacto asociado (customerId)."
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Coincidencia parcial en prefix o nombre del cliente (regex sin escapar); exacta en number e identificación del cliente."
          },
          {
            "name": "dateFrom",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "YYYY-MM-DD inclusive; comparación de cadenas contra date.",
            "example": "2026-09-01"
          },
          {
            "name": "dateTo",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "YYYY-MM-DD inclusive; comparación de cadenas contra date.",
            "example": "2026-09-30"
          },
          {
            "name": "sortBy",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "date",
                "createdAt",
                "number"
              ],
              "default": "date"
            },
            "description": "Un valor fuera del enum cae a date sin error."
          },
          {
            "name": "sortOrder",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Facturas y paginación; data puede estar vacío.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceListResult"
                },
                "examples": {
                  "list": {
                    "summary": "Una factura válida y una nota crédito (sin payment_form ni contacto).",
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "507f1f77bcf86cd799439071",
                          "number": 623,
                          "prefix": "FS",
                          "fullNumber": "FS-623",
                          "direction": "outgoing",
                          "type_document_id": 1,
                          "date": "2026-09-05",
                          "time": "10:15:00",
                          "status": "valido",
                          "customer": {
                            "identification_number": 900123456,
                            "dv": 1,
                            "name": "Microm S.A.S",
                            "email": "facturacion@example.com",
                            "type_document_identification_id": 6,
                            "type_organization_id": 1,
                            "type_liability_id": 117,
                            "type_regime_id": 2,
                            "municipality_id": 149
                          },
                          "contact": {
                            "id": "507f1f77bcf86cd799439011",
                            "fullName": "Ana Ejemplo",
                            "company": "Microm S.A.S",
                            "email": "ana@example.com",
                            "mobile": "573000000000"
                          },
                          "invoice_lines": [
                            {
                              "unit_measure_id": 70,
                              "invoiced_quantity": "500",
                              "line_extension_amount": "200000.00",
                              "free_of_charge_indicator": false,
                              "tax_totals": [
                                {
                                  "tax_id": 1,
                                  "tax_amount": "38000.00",
                                  "percent": "19.00",
                                  "taxable_amount": "200000.00"
                                }
                              ],
                              "description": "Manilla Tyvek 19mm impresa a 1 tinta",
                              "type_item_identification_id": 4,
                              "price_amount": "400.00",
                              "base_quantity": "1.000000"
                            }
                          ],
                          "legal_monetary_totals": {
                            "line_extension_amount": "200000.00",
                            "tax_exclusive_amount": "200000.00",
                            "tax_inclusive_amount": "238000.00",
                            "payable_amount": "238000.00"
                          },
                          "tax_totals": [
                            {
                              "tax_id": 1,
                              "tax_amount": "38000.00",
                              "percent": "19.00",
                              "taxable_amount": "200000.00"
                            }
                          ],
                          "payment_form": {
                            "payment_form_id": 1,
                            "payment_method_id": 10,
                            "payment_due_date": "2026-09-05",
                            "duration_measure": "0"
                          },
                          "dian_response": {
                            "isValid": "true",
                            "statusCode": "00",
                            "statusDescription": "Procesado Correctamente.",
                            "cufe": "3a0b1c2d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f6071"
                          },
                          "createdAt": "2026-09-05T15:15:03.000Z",
                          "updatedAt": "2026-09-05T15:15:03.000Z",
                          "withholdings": null,
                          "netPayable": 238000
                        },
                        {
                          "id": "507f1f77bcf86cd799439072",
                          "number": 14,
                          "prefix": "NC",
                          "fullNumber": "NC-14",
                          "direction": "outgoing",
                          "type_document_id": 91,
                          "date": "2026-09-04",
                          "time": "16:40:00",
                          "status": "valido",
                          "customer": {
                            "identification_number": "B12345678",
                            "name": "Comercial Ibérica S.L.",
                            "type_document_identification_id": 9,
                            "country_id": 64,
                            "municipality_name": "Madrid",
                            "state_name": "Comunidad de Madrid",
                            "email": "facturas@example.es"
                          },
                          "contact": null,
                          "invoice_lines": [
                            {
                              "unit_measure_id": 70,
                              "invoiced_quantity": "1",
                              "line_extension_amount": "1000000.00",
                              "free_of_charge_indicator": false,
                              "tax_totals": [],
                              "description": "Devolución servicio de desarrollo",
                              "type_item_identification_id": 4,
                              "price_amount": "1000000.00",
                              "base_quantity": "1.000000"
                            }
                          ],
                          "legal_monetary_totals": {
                            "line_extension_amount": "1000000.00",
                            "tax_exclusive_amount": "1000000.00",
                            "tax_inclusive_amount": "1000000.00",
                            "payable_amount": "1000000.00"
                          },
                          "tax_totals": [],
                          "dian_response": null,
                          "createdAt": "2026-09-04T21:40:12.000Z",
                          "updatedAt": "2026-09-04T21:40:12.000Z",
                          "withholdings": null,
                          "netPayable": 1000000
                        }
                      ],
                      "pagination": {
                        "total": 2,
                        "page": 1,
                        "limit": 100,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "description": "Permiso insuficiente (INSUFFICIENT_API_PERMISSIONS) o plan sin el módulo de facturación electrónica (PLAN_MODULE_REQUIRED).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "examples": {
                  "permission": {
                    "value": {
                      "message": "Permiso insuficiente. Se requiere: invoices:read",
                      "code": "INSUFFICIENT_API_PERMISSIONS",
                      "requiredPermission": "invoices:read",
                      "availablePermissions": []
                    }
                  },
                  "module": {
                    "value": {
                      "message": "La facturación electrónica es un módulo adicional de tu plan. Contáctanos para activarlo.",
                      "code": "PLAN_MODULE_REQUIRED",
                      "module": "invoices"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Error interno (incluye filtros inválidos como un contactId que no es ObjectId o un search con regex inválida).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceError"
                },
                "example": {
                  "success": false,
                  "error": "Internal server error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createInvoice",
        "tags": [
          "Invoices"
        ],
        "summary": "Emitir una factura de venta electrónica",
        "description": "Requiere invoices:write o *, que el plan incluya el módulo de facturación electrónica (PLAN_MODULE_REQUIRED → 403) y aplica el límite mensual de facturas del plan (PLAN_LIMIT_REACHED → 403). EFECTO EXTERNO: emite un documento fiscal REAL ante la DIAN a través del proveedor de facturación (POST /ubl2.1/invoice, siempre en ambiente de producción — no existe modo de prueba) y, si el cliente tiene email y sendmail no es false, el proveedor le envía la factura por correo. Guarda la copia local solo si el proveedor respondió, y dispara el trigger de automatizaciones invoice_created. Flujo: valida el body (400) → reserva atómica del consecutivo (400 INVOICE_CONFIG_MISSING / 409 RESOLUTION_EXHAUSTED, sin quemar número) → completa prefix, resolution_number, establishment_* y el HTML del correo desde la configuración de la organización → llama al proveedor. Si el proveedor o la DIAN rechazan, el número reservado se QUEMA (burnedNumber) y no hay copia local. Desde el 05-sep-2026 el body se valida contra el schema local (preflight) ANTES de reservar consecutivo y de emitir: campos como date, time, customer.name, customer.type_document_identification_id, legal_monetary_totals.*, tax_totals, invoice_lines[].unit_measure_id/invoiced_quantity y payment_form.* (si viene payment_form) faltantes responden 400 INVOICE_VALIDATION con la lista de paths, sin gastar número ni tocar la DIAN. Si aun así la copia local falla después de emitir, responde 201 con warning (data reducida: prefix, number, status, dian_response, externalId) y deja un log de error; nunca 500 con la factura emitida. Responde 201 normal con la copia local (status valido / rechazado / sent según la respuesta DIAN).",
        "x-required-permissions": [
          "invoices:write"
        ],
        "x-plan-limit": "invoices",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InvoiceCreate"
              },
              "examples": {
                "colombian": {
                  "summary": "Factura a crédito a un cliente colombiano (NIT); number, prefix y resolution_number los pone el servidor.",
                  "value": {
                    "type_document_id": 1,
                    "date": "2026-09-05",
                    "time": "10:15:00",
                    "notes": "Pedido 4521 — entrega en Medellín",
                    "sendmail": true,
                    "sendmailtome": false,
                    "customer": {
                      "identification_number": 900123456,
                      "name": "Microm S.A.S",
                      "type_document_identification_id": 6,
                      "type_organization_id": 1,
                      "type_liability_id": 117,
                      "type_regime_id": 2,
                      "municipality_id": 149,
                      "email": "facturacion@example.com",
                      "phone": "3001234567",
                      "address": "Calle 10"
                    },
                    "payment_form": {
                      "payment_form_id": 2,
                      "payment_method_id": 47,
                      "payment_due_date": "2026-10-05",
                      "duration_measure": "30"
                    },
                    "legal_monetary_totals": {
                      "line_extension_amount": "200000.00",
                      "tax_exclusive_amount": "200000.00",
                      "tax_inclusive_amount": "238000.00",
                      "allowance_total_amount": "0.00",
                      "payable_amount": "238000.00"
                    },
                    "tax_totals": [
                      {
                        "tax_id": 1,
                        "tax_amount": "38000.00",
                        "percent": "19.00",
                        "taxable_amount": "200000.00"
                      }
                    ],
                    "invoice_lines": [
                      {
                        "unit_measure_id": 70,
                        "invoiced_quantity": "500",
                        "line_extension_amount": "200000.00",
                        "free_of_charge_indicator": false,
                        "tax_totals": [
                          {
                            "tax_id": 1,
                            "tax_amount": "38000.00",
                            "percent": "19.00",
                            "taxable_amount": "200000.00"
                          }
                        ],
                        "description": "Manilla Tyvek 19mm impresa a 1 tinta",
                        "notes": "",
                        "code": "TYV19",
                        "type_item_identification_id": 4,
                        "price_amount": "400.00",
                        "base_quantity": "1.000000"
                      }
                    ]
                  }
                },
                "foreign": {
                  "summary": "Cliente del exterior (NIT de otro país, CIF alfanumérico, sin dv ni municipio) y montos como número.",
                  "value": {
                    "type_document_id": 1,
                    "date": "2026-09-05",
                    "time": "11:00:00",
                    "customer": {
                      "identification_number": "B12345678",
                      "name": "Comercial Ibérica S.L.",
                      "type_document_identification_id": 9,
                      "country_id": 64,
                      "municipality_name": "Madrid",
                      "state_name": "Comunidad de Madrid",
                      "address": "Calle Gran Vía 1",
                      "email": "facturas@example.es"
                    },
                    "legal_monetary_totals": {
                      "line_extension_amount": 1000000,
                      "tax_exclusive_amount": 1000000,
                      "tax_inclusive_amount": 1000000,
                      "payable_amount": 1000000
                    },
                    "tax_totals": [],
                    "invoice_lines": [
                      {
                        "unit_measure_id": 70,
                        "invoiced_quantity": 1,
                        "line_extension_amount": 1000000,
                        "free_of_charge_indicator": false,
                        "tax_totals": [],
                        "description": "Servicio de desarrollo",
                        "type_item_identification_id": 4,
                        "price_amount": 1000000,
                        "base_quantity": 1
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Factura emitida. Caso normal: guardada localmente (contacto poblado si algún contacto tiene la property taxId con ese número). Caso degradado: emitida ante la DIAN pero la copia local falló — trae warning y data reducida (sin id); hay que registrarla a mano con prefix+number.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvoiceCreatedResult"
                    },
                    {
                      "type": "object",
                      "description": "Factura emitida ante la DIAN pero NO guardada en el CRM (falló la persistencia local después de emitir). No trae id ni message; data es reducida.",
                      "required": [
                        "success",
                        "warning",
                        "data"
                      ],
                      "properties": {
                        "success": {
                          "type": "boolean",
                          "const": true
                        },
                        "warning": {
                          "type": "string",
                          "const": "Invoice was issued but could not be stored locally"
                        },
                        "data": {
                          "type": "object",
                          "required": [
                            "prefix",
                            "number",
                            "status",
                            "dian_response",
                            "externalId"
                          ],
                          "properties": {
                            "prefix": {
                              "type": "string"
                            },
                            "number": {
                              "type": "integer"
                            },
                            "status": {
                              "type": "string",
                              "description": "valido / rechazado / sent según la respuesta DIAN."
                            },
                            "dian_response": {
                              "type": [
                                "object",
                                "null"
                              ],
                              "additionalProperties": true
                            },
                            "externalId": {
                              "type": [
                                "string",
                                "null"
                              ]
                            }
                          }
                        }
                      }
                    }
                  ]
                },
                "examples": {
                  "created": {
                    "summary": "Factura emitida y aceptada por la DIAN; contact null porque ningún contacto tiene la property idNumber con ese NIT.",
                    "value": {
                      "success": true,
                      "message": "Invoice created successfully",
                      "data": {
                        "id": "507f1f77bcf86cd799439071",
                        "number": 623,
                        "prefix": "FS",
                        "fullNumber": "FS-623",
                        "direction": "outgoing",
                        "type_document_id": 1,
                        "date": "2026-09-05",
                        "time": "10:15:00",
                        "status": "valido",
                        "customer": {
                          "identification_number": 900123456,
                          "dv": 1,
                          "name": "Microm S.A.S",
                          "phone": "3001234567",
                          "address": "Calle 10",
                          "email": "facturacion@example.com",
                          "type_document_identification_id": 6,
                          "type_organization_id": 1,
                          "type_liability_id": 117,
                          "type_regime_id": 2,
                          "municipality_id": 149
                        },
                        "contact": null,
                        "invoice_lines": [
                          {
                            "unit_measure_id": 70,
                            "invoiced_quantity": "500",
                            "line_extension_amount": "200000.00",
                            "free_of_charge_indicator": false,
                            "tax_totals": [
                              {
                                "tax_id": 1,
                                "tax_amount": "38000.00",
                                "percent": "19.00",
                                "taxable_amount": "200000.00"
                              }
                            ],
                            "description": "Manilla Tyvek 19mm impresa a 1 tinta",
                            "code": "TYV19",
                            "type_item_identification_id": 4,
                            "price_amount": "400.00",
                            "base_quantity": "1.000000"
                          }
                        ],
                        "legal_monetary_totals": {
                          "line_extension_amount": "200000.00",
                          "tax_exclusive_amount": "200000.00",
                          "tax_inclusive_amount": "238000.00",
                          "payable_amount": "238000.00"
                        },
                        "tax_totals": [
                          {
                            "tax_id": 1,
                            "tax_amount": "38000.00",
                            "percent": "19.00",
                            "taxable_amount": "200000.00"
                          }
                        ],
                        "payment_form": {
                          "payment_form_id": 2,
                          "payment_method_id": 47,
                          "payment_due_date": "2026-10-05",
                          "duration_measure": "30"
                        },
                        "dian_response": {
                          "isValid": "true",
                          "statusCode": "00",
                          "statusDescription": "Procesado Correctamente.",
                          "statusMessage": "La Factura electrónica FS-623, ha sido autorizada.",
                          "cufe": "3a0b1c2d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f6071",
                          "qr": "https://catalogo-vpfe.dian.gov.co/document/searchqr?documentkey=3a0b1c2d4e5f6071",
                          "IsValid": "true",
                          "StatusCode": "00",
                          "StatusDescription": "Procesado Correctamente.",
                          "StatusMessage": "La Factura electrónica FS-623, ha sido autorizada."
                        },
                        "notes": "Pedido 4521 — entrega en Medellín",
                        "createdAt": "2026-09-05T15:15:03.000Z",
                        "updatedAt": "2026-09-05T15:15:03.000Z",
                        "withholdings": null,
                        "netPayable": 238000
                      }
                    }
                  },
                  "issuedNotStored": {
                    "summary": "Emitida ante la DIAN pero sin copia local.",
                    "value": {
                      "success": true,
                      "warning": "Invoice was issued but could not be stored locally",
                      "data": {
                        "prefix": "FE",
                        "number": 1042,
                        "status": "valido",
                        "dian_response": {
                          "cufe": "3f1a9c0e7b2d4e6f8a1b2c3d4e5f60718293a4b5c6d7e8f9a0b1c2d3e4f5a6b7",
                          "is_valid": true
                        },
                        "externalId": "9a1f2b3c-4d5e-6f70-8192-a3b4c5d6e7f8"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Body incompleto, total no positivo, payload que no pasa el schema local (INVOICE_VALIDATION, antes de reservar número), u organización sin configuración de facturación (INVOICE_CONFIG_MISSING). quotationId que no existe en la organización, inválido o de una cotización anulada o rechazada (QUOTATION_NOT_FOUND, INVALID_QUOTATION_ID, QUOTATION_CANCELED, QUOTATION_REJECTED): sin reservar número ni emitir.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvoiceNumberingError"
                    },
                    {
                      "type": "object",
                      "description": "Preflight contra el schema local fallido; no se reservó número ni se llamó al proveedor.",
                      "required": [
                        "success",
                        "error",
                        "code",
                        "paths",
                        "details"
                      ],
                      "properties": {
                        "success": {
                          "type": "boolean",
                          "const": false
                        },
                        "error": {
                          "type": "string",
                          "const": "Invoice payload failed validation"
                        },
                        "code": {
                          "type": "string",
                          "const": "INVOICE_VALIDATION"
                        },
                        "paths": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "required": [
                              "path"
                            ],
                            "properties": {
                              "path": {
                                "type": "string"
                              },
                              "message": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      }
                    },
                    {
                      "$ref": "#/components/schemas/InvoiceError"
                    }
                  ]
                },
                "examples": {
                  "validation": {
                    "summary": "Preflight fallido — nada se reservó ni emitió.",
                    "value": {
                      "success": false,
                      "error": "Invoice payload failed validation",
                      "code": "INVOICE_VALIDATION",
                      "paths": [
                        "payment_form.payment_due_date"
                      ],
                      "details": [
                        {
                          "path": "payment_form.payment_due_date",
                          "message": "Path `payment_form.payment_due_date` is required."
                        }
                      ]
                    }
                  },
                  "customer": {
                    "value": {
                      "success": false,
                      "error": "customer is required"
                    }
                  },
                  "lines": {
                    "value": {
                      "success": false,
                      "error": "invoice_lines is required and must not be empty"
                    }
                  },
                  "identification": {
                    "value": {
                      "success": false,
                      "error": "customer.identification_number is required"
                    }
                  },
                  "total": {
                    "value": {
                      "success": false,
                      "error": "legal_monetary_totals.payable_amount must be greater than 0"
                    }
                  },
                  "config": {
                    "value": {
                      "success": false,
                      "error": "La organización no tiene configurada la facturación electrónica.",
                      "code": "INVOICE_CONFIG_MISSING"
                    }
                  },
                  "quotation": {
                    "value": {
                      "success": false,
                      "error": "La cotización no existe en esta organización",
                      "code": "QUOTATION_NOT_FOUND",
                      "field": "quotationId"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "description": "Permiso insuficiente, módulo de facturación no incluido en el plan, o límite mensual de facturas del plan alcanzado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "examples": {
                  "permission": {
                    "value": {
                      "message": "Permiso insuficiente. Se requiere: invoices:write",
                      "code": "INSUFFICIENT_API_PERMISSIONS",
                      "requiredPermission": "invoices:write",
                      "availablePermissions": [
                        "invoices:read"
                      ]
                    }
                  },
                  "module": {
                    "value": {
                      "message": "La facturación electrónica es un módulo adicional de tu plan. Contáctanos para activarlo.",
                      "code": "PLAN_MODULE_REQUIRED",
                      "module": "invoices"
                    }
                  },
                  "limit": {
                    "value": {
                      "message": "Has alcanzado el límite de tu plan. Actualiza tu plan para continuar.",
                      "code": "PLAN_LIMIT_REACHED",
                      "details": {
                        "allowed": false,
                        "limit": 300,
                        "used": 300,
                        "remaining": 0,
                        "planName": "Pro"
                      }
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "La resolución DIAN está agotada — el siguiente consecutivo supera su tope. No se quema número.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceNumberingError"
                },
                "example": {
                  "success": false,
                  "error": "La resolución de facturación está agotada: el siguiente número (5001) supera el tope autorizado (5000). Registra una resolución nueva en Configuración → Facturación.",
                  "code": "RESOLUTION_EXHAUSTED",
                  "next": 5001,
                  "to": 5000
                }
              }
            }
          },
          "422": {
            "description": "El proveedor de facturación rechazó el payload por validación (código reenviado tal cual). Número quemado, sin copia local.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceProviderError"
                },
                "examples": {
                  "validation": {
                    "summary": "El proveedor rechazó el payload (422); el número FS-624 quedó quemado.",
                    "value": {
                      "success": false,
                      "error": "API Error (422): The given data was invalid.",
                      "burnedNumber": {
                        "prefix": "FS",
                        "number": 624
                      },
                      "errors": {
                        "customer.name": [
                          "The customer.name field is required."
                        ]
                      },
                      "details": "The given data was invalid."
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Rechazo de la DIAN (Invalid invoice), fallo lógico o de red del proveedor, o cualquier otro estado devuelto por el proveedor: todos con burnedNumber. También error interno genérico (success:false + error + message), p. ej. DV no calculable, validación del modelo local tras emitir, o fallo validando límites del plan (message + error, sin success).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvoiceProviderError"
                    },
                    {
                      "$ref": "#/components/schemas/InvoiceError"
                    },
                    {
                      "$ref": "#/components/schemas/ApiError"
                    }
                  ]
                },
                "examples": {
                  "dian": {
                    "summary": "La DIAN rechazó el documento (IsValid false); responde 500 con el número quemado.",
                    "value": {
                      "success": false,
                      "error": "Invalid invoice: Regla: FAJ43b, Rechazo: Total de línea no coincide con cantidad por precio",
                      "burnedNumber": {
                        "prefix": "FS",
                        "number": 625
                      }
                    }
                  },
                  "internal": {
                    "value": {
                      "success": false,
                      "error": "Internal server error",
                      "message": "El NIT/cédula 'ABC123' no es válido(a)."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/invoices/withholdings": {
      "put": {
        "operationId": "putInvoiceWithholdingsByNumber",
        "tags": [
          "Invoices"
        ],
        "summary": "Registrar retenciones por prefijo y número",
        "description": "Igual que PUT /invoices/{id}/withholdings pero ubicando la factura por prefix + number (o por invoiceId en el body). Requiere invoices:write o * y el módulo de facturación. Registra las retenciones que el CLIENTE practicó al pagar: no emite nada ante la DIAN ni consume consecutivo. REEMPLAZA el set completo (idempotente). Netea la cartera (balance = total − retenciones − abonos) y sale en el Excel del contador y en el informe mensual. Solo facturas de venta.",
        "x-required-permissions": [
          "invoices:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/paths/~1invoices~1{id}~1withholdings/put/requestBody/content/application~1json/schema"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "invoiceId": {
                        "type": "string",
                        "description": "Id de la factura (si se manda",
                        "prefix y number se ignoran).": null
                      },
                      "prefix": {
                        "type": "string",
                        "description": "Prefijo de la factura (FS)."
                      },
                      "number": {
                        "type": "integer",
                        "description": "Número de la factura."
                      }
                    }
                  }
                ]
              },
              "examples": {
                "byPct": {
                  "value": {
                    "prefix": "FS",
                    "number": 690,
                    "items": [
                      {
                        "key": "compras25",
                        "pct": 2.5
                      }
                    ]
                  }
                },
                "dryRun": {
                  "value": {
                    "prefix": "FS",
                    "number": 690,
                    "items": [
                      {
                        "key": "compras25",
                        "amount": 24012.5
                      }
                    ],
                    "dryRun": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Retenciones registradas (o calculadas, con dryRun).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "invoice",
                        "withholdings",
                        "netPayable"
                      ],
                      "properties": {
                        "invoice": {
                          "type": "object",
                          "required": [
                            "id",
                            "prefix",
                            "number",
                            "fullNumber"
                          ],
                          "properties": {
                            "id": {
                              "type": "string"
                            },
                            "prefix": {
                              "type": "string"
                            },
                            "number": {
                              "type": "integer"
                            },
                            "fullNumber": {
                              "type": "string"
                            }
                          }
                        },
                        "withholdings": {
                          "oneOf": [
                            {
                              "$ref": "#/components/schemas/Invoice/properties/withholdings/oneOf/0"
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "netPayable": {
                          "type": "number"
                        },
                        "dryRun": {
                          "type": "boolean",
                          "description": "Solo en PUT. true = no se guardó nada."
                        },
                        "removed": {
                          "type": "boolean",
                          "description": "Solo en DELETE. false si la factura no tenía retenciones."
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "applied": {
                    "summary": "FS-690 con retefuente 2.5% (base 960500, IVA 182495, total 1142995)",
                    "value": {
                      "success": true,
                      "data": {
                        "invoice": {
                          "id": "6ab2a04344842ad65f09f187",
                          "prefix": "FS",
                          "number": 690,
                          "fullNumber": "FS-690"
                        },
                        "withholdings": {
                          "base": 960500,
                          "iva": 182495,
                          "items": [
                            {
                              "key": "compras25",
                              "kind": "rteFuente",
                              "label": "Compras (2.5%)",
                              "pct": 2.5,
                              "amount": 24012.5
                            }
                          ],
                          "byKind": {
                            "rteFuente": 24012.5,
                            "rteIva": 0,
                            "rteIca": 0
                          },
                          "total": 24012.5,
                          "netPayable": 1118982.5,
                          "appliedAt": "2026-09-22T16:10:00.000Z",
                          "appliedBy": "6594a74983de58ca5547b945",
                          "source": "api"
                        },
                        "netPayable": 1118982.5,
                        "dryRun": false
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Id inválido, sin ítems, concepto desconocido o monto no positivo.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": false
                    },
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "INVOICE_NOT_FOUND",
                        "INVALID_INVOICE_REF",
                        "NOT_A_SALES_INVOICE",
                        "EMPTY_ITEMS",
                        "UNKNOWN_CONCEPT",
                        "AMOUNT_NOT_POSITIVE",
                        "PCT_WITHOUT_BASE",
                        "RTEIVA_EXCEEDS_IVA",
                        "TOTAL_EXCEEDS_BASE_PLUS_IVA"
                      ]
                    }
                  }
                },
                "examples": {
                  "unknownConcept": {
                    "value": {
                      "success": false,
                      "error": "Concepto de retención desconocido: retefuente.",
                      "code": "UNKNOWN_CONCEPT"
                    }
                  },
                  "empty": {
                    "value": {
                      "success": false,
                      "error": "Agrega al menos una retención.",
                      "code": "EMPTY_ITEMS"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1invoices/get/responses/403"
          },
          "404": {
            "description": "Factura inexistente o de otra organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1invoices~1withholdings/put/responses/400/content/application~1json/schema"
                },
                "example": {
                  "success": false,
                  "error": "Factura no encontrada.",
                  "code": "INVOICE_NOT_FOUND"
                }
              }
            }
          },
          "422": {
            "description": "La factura no admite retenciones o las cifras no cuadran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1invoices~1withholdings/put/responses/400/content/application~1json/schema"
                },
                "examples": {
                  "notSales": {
                    "value": {
                      "success": false,
                      "error": "Solo se aplican retenciones a facturas de venta (no a notas ni a facturas de proveedor).",
                      "code": "NOT_A_SALES_INVOICE"
                    }
                  },
                  "exceeds": {
                    "value": {
                      "success": false,
                      "error": "La ReteIVA ($200000) no puede superar el IVA de la factura ($182495).",
                      "code": "RTEIVA_EXCEEDS_IVA"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1invoices/get/responses/500"
          }
        }
      }
    },
    "/invoices/receivables": {
      "get": {
        "operationId": "listReceivables",
        "tags": [
          "Invoices"
        ],
        "summary": "Cartera (facturas de venta con su saldo)",
        "description": "Requiere invoices:read o * y el módulo de facturación. Una fila compacta por factura de venta cobrable (contado y crédito; sin rechazadas, anuladas, borradores ni notas), sin XML ni líneas: es lo que hay que usar para conciliar pagos. El saldo se calcula: total − retenciones − notas crédito − abonos. status open (default) = con saldo. Los totales y los tramos de antigüedad son de todo lo filtrado; limit solo recorta las filas.",
        "x-required-permissions": [
          "invoices:read"
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "pending",
                "partial",
                "overdue",
                "paid",
                "credited",
                "all"
              ],
              "default": "open"
            },
            "description": "credited = anuladas por nota crédito."
          },
          {
            "name": "dateFrom",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Fecha de emisión desde (YYYY-MM-DD, inclusive)."
          },
          {
            "name": "dateTo",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Fecha de emisión hasta (YYYY-MM-DD, inclusive)."
          },
          {
            "name": "nit",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "NIT o cédula del cliente, exacto, sin DV (se ignoran puntos y guiones)."
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Texto en el nombre del cliente (regex escapada, sin mayúsculas)."
          },
          {
            "name": "contactId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cartera.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "receivables",
                        "aging",
                        "totals"
                      ],
                      "properties": {
                        "receivables": {
                          "type": "array",
                          "items": {
                            "$ref": "#/paths/~1invoices~1{id}~1payments/get/responses/200/content/application~1json/schema/properties/data/properties/receivable"
                          }
                        },
                        "aging": {
                          "type": "object",
                          "description": "Saldo y número de facturas por tramo de antigüedad (corriente, 1-30, 31-60, 61-90, 90+).",
                          "additionalProperties": true
                        },
                        "totals": {
                          "type": "object",
                          "required": [
                            "count",
                            "balance"
                          ],
                          "properties": {
                            "count": {
                              "type": "integer",
                              "description": "Facturas que cumplen el filtro (no solo las devueltas)."
                            },
                            "balance": {
                              "type": "number"
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "receivables": [
                      {
                        "invoiceId": "6ab2a04344842ad65f09f190",
                        "prefix": "FS",
                        "number": 702,
                        "reference": "FS702",
                        "fullNumber": "FS-702",
                        "date": "2026-09-29",
                        "customerName": "Gran Papeleria Bolivar S.A.S.",
                        "customerNit": "860400602",
                        "customerId": null,
                        "paymentForm": 2,
                        "dianStatus": "valido",
                        "total": 242998,
                        "withheld": 0,
                        "netTotal": 242998,
                        "paid": 0,
                        "balance": 242998,
                        "status": "pending",
                        "dueDate": "2026-10-29",
                        "daysOverdue": 0,
                        "isCredit": true
                      }
                    ],
                    "aging": {
                      "corriente": {
                        "count": 1,
                        "balance": 242998
                      }
                    },
                    "totals": {
                      "count": 1,
                      "balance": 242998
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1invoices/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1invoices/get/responses/500"
          }
        }
      }
    },
    "/invoices/payments": {
      "post": {
        "operationId": "registerInvoicePaymentsBatch",
        "tags": [
          "Invoices"
        ],
        "summary": "Registrar abonos en lote (conciliación del extracto)",
        "description": "Requiere invoices:write o * y el módulo de facturación. No emite nada ante la DIAN ni cambia el estado DIAN. Cada ítem identifica la factura por id o por prefix + number y es independiente (NO es atómico): uno con error queda con result error y su code, y no frena a los demás. Un abono no puede pasar del saldo; con full true se abona el saldo exacto. Si el pago llegó corto por retenciones, withholdings en el mismo ítem las aplica antes del abono. Antiduplicados por defecto (mismo monto el mismo día en la misma factura → skipped). En dryRun los ítems sobre la misma factura se simulan en orden. Un error inesperado queda en su ítem (INTERNAL_ERROR). Máximo 100 ítems. Recomendado: primero con dryRun.",
        "x-required-permissions": [
          "invoices:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "type": "object",
                    "properties": {
                      "dryRun": {
                        "type": "boolean",
                        "description": "true = calcular sin guardar."
                      },
                      "skipDuplicates": {
                        "type": "boolean",
                        "default": true,
                        "description": "Antiduplicados por factura: un abono con el mismo monto el mismo día (hora de Bogotá) se salta con result skipped, aunque la referencia sea distinta (el mismo pago registrado antes desde la web). Repetir una conciliación es seguro. false solo si de verdad hubo dos pagos del mismo monto el mismo día."
                      }
                    }
                  },
                  {
                    "type": "object",
                    "required": [
                      "items"
                    ],
                    "properties": {
                      "items": {
                        "type": "array",
                        "minItems": 1,
                        "maxItems": 100,
                        "items": {
                          "allOf": [
                            {
                              "$ref": "#/paths/~1invoices~1{id}~1payments/post/requestBody/content/application~1json/schema/allOf/0"
                            },
                            {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "string",
                                  "description": "Id de la factura (preferido)."
                                },
                                "prefix": {
                                  "type": "string",
                                  "description": "Prefijo (si no hay id)."
                                },
                                "number": {
                                  "type": "integer",
                                  "description": "Número (si no hay id)."
                                }
                              }
                            }
                          ]
                        }
                      }
                    }
                  }
                ]
              },
              "examples": {
                "conciliacion": {
                  "value": {
                    "dryRun": true,
                    "items": [
                      {
                        "prefix": "FS",
                        "number": 702,
                        "amount": 242998,
                        "paidAt": "2026-09-30",
                        "method": "transferencia",
                        "reference": "GRAN PAPELERIA B 033741729"
                      },
                      {
                        "prefix": "FS",
                        "number": 707,
                        "amount": 1770000,
                        "full": true,
                        "paidAt": "2026-09-28",
                        "reference": "SUCURSAL VIRTUAL 3 48-83861045"
                      },
                      {
                        "prefix": "FS",
                        "number": 616,
                        "amount": 640357,
                        "paidAt": "2026-09-18",
                        "reference": "COMPANIA DE JESU 265073461",
                        "withholdings": [
                          {
                            "key": "reteIva",
                            "amount": 15713.8
                          }
                        ]
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resultado por ítem y resumen.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "dryRun",
                        "summary",
                        "results"
                      ],
                      "properties": {
                        "dryRun": {
                          "type": "boolean"
                        },
                        "summary": {
                          "type": "object",
                          "required": [
                            "total",
                            "registered",
                            "wouldRegister",
                            "skipped",
                            "errors",
                            "amount"
                          ],
                          "properties": {
                            "total": {
                              "type": "integer"
                            },
                            "registered": {
                              "type": "integer"
                            },
                            "wouldRegister": {
                              "type": "integer"
                            },
                            "skipped": {
                              "type": "integer"
                            },
                            "errors": {
                              "type": "integer"
                            },
                            "amount": {
                              "type": "number",
                              "description": "Suma de lo registrado (o de lo que se registraría con dryRun)."
                            }
                          }
                        },
                        "results": {
                          "type": "array",
                          "description": "Uno por ítem, en el mismo orden (index). NO es atómico.",
                          "items": {
                            "oneOf": [
                              {
                                "allOf": [
                                  {
                                    "type": "object",
                                    "required": [
                                      "index"
                                    ],
                                    "properties": {
                                      "index": {
                                        "type": "integer"
                                      }
                                    }
                                  },
                                  {
                                    "$ref": "#/paths/~1invoices~1{id}~1payments/post/responses/200/content/application~1json/schema/properties/data/allOf/0"
                                  }
                                ]
                              },
                              {
                                "type": "object",
                                "required": [
                                  "index",
                                  "result",
                                  "code",
                                  "message"
                                ],
                                "properties": {
                                  "index": {
                                    "type": "integer"
                                  },
                                  "result": {
                                    "type": "string",
                                    "const": "error"
                                  },
                                  "code": {
                                    "type": "string"
                                  },
                                  "message": {
                                    "type": "string"
                                  }
                                }
                              }
                            ]
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "dryRun": true,
                    "summary": {
                      "total": 2,
                      "registered": 0,
                      "wouldRegister": 1,
                      "skipped": 0,
                      "errors": 1,
                      "amount": 242998
                    },
                    "results": [
                      {
                        "index": 0,
                        "result": "would_register",
                        "dryRun": true,
                        "withholdingsApplied": null,
                        "payment": {
                          "amount": 242998,
                          "paidAt": "2026-09-30T17:00:00.000Z",
                          "method": "transferencia",
                          "reference": "GRAN PAPELERIA B 033741729"
                        },
                        "before": {
                          "total": 242998,
                          "withheld": 0,
                          "netTotal": 242998,
                          "paid": 0,
                          "balance": 242998,
                          "status": "pending"
                        },
                        "receivable": {
                          "invoiceId": "6ab2a04344842ad65f09f190",
                          "prefix": "FS",
                          "number": 702,
                          "fullNumber": "FS-702",
                          "total": 242998,
                          "withheld": 0,
                          "netTotal": 242998,
                          "paid": 242998,
                          "balance": 0,
                          "status": "paid"
                        }
                      },
                      {
                        "index": 1,
                        "result": "error",
                        "code": "INVOICE_NOT_FOUND",
                        "message": "Factura no encontrada."
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "Lote vacío o de más de 100 ítems.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": false
                    },
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "INVOICE_NOT_FOUND",
                        "INVALID_INVOICE_REF",
                        "NOT_COLLECTIBLE",
                        "PAYMENT_INVALID",
                        "ALREADY_PAID",
                        "PAYMENT_NOT_FOUND",
                        "BATCH_INVALID",
                        "CONFLICT",
                        "NOT_A_SALES_INVOICE",
                        "EMPTY_ITEMS",
                        "UNKNOWN_CONCEPT",
                        "AMOUNT_NOT_POSITIVE",
                        "PCT_WITHOUT_BASE",
                        "RTEIVA_EXCEEDS_IVA",
                        "TOTAL_EXCEEDS_BASE_PLUS_IVA"
                      ]
                    }
                  }
                },
                "example": {
                  "success": false,
                  "error": "Manda al menos un abono en `items`.",
                  "code": "BATCH_INVALID"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1invoices/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1invoices/get/responses/500"
          }
        }
      }
    },
    "/invoices/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB de la factura (campo id de las respuestas). No confundir con number ni fullNumber.",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "507f1f77bcf86cd799439071"
        }
      ],
      "get": {
        "operationId": "getInvoiceById",
        "tags": [
          "Invoices"
        ],
        "summary": "Consultar una factura por ID",
        "description": "Requiere invoices:read o *, y que el plan incluya el módulo de facturación electrónica. La factura debe pertenecer a la organización del token. Devuelve cualquier dirección y tipo de documento (emitida, nota o entrante).",
        "x-required-permissions": [
          "invoices:read"
        ],
        "responses": {
          "200": {
            "description": "Factura encontrada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceResult"
                },
                "examples": {
                  "found": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439071",
                        "number": 623,
                        "prefix": "FS",
                        "fullNumber": "FS-623",
                        "direction": "outgoing",
                        "type_document_id": 1,
                        "date": "2026-09-05",
                        "time": "10:15:00",
                        "status": "valido",
                        "customer": {
                          "identification_number": 900123456,
                          "dv": 1,
                          "name": "Microm S.A.S",
                          "phone": "3001234567",
                          "address": "Calle 10",
                          "email": "facturacion@example.com",
                          "type_document_identification_id": 6,
                          "type_organization_id": 1,
                          "type_liability_id": 117,
                          "type_regime_id": 2,
                          "municipality_id": 149
                        },
                        "contact": {
                          "id": "507f1f77bcf86cd799439011",
                          "fullName": "Ana Ejemplo",
                          "company": "Microm S.A.S",
                          "email": "ana@example.com",
                          "mobile": "573000000000",
                          "idNumber": "900123456"
                        },
                        "invoice_lines": [
                          {
                            "unit_measure_id": 70,
                            "invoiced_quantity": "500",
                            "line_extension_amount": "200000.00",
                            "free_of_charge_indicator": false,
                            "tax_totals": [
                              {
                                "tax_id": 1,
                                "tax_amount": "38000.00",
                                "percent": "19.00",
                                "taxable_amount": "200000.00"
                              }
                            ],
                            "description": "Manilla Tyvek 19mm impresa a 1 tinta",
                            "notes": "",
                            "code": "TYV19",
                            "type_item_identification_id": 4,
                            "price_amount": "400.00",
                            "base_quantity": "1.000000"
                          }
                        ],
                        "legal_monetary_totals": {
                          "line_extension_amount": "200000.00",
                          "tax_exclusive_amount": "200000.00",
                          "tax_inclusive_amount": "238000.00",
                          "allowance_total_amount": "0.00",
                          "payable_amount": "238000.00"
                        },
                        "tax_totals": [
                          {
                            "tax_id": 1,
                            "tax_amount": "38000.00",
                            "percent": "19.00",
                            "taxable_amount": "200000.00"
                          }
                        ],
                        "payment_form": {
                          "payment_form_id": 2,
                          "payment_method_id": 47,
                          "payment_due_date": "2026-10-05",
                          "duration_measure": "30"
                        },
                        "dian_response": {
                          "isValid": "true",
                          "statusCode": "00",
                          "statusDescription": "Procesado Correctamente.",
                          "statusMessage": "La Factura electrónica FS-623, ha sido autorizada.",
                          "cufe": "3a0b1c2d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f6071",
                          "qr": "https://catalogo-vpfe.dian.gov.co/document/searchqr?documentkey=3a0b1c2d4e5f6071",
                          "IsValid": "true",
                          "StatusCode": "00",
                          "StatusDescription": "Procesado Correctamente.",
                          "StatusMessage": "La Factura electrónica FS-623, ha sido autorizada."
                        },
                        "notes": "Pedido 4521 — entrega en Medellín",
                        "createdAt": "2026-09-05T15:15:03.000Z",
                        "updatedAt": "2026-09-05T15:15:03.000Z",
                        "withholdings": null,
                        "netPayable": 238000
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID de factura inválido (no es un ObjectId).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceError"
                },
                "example": {
                  "success": false,
                  "error": "Invalid invoice ID"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1invoices/get/responses/403"
          },
          "404": {
            "description": "Factura inexistente o no perteneciente a la organización del token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceError"
                },
                "example": {
                  "success": false,
                  "error": "Invoice not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1invoices/get/responses/500"
          }
        }
      }
    },
    "/invoices/{id}/withholdings": {
      "parameters": [
        {
          "$ref": "#/paths/~1invoices~1{id}/parameters/0"
        }
      ],
      "get": {
        "operationId": "getInvoiceWithholdings",
        "tags": [
          "Invoices"
        ],
        "summary": "Consultar las retenciones de una factura",
        "description": "Requiere invoices:read o * y el módulo de facturación. Devuelve las retenciones que el cliente practicó (null si no hay) y netPayable. Es lo que hay que consultar antes de registrar, porque PUT reemplaza el set completo.",
        "x-required-permissions": [
          "invoices:read"
        ],
        "responses": {
          "200": {
            "description": "Retenciones de la factura (withholdings null si no tiene).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1invoices~1withholdings/put/responses/200/content/application~1json/schema"
                },
                "examples": {
                  "withholdings": {
                    "$ref": "#/paths/~1invoices~1withholdings/put/responses/200/content/application~1json/examples/applied"
                  },
                  "none": {
                    "summary": "Factura sin retenciones",
                    "value": {
                      "success": true,
                      "data": {
                        "invoice": {
                          "id": "507f1f77bcf86cd799439071",
                          "prefix": "FS",
                          "number": 623,
                          "fullNumber": "FS-623"
                        },
                        "withholdings": null,
                        "netPayable": 119000
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1invoices~1withholdings/put/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1invoices/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1invoices~1withholdings/put/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1invoices/get/responses/500"
          }
        }
      },
      "put": {
        "operationId": "putInvoiceWithholdings",
        "tags": [
          "Invoices"
        ],
        "summary": "Registrar o reemplazar las retenciones de una factura",
        "description": "Requiere invoices:write o * y el módulo de facturación. Registra las retenciones que el CLIENTE practicó al pagar (retefuente, ReteIVA, ReteICA): no emite nada ante la DIAN. Cada ítem lleva amount o pct; con pct el servidor calcula sobre la base sin IVA (sobre el IVA para reteIva); sin ninguno usa el porcentaje sugerido del concepto. REEMPLAZA el set completo (idempotente): para editar, reenviar todos los ítems. Con dryRun devuelve el cálculo sin guardar. Efectos: netea la cartera (balance = total − retenciones − abonos) y aparece en el Excel del contador y en el informe mensual. Solo facturas de venta (422 NOT_A_SALES_INVOICE para notas y entrantes).",
        "x-required-permissions": [
          "invoices:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "items"
                ],
                "properties": {
                  "items": {
                    "type": "array",
                    "minItems": 1,
                    "description": "Retenciones a registrar. REEMPLAZAN el set completo de la factura (idempotente). Cada ítem lleva amount o pct; sin ninguno se usa el porcentaje sugerido del concepto. Con pct el servidor calcula sobre la base sin IVA (sobre el IVA para reteIva) y redondea a 2 decimales.",
                    "items": {
                      "type": "object",
                      "required": [
                        "key"
                      ],
                      "properties": {
                        "key": {
                          "type": "string",
                          "enum": [
                            "compras25",
                            "compras35",
                            "serviciosGeneral4",
                            "serviciosGeneral6",
                            "honorarios10",
                            "honorarios11",
                            "arrendamientoBienesMuebles",
                            "arrendamientoBienesRaices",
                            "serviciosAseoVigilancia",
                            "serviciosHotelesRestaurantes",
                            "transporteCarga",
                            "reteIva",
                            "reteIca"
                          ],
                          "description": "Concepto de retención. Porcentajes sugeridos: compras25 2.5, compras35 3.5, serviciosGeneral4 4, serviciosGeneral6 6, honorarios10 10, honorarios11 11, arrendamientoBienesMuebles 4, arrendamientoBienesRaices 3.5, serviciosAseoVigilancia 2, serviciosHotelesRestaurantes 3.5, transporteCarga 1, reteIva 15 (sobre el IVA), reteIca sin sugerencia (enviar amount)."
                        },
                        "amount": {
                          "type": "number",
                          "description": "Monto retenido en COP (> 0). Manda si difiere del cálculo."
                        },
                        "pct": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 100
                        }
                      }
                    }
                  },
                  "dryRun": {
                    "type": "boolean",
                    "description": "true = devolver el cálculo sin guardar."
                  }
                }
              },
              "examples": {
                "byPct": {
                  "value": {
                    "items": [
                      {
                        "key": "compras25",
                        "pct": 2.5
                      }
                    ]
                  }
                },
                "byAmount": {
                  "value": {
                    "items": [
                      {
                        "key": "compras25",
                        "amount": 24012.5
                      },
                      {
                        "key": "reteIva",
                        "amount": 27374.25
                      }
                    ]
                  }
                },
                "dryRun": {
                  "value": {
                    "items": [
                      {
                        "key": "compras25"
                      }
                    ],
                    "dryRun": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Retenciones registradas (o calculadas, con dryRun).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1invoices~1withholdings/put/responses/200/content/application~1json/schema"
                },
                "examples": {
                  "applied": {
                    "$ref": "#/paths/~1invoices~1withholdings/put/responses/200/content/application~1json/examples/applied"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1invoices~1withholdings/put/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1invoices/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1invoices~1withholdings/put/responses/404"
          },
          "422": {
            "$ref": "#/paths/~1invoices~1withholdings/put/responses/422"
          },
          "500": {
            "$ref": "#/paths/~1invoices/get/responses/500"
          }
        }
      },
      "delete": {
        "operationId": "deleteInvoiceWithholdings",
        "tags": [
          "Invoices"
        ],
        "summary": "Quitar las retenciones de una factura",
        "description": "Requiere invoices:write o * y el módulo de facturación. Idempotente: si la factura no tenía retenciones responde 200 con removed false. La cartera vuelve a cobrar el total.",
        "x-required-permissions": [
          "invoices:write"
        ],
        "responses": {
          "200": {
            "description": "Retenciones eliminadas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1invoices~1withholdings/put/responses/200/content/application~1json/schema"
                },
                "example": {
                  "success": true,
                  "data": {
                    "invoice": {
                      "id": "6ab2a04344842ad65f09f187",
                      "prefix": "FS",
                      "number": 690,
                      "fullNumber": "FS-690"
                    },
                    "withholdings": null,
                    "netPayable": 1142995,
                    "removed": true
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1invoices~1withholdings/put/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1invoices/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1invoices~1withholdings/put/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1invoices/get/responses/500"
          }
        }
      }
    },
    "/invoices/{id}/payments": {
      "parameters": [
        {
          "$ref": "#/paths/~1invoices~1{id}/parameters/0"
        }
      ],
      "get": {
        "operationId": "getInvoicePayments",
        "tags": [
          "Invoices"
        ],
        "summary": "Estado de cobro y abonos de una factura",
        "description": "Requiere invoices:read o * y el módulo de facturación.",
        "x-required-permissions": [
          "invoices:read"
        ],
        "responses": {
          "200": {
            "description": "Estado de cobro y abonos.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "receivable",
                        "payments"
                      ],
                      "properties": {
                        "receivable": {
                          "type": "object",
                          "description": "Fila compacta de cartera (sin XML ni líneas de la factura).",
                          "required": [
                            "invoiceId",
                            "prefix",
                            "number",
                            "fullNumber",
                            "total",
                            "withheld",
                            "netTotal",
                            "paid",
                            "balance",
                            "status"
                          ],
                          "properties": {
                            "invoiceId": {
                              "type": "string"
                            },
                            "prefix": {
                              "type": "string"
                            },
                            "number": {
                              "type": "integer"
                            },
                            "reference": {
                              "type": "string",
                              "description": "Prefijo y número pegados (FS702)."
                            },
                            "fullNumber": {
                              "type": "string",
                              "description": "FS-702"
                            },
                            "date": {
                              "type": "string",
                              "description": "Fecha de emisión (YYYY-MM-DD)."
                            },
                            "customerName": {
                              "type": "string"
                            },
                            "customerNit": {
                              "type": [
                                "string",
                                "null"
                              ],
                              "description": "NIT o cédula del cliente",
                              "sin DV.": null
                            },
                            "customerId": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "paymentForm": {
                              "type": [
                                "integer",
                                "null"
                              ],
                              "description": "1 contado",
                              "2 crédito.": null
                            },
                            "dianStatus": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "total": {
                              "type": "number",
                              "description": "Total facturado (payable_amount)."
                            },
                            "withheld": {
                              "type": "number",
                              "description": "Retenciones que practicó el cliente."
                            },
                            "credited": {
                              "type": "number",
                              "description": "Lo que anularon las notas crédito válidas que la referencian."
                            },
                            "creditNotes": {
                              "type": "array",
                              "description": "Notas crédito que la afectan (para mostrar \"Anulada · NC-143\").",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "fullNumber": {
                                    "type": "string"
                                  },
                                  "amount": {
                                    "type": "number"
                                  },
                                  "date": {
                                    "type": [
                                      "string",
                                      "null"
                                    ]
                                  }
                                }
                              }
                            },
                            "netTotal": {
                              "type": "number",
                              "description": "Lo que el cliente consigna = total − withheld − credited."
                            },
                            "paid": {
                              "type": "number",
                              "description": "Suma de abonos."
                            },
                            "balance": {
                              "type": "number",
                              "description": "Lo que falta por cobrar (nunca negativo)."
                            },
                            "status": {
                              "type": "string",
                              "enum": [
                                "pending",
                                "partial",
                                "overdue",
                                "paid",
                                "credited"
                              ],
                              "description": "Estado de COBRO (eje distinto del estado DIAN). overdue pisa a pending y partial cuando la factura ya venció; paid = saldo en cero con abonos; credited = \"Anulada\": notas crédito la dejaron en cero sin abonos (no es un cobro)."
                            },
                            "dueDate": {
                              "type": [
                                "string",
                                "null"
                              ],
                              "description": "Vencimiento; contado vence el día de emisión."
                            },
                            "daysOverdue": {
                              "type": "integer"
                            },
                            "isCredit": {
                              "type": "boolean"
                            }
                          }
                        },
                        "collectible": {
                          "type": "boolean",
                          "description": "false si está rechazada",
                          "anulada": null,
                          "en borrador o es una nota.": null
                        },
                        "payments": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "required": [
                              "amount",
                              "paidAt"
                            ],
                            "properties": {
                              "_id": {
                                "type": "string"
                              },
                              "amount": {
                                "type": "number"
                              },
                              "paidAt": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "method": {
                                "type": "string"
                              },
                              "reference": {
                                "type": "string"
                              },
                              "note": {
                                "type": "string"
                              },
                              "userId": {
                                "type": "string"
                              },
                              "createdAt": {
                                "type": "string",
                                "format": "date-time"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "receivable": {
                      "invoiceId": "6ab2a04344842ad65f09f187",
                      "prefix": "FS",
                      "number": 695,
                      "fullNumber": "FS-695",
                      "total": 148399.42,
                      "withheld": 0,
                      "netTotal": 148399.42,
                      "paid": 148399.42,
                      "balance": 0,
                      "status": "paid"
                    },
                    "collectible": true,
                    "payments": [
                      {
                        "_id": "6ab3c0d1e2f3a4b5c6d7e8f9",
                        "amount": 148399.42,
                        "paidAt": "2026-09-25T17:00:00.000Z",
                        "method": "transferencia"
                      }
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1invoices/get/responses/403"
          },
          "404": {
            "description": "Factura (o abono) inexistente o de otra organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1invoices~1payments/post/responses/400/content/application~1json/schema"
                },
                "examples": {
                  "invoice": {
                    "value": {
                      "success": false,
                      "error": "Factura no encontrada.",
                      "code": "INVOICE_NOT_FOUND"
                    }
                  },
                  "payment": {
                    "value": {
                      "success": false,
                      "error": "Abono no encontrado.",
                      "code": "PAYMENT_NOT_FOUND"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1invoices/get/responses/500"
          }
        }
      },
      "post": {
        "operationId": "registerInvoicePayment",
        "tags": [
          "Invoices"
        ],
        "summary": "Registrar un abono a una factura",
        "description": "Requiere invoices:write o * y el módulo de facturación. Mismas reglas que un ítem del lote (POST /invoices/payments). Responde 201 si registró y 200 con dryRun o si el antiduplicados lo saltó.",
        "x-required-permissions": [
          "invoices:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "type": "object",
                    "properties": {
                      "amount": {
                        "type": [
                          "number",
                          "string"
                        ],
                        "description": "Monto recibido en COP. Número, o texto sin separadores de miles (\"1500000.50\"). No puede pasar del saldo salvo con full."
                      },
                      "full": {
                        "type": "boolean",
                        "description": "true = abonar exactamente el saldo. Para el pago que llega con unos pesos de más (redondeo); lo recibido queda en la nota. Se rechaza si amount se aleja del saldo más de $1.000 o 1 % (un pago parcial va sin full)."
                      },
                      "paidAt": {
                        "type": "string",
                        "description": "Fecha del pago. YYYY-MM-DD se guarda al mediodía de Bogotá. Default: ahora."
                      },
                      "method": {
                        "type": "string",
                        "maxLength": 60
                      },
                      "reference": {
                        "type": "string",
                        "maxLength": 120,
                        "description": "Referencia o descripción del movimiento bancario."
                      },
                      "note": {
                        "type": "string",
                        "maxLength": 500
                      },
                      "withholdings": {
                        "type": "array",
                        "description": "Retenciones que el cliente practicó en este pago. Se aplican ANTES del abono y REEMPLAZAN las de la factura (mismas reglas que PUT /invoices/{id}/withholdings).",
                        "items": {
                          "type": "object",
                          "required": [
                            "key"
                          ],
                          "properties": {
                            "key": {
                              "$ref": "#/paths/~1invoices~1{id}~1withholdings/put/requestBody/content/application~1json/schema/properties/items/items/properties/key"
                            },
                            "amount": {
                              "type": "number"
                            },
                            "pct": {
                              "type": "number",
                              "minimum": 0,
                              "maximum": 100
                            }
                          }
                        }
                      }
                    }
                  },
                  {
                    "$ref": "#/paths/~1invoices~1payments/post/requestBody/content/application~1json/schema/allOf/0"
                  }
                ]
              },
              "example": {
                "amount": 98500,
                "paidAt": "2026-09-01",
                "method": "transferencia",
                "reference": "PAGO saldo manilas jbl"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "dryRun, o abono saltado por el antiduplicados (result skipped).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "allOf": [
                        {
                          "type": "object",
                          "required": [
                            "result",
                            "dryRun"
                          ],
                          "properties": {
                            "result": {
                              "type": "string",
                              "enum": [
                                "registered",
                                "would_register",
                                "skipped"
                              ],
                              "description": "would_register = dryRun; skipped = ya existía (antiduplicados)."
                            },
                            "reason": {
                              "type": "string",
                              "enum": [
                                "DUPLICATE"
                              ]
                            },
                            "message": {
                              "type": "string"
                            },
                            "existingPaymentId": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "dryRun": {
                              "type": "boolean"
                            },
                            "withholdingsApplied": {
                              "oneOf": [
                                {
                                  "$ref": "#/components/schemas/Invoice/properties/withholdings/oneOf/0"
                                },
                                {
                                  "type": "null"
                                }
                              ]
                            },
                            "payment": {
                              "oneOf": [
                                {
                                  "$ref": "#/paths/~1invoices~1{id}~1payments/get/responses/200/content/application~1json/schema/properties/data/properties/payments/items"
                                },
                                {
                                  "type": "null"
                                }
                              ]
                            },
                            "before": {
                              "type": "object",
                              "additionalProperties": true,
                              "description": "Estado de cobro antes del abono (ya con las retenciones nuevas)."
                            },
                            "receivable": {
                              "$ref": "#/paths/~1invoices~1{id}~1payments/get/responses/200/content/application~1json/schema/properties/data/properties/receivable"
                            }
                          }
                        },
                        {
                          "type": "object",
                          "properties": {
                            "payments": {
                              "type": "array",
                              "items": {
                                "$ref": "#/paths/~1invoices~1{id}~1payments/get/responses/200/content/application~1json/schema/properties/data/properties/payments/items"
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Abono registrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1invoices~1{id}~1payments/post/responses/200/content/application~1json/schema"
                },
                "example": {
                  "success": true,
                  "data": {
                    "result": "registered",
                    "dryRun": false,
                    "withholdingsApplied": null,
                    "payment": {
                      "_id": "6ab3c0d1e2f3a4b5c6d7e8fa",
                      "amount": 98500,
                      "paidAt": "2026-09-01T17:00:00.000Z",
                      "method": "transferencia",
                      "reference": "PAGO saldo manilas jbl"
                    },
                    "before": {
                      "total": 178500,
                      "withheld": 0,
                      "netTotal": 178500,
                      "paid": 0,
                      "balance": 178500,
                      "status": "overdue"
                    },
                    "receivable": {
                      "invoiceId": "6ab2a04344842ad65f09f188",
                      "prefix": "FS",
                      "number": 605,
                      "fullNumber": "FS-605",
                      "total": 178500,
                      "withheld": 0,
                      "netTotal": 178500,
                      "paid": 98500,
                      "balance": 80000,
                      "status": "overdue"
                    },
                    "payments": [
                      {
                        "_id": "6ab3c0d1e2f3a4b5c6d7e8fa",
                        "amount": 98500,
                        "paidAt": "2026-09-01T17:00:00.000Z",
                        "method": "transferencia",
                        "reference": "PAGO saldo manilas jbl"
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "Monto no positivo, mayor que el saldo, fecha inválida o retención inválida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1invoices~1payments/post/responses/400/content/application~1json/schema"
                },
                "examples": {
                  "exceeds": {
                    "value": {
                      "success": false,
                      "error": "El abono ($1770000) supera el saldo pendiente ($1769935).",
                      "code": "PAYMENT_INVALID"
                    }
                  },
                  "badDate": {
                    "value": {
                      "success": false,
                      "error": "La fecha del pago no es válida.",
                      "code": "PAYMENT_INVALID"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1invoices/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1invoices~1{id}~1payments/get/responses/404"
          },
          "409": {
            "description": "La factura no se cobra (rechazada, anulada, borrador o nota), ya está pagada, o cambió mientras se registraba (otro abono al mismo tiempo; CONFLICT, reintentar).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1invoices~1payments/post/responses/400/content/application~1json/schema"
                },
                "examples": {
                  "notCollectible": {
                    "value": {
                      "success": false,
                      "error": "Esta factura está rechazada, anulada, en borrador o es una nota: no se puede cobrar.",
                      "code": "NOT_COLLECTIBLE"
                    }
                  },
                  "paid": {
                    "value": {
                      "success": false,
                      "error": "Esta factura ya está pagada.",
                      "code": "ALREADY_PAID"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "$ref": "#/paths/~1invoices~1withholdings/put/responses/422"
          },
          "500": {
            "$ref": "#/paths/~1invoices/get/responses/500"
          }
        }
      }
    },
    "/invoices/{id}/payments/{paymentId}": {
      "parameters": [
        {
          "$ref": "#/paths/~1invoices~1{id}/parameters/0"
        },
        {
          "name": "paymentId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "_id del abono (sale de GET /invoices/{id}/payments)."
        }
      ],
      "delete": {
        "operationId": "deleteInvoicePayment",
        "tags": [
          "Invoices"
        ],
        "summary": "Eliminar un abono",
        "description": "Requiere invoices:write o * y el módulo de facturación. Quita un abono mal registrado y recalcula el saldo. No toca las retenciones.",
        "x-required-permissions": [
          "invoices:write"
        ],
        "responses": {
          "200": {
            "description": "Abono eliminado.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "receivable",
                        "payments"
                      ],
                      "properties": {
                        "receivable": {
                          "$ref": "#/paths/~1invoices~1{id}~1payments/get/responses/200/content/application~1json/schema/properties/data/properties/receivable"
                        },
                        "payments": {
                          "type": "array",
                          "items": {
                            "$ref": "#/paths/~1invoices~1{id}~1payments/get/responses/200/content/application~1json/schema/properties/data/properties/payments/items"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "receivable": {
                      "invoiceId": "6ab2a04344842ad65f09f188",
                      "prefix": "FS",
                      "number": 605,
                      "fullNumber": "FS-605",
                      "total": 178500,
                      "withheld": 0,
                      "netTotal": 178500,
                      "paid": 0,
                      "balance": 178500,
                      "status": "overdue"
                    },
                    "payments": []
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1invoices/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1invoices~1{id}~1payments/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1invoices/get/responses/500"
          }
        }
      }
    },
    "/purchases": {
      "get": {
        "operationId": "listPurchases",
        "tags": [
          "Purchases"
        ],
        "summary": "Listar órdenes de compra",
        "description": "Requiere purchases:read o *, y que el plan incluya el módulo de facturación electrónica (PLAN_MODULE_REQUIRED → 403), que desde el 03-sep-2026 cubre también compras y proveedores. Lista las órdenes de la organización del API Token con el proveedor resuelto. search escapa la expresión regular y busca en orderNumber, supplierName, supplierNIT y description. dateFrom/dateTo se parsean como fecha (400 si no son fechas válidas) y comparan purchaseDate, ambos inclusive. status y supplierId se validan: un valor fuera del enum o un supplierId que no es ObjectId responden 400 en vez de devolver lista vacía o reventar.",
        "x-required-permissions": [
          "purchases:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          },
          {
            "name": "kind",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "order",
                "invoice"
              ],
              "description": "invoice = factura del proveedor (el registro de gasto; el número es el de la factura). order = orden de compra: compromiso de gasto anterior a la factura, con consecutivo propio OC-n. Por defecto invoice. Misma colección, mismo formulario y mismas retenciones; lo que suma o certifica excluye las OC."
            },
            "description": "order = solo órdenes de compra; invoice = solo facturas de proveedor (incluye las registradas antes de que existiera el campo). Sin él devuelve ambas. Fuera del enum → 400."
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "completed",
                "canceled"
              ],
              "description": "Estado. Por defecto pending. En una OC: pending = abierta, completed = facturada (tiene invoiceRef), canceled = cancelada."
            }
          },
          {
            "name": "supplierId",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "ID del contacto proveedor asociado."
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Coincidencia parcial, sin distinguir mayúsculas, en orderNumber, supplierName, supplierNIT o description."
          },
          {
            "name": "dateFrom",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "purchaseDate desde (inclusive).",
            "example": "2026-09-01"
          },
          {
            "name": "dateTo",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "purchaseDate hasta (inclusive).",
            "example": "2026-09-30"
          },
          {
            "name": "sortBy",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "purchaseDate",
                "createdAt",
                "total",
                "orderNumber"
              ],
              "default": "purchaseDate"
            },
            "description": "Un valor fuera del enum cae a purchaseDate sin error."
          },
          {
            "name": "sortOrder",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Órdenes de compra y paginación; data puede estar vacío.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data",
                    "pagination"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/paths/~1purchases/post/responses/201/content/application~1json/schema/properties/data"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                },
                "examples": {
                  "list": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "507f1f77bcf86cd799439101",
                          "orderNumber": "OC-2026-014",
                          "kind": "invoice",
                          "orderRef": null,
                          "invoiceRef": null,
                          "supplierId": "507f1f77bcf86cd799439011",
                          "supplier": {
                            "id": "507f1f77bcf86cd799439011",
                            "fullName": "Papelería Central",
                            "company": "Papelería Central SAS",
                            "taxId": "901234567"
                          },
                          "supplierName": "Papelería Central SAS",
                          "supplierNIT": "901234567",
                          "purchaseDate": "2026-09-10T00:00:00.000Z",
                          "tax": 190000,
                          "total": 1190000,
                          "discounts": null,
                          "description": "Insumos de oficina del trimestre",
                          "paymentMethod": "Transferencia",
                          "voucherType": "Factura electrónica",
                          "status": "completed",
                          "items": [
                            {
                              "id": "507f1f77bcf86cd799439121",
                              "name": "Resma carta",
                              "description": "",
                              "quantity": 50,
                              "unitPrice": 20000,
                              "tax": 19,
                              "total": 1000000
                            }
                          ],
                          "withholdings": {
                            "status": "applied",
                            "conceptCode": "compras-generales",
                            "conceptLabel": "Compras generales",
                            "source": "auto",
                            "configYear": 2026,
                            "uvtValue": 50000,
                            "appliedAt": "2026-09-17T16:00:00.000Z",
                            "lines": [
                              {
                                "kind": "rteFuente",
                                "conceptCode": "compras-generales",
                                "conceptLabel": "Compras generales",
                                "base": 1000000,
                                "rate": 2.5,
                                "rateType": "percent",
                                "amount": 25000,
                                "manual": false,
                                "skippedReason": null
                              }
                            ]
                          },
                          "withholdingTotal": 25000,
                          "withheldByKind": {
                            "rteFuente": 25000,
                            "rteIva": 0,
                            "rteIca": 0
                          },
                          "netPayable": 1165000,
                          "lastModified": "2026-09-10T15:00:00.000Z",
                          "createdAt": "2026-09-10T15:00:00.000Z",
                          "updatedAt": "2026-09-10T15:00:00.000Z"
                        },
                        {
                          "id": "507f1f77bcf86cd799439102",
                          "orderNumber": "OC-2026-015",
                          "kind": "invoice",
                          "orderRef": null,
                          "invoiceRef": null,
                          "supplierId": null,
                          "supplier": null,
                          "supplierName": "Transportes del Valle",
                          "supplierNIT": "800111222",
                          "purchaseDate": "2026-09-12T00:00:00.000Z",
                          "tax": 0,
                          "total": 350000,
                          "discounts": null,
                          "description": "",
                          "paymentMethod": "Efectivo",
                          "voucherType": null,
                          "status": "pending",
                          "items": [],
                          "withholdings": null,
                          "withholdingTotal": 0,
                          "withheldByKind": {
                            "rteFuente": 0,
                            "rteIva": 0,
                            "rteIca": 0
                          },
                          "netPayable": 350000,
                          "lastModified": "2026-09-12T09:30:00.000Z",
                          "createdAt": "2026-09-12T09:30:00.000Z",
                          "updatedAt": "2026-09-12T09:30:00.000Z"
                        }
                      ],
                      "pagination": {
                        "total": 2,
                        "page": 1,
                        "limit": 100,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "status fuera del enum, supplierId que no es ObjectId, o dateFrom/dateTo no parseables.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Contrato de error de los controllers de compras (success:false + error).",
                  "required": [
                    "success",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": false
                    },
                    "error": {
                      "type": "string"
                    },
                    "required": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Solo en \"Missing required fields\" de POST."
                    },
                    "missing": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Los obligatorios que faltaron en ese POST."
                    },
                    "allowed": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Valores válidos cuando se rechaza status, voucherType o kind."
                    },
                    "details": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Solo en \"Validation error\" (mensajes del modelo Mongoose)."
                    }
                  }
                },
                "examples": {
                  "status": {
                    "value": {
                      "success": false,
                      "error": "Invalid status",
                      "allowed": [
                        "pending",
                        "completed",
                        "canceled"
                      ]
                    }
                  },
                  "supplier": {
                    "value": {
                      "success": false,
                      "error": "Invalid supplierId"
                    }
                  },
                  "date": {
                    "value": {
                      "success": false,
                      "error": "Invalid dateFrom"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "description": "Permiso insuficiente (INSUFFICIENT_API_PERMISSIONS) o plan sin el módulo de facturación electrónica (PLAN_MODULE_REQUIRED).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "examples": {
                  "permission": {
                    "value": {
                      "message": "Permiso insuficiente. Se requiere: purchases:read",
                      "code": "INSUFFICIENT_API_PERMISSIONS",
                      "requiredPermission": "purchases:read",
                      "availablePermissions": []
                    }
                  },
                  "module": {
                    "value": {
                      "message": "La facturación electrónica es un módulo adicional de tu plan. Contáctanos para activarlo.",
                      "code": "PLAN_MODULE_REQUIRED",
                      "module": "invoices"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Error interno del servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1purchases/get/responses/400/content/application~1json/schema"
                },
                "example": {
                  "success": false,
                  "error": "Internal server error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createPurchase",
        "tags": [
          "Purchases"
        ],
        "summary": "Registrar una orden de compra",
        "description": "Requiere purchases:write o * y el módulo de facturación en el plan. NO aplica el límite mensual del plan: ese tope cuenta facturas emitidas ante la DIAN, y registrar una compra no emite ningún documento fiscal. Con kind=invoice (default) orderNumber (el número de la factura del proveedor), total y paymentMethod son obligatorios. Con kind=order el servidor reserva el consecutivo OC-n (settings.purchases.numberPrefix + contador atómico) e IGNORA el orderNumber del body; solo total y paymentMethod son obligatorios. Una factura puede cubrir una OC abierta enviando orderRef: se valida que sea una OC pendiente, sin factura y de la misma organización (si no, 409) y al guardar la OC pasa a completed con invoiceRef. kind inválido → 400. El proveedor se valida contra la organización del token: un supplierId de otra organización o inexistente responde 400 (la ruta interna solo comprueba que sea un ObjectId, con lo que guardaría la referencia ajena). Los ítems se normalizan: tax ausente = 0 y total ausente = quantity × unitPrice. La compra queda a nombre del usuario dueño del token.",
        "x-required-permissions": [
          "purchases:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "total y paymentMethod son obligatorios; orderNumber también cuando kind=invoice (cadena vacía cuenta como ausente y responde 400 con la lista missing). Con kind=order el servidor reserva el consecutivo OC-n e ignora orderNumber. El proveedor puede ir como supplierId (contacto de la MISMA organización; uno ajeno o inexistente responde 400) o solo como supplierName + supplierNIT. La compra queda a nombre del usuario dueño del API Token.",
                "required": [
                  "total",
                  "paymentMethod"
                ],
                "properties": {
                  "kind": {
                    "$ref": "#/paths/~1purchases/get/parameters/2/schema"
                  },
                  "orderNumber": {
                    "type": "string",
                    "minLength": 1,
                    "description": "Obligatorio si kind=invoice; ignorado si kind=order."
                  },
                  "orderRef": {
                    "type": "string",
                    "description": "Solo con kind=invoice: id de la OC abierta que esta factura cubre. Debe ser una OC (kind=order) pendiente, sin factura y de la misma organización; si no, 409. Al guardar, la OC pasa a completed con invoiceRef. Una OC ↔ una factura."
                  },
                  "total": {
                    "type": [
                      "number",
                      "string"
                    ]
                  },
                  "paymentMethod": {
                    "type": "string",
                    "minLength": 1
                  },
                  "tax": {
                    "type": [
                      "number",
                      "string"
                    ],
                    "description": "Por defecto 0."
                  },
                  "discounts": {
                    "type": [
                      "number",
                      "string"
                    ]
                  },
                  "supplierId": {
                    "type": "string",
                    "description": "ObjectId de un contacto de la organización."
                  },
                  "supplierName": {
                    "type": "string"
                  },
                  "supplierNIT": {
                    "type": "string"
                  },
                  "purchaseDate": {
                    "type": "string",
                    "format": "date",
                    "description": "Por defecto la fecha de creación."
                  },
                  "description": {
                    "type": "string"
                  },
                  "voucherType": {
                    "type": "string",
                    "enum": [
                      "Factura electrónica",
                      "Nota de crédito electrónica",
                      "Documento soporte con no obligados",
                      "Application response",
                      "Otro"
                    ],
                    "description": "Tipo de comprobante que respalda la compra. Enum cerrado; otro valor responde 400."
                  },
                  "status": {
                    "$ref": "#/paths/~1purchases/get/parameters/3/schema"
                  },
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "description": "Línea de la compra. name, quantity y unitPrice son obligatorios. tax ausente se toma como 0 y total ausente se calcula como quantity × unitPrice (el integrador no repite la aritmética). Un valor no numérico responde 400 indicando el índice del ítem.",
                      "required": [
                        "name",
                        "quantity",
                        "unitPrice"
                      ],
                      "properties": {
                        "name": {
                          "type": "string",
                          "minLength": 1
                        },
                        "description": {
                          "type": "string"
                        },
                        "quantity": {
                          "type": "number"
                        },
                        "unitPrice": {
                          "type": "number"
                        },
                        "tax": {
                          "type": "number"
                        },
                        "total": {
                          "type": "number"
                        }
                      }
                    }
                  },
                  "applyWithholdings": {
                    "type": "boolean",
                    "description": "true = el servidor calcula las retenciones con la tabla del año de la compra. Si no hay tabla, o le falta la UVT, responde 400 explicando qué falta en vez de guardar un cero que parecería \"esta compra no retiene\"."
                  },
                  "withholdingConcept": {
                    "type": "string",
                    "description": "Código del concepto a aplicar; si se omite gana el del proveedor y luego el marcado por defecto."
                  },
                  "withholdings": {
                    "description": "Montos explícitos. El servidor recalcula y **decide él** qué renglón queda como `manual` (el que difiera de lo calculado): la bandera que mande el cliente se ignora. `null` quita las retenciones.",
                    "oneOf": [
                      {
                        "type": "null"
                      },
                      {
                        "type": "object",
                        "properties": {
                          "conceptCode": {
                            "type": "string"
                          },
                          "notes": {
                            "type": "string"
                          },
                          "lines": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "required": [
                                "kind",
                                "amount"
                              ],
                              "properties": {
                                "kind": {
                                  "type": "string",
                                  "enum": [
                                    "rteFuente",
                                    "rteIva",
                                    "rteIca"
                                  ]
                                },
                                "amount": {
                                  "type": "number",
                                  "minimum": 0
                                },
                                "base": {
                                  "type": "number"
                                },
                                "rate": {
                                  "type": "number"
                                },
                                "rateType": {
                                  "type": "string",
                                  "enum": [
                                    "percent",
                                    "perMil"
                                  ]
                                }
                              }
                            }
                          }
                        }
                      }
                    ]
                  }
                }
              },
              "examples": {
                "full": {
                  "summary": "Compra completa con proveedor vinculado a un contacto.",
                  "value": {
                    "orderNumber": "OC-2026-014",
                    "supplierId": "507f1f77bcf86cd799439011",
                    "supplierName": "Papelería Central SAS",
                    "supplierNIT": "901234567",
                    "purchaseDate": "2026-09-10",
                    "tax": 190000,
                    "total": 1190000,
                    "description": "Insumos de oficina del trimestre",
                    "paymentMethod": "Transferencia",
                    "voucherType": "Factura electrónica",
                    "status": "completed",
                    "items": [
                      {
                        "name": "Resma carta",
                        "quantity": 50,
                        "unitPrice": 20000,
                        "tax": 19,
                        "total": 1000000
                      }
                    ]
                  }
                },
                "minimal": {
                  "summary": "Solo los obligatorios y un ítem sin total (se calcula).",
                  "value": {
                    "orderNumber": "OC-2026-015",
                    "total": 350000,
                    "paymentMethod": "Efectivo",
                    "supplierName": "Transportes del Valle",
                    "supplierNIT": "800111222",
                    "items": [
                      {
                        "name": "Flete Medellín - Bogotá",
                        "quantity": 1,
                        "unitPrice": 350000
                      }
                    ]
                  }
                },
                "order": {
                  "summary": "Orden de compra; el número lo asigna el servidor (OC-n).",
                  "value": {
                    "kind": "order",
                    "total": 1190000,
                    "tax": 190000,
                    "paymentMethod": "Transferencia",
                    "supplierName": "Papelería Central SAS",
                    "supplierNIT": "901234567",
                    "items": [
                      {
                        "name": "Resma carta",
                        "quantity": 50,
                        "unitPrice": 20000
                      }
                    ]
                  }
                },
                "linked": {
                  "summary": "Factura del proveedor que cubre una OC abierta.",
                  "value": {
                    "orderNumber": "FV-8821",
                    "orderRef": "507f1f77bcf86cd799439201",
                    "total": 1190000,
                    "tax": 190000,
                    "paymentMethod": "Transferencia",
                    "supplierName": "Papelería Central SAS",
                    "supplierNIT": "901234567",
                    "voucherType": "Factura electrónica",
                    "status": "completed"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Orden de compra registrada.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "message",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "message": {
                      "type": "string",
                      "const": "Purchase order created successfully"
                    },
                    "data": {
                      "type": "object",
                      "description": "Representación pública de la orden de compra. No expone organizationId ni userId. Los campos de texto opcionales vuelven como ''; supplierId, supplier, voucherType y discounts vuelven como null cuando no tienen valor.",
                      "required": [
                        "id",
                        "kind",
                        "orderNumber",
                        "orderRef",
                        "invoiceRef",
                        "supplierId",
                        "supplier",
                        "supplierName",
                        "supplierNIT",
                        "purchaseDate",
                        "tax",
                        "total",
                        "discounts",
                        "description",
                        "paymentMethod",
                        "voucherType",
                        "status",
                        "items",
                        "withholdings",
                        "withholdingTotal",
                        "netPayable"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "kind": {
                          "$ref": "#/paths/~1purchases/get/parameters/2/schema"
                        },
                        "orderNumber": {
                          "type": "string",
                          "description": "Factura: el número del proveedor, lo pone quien registra. OC: consecutivo OC-n asignado por el servidor."
                        },
                        "orderRef": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "En una factura",
                          "id de la OC que cubre; null si no cubre ninguna.": null
                        },
                        "invoiceRef": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "En una OC",
                          "id de la factura que la cerró; null mientras esté abierta.": null
                        },
                        "supplierId": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "ID del contacto proveedor; null si la compra solo tiene nombre/NIT."
                        },
                        "supplier": {
                          "type": [
                            "object",
                            "null"
                          ],
                          "description": "Proveedor resuelto desde el contacto referenciado por supplierId, con sus properties aplanadas (gana la primaria; si ninguna lo es, la primera). null si la compra no tiene supplierId o si el contacto ya no existe.",
                          "properties": {
                            "id": {
                              "type": "string"
                            },
                            "fullName": {
                              "type": "string",
                              "description": "firstName + lastName del contacto."
                            },
                            "company": {
                              "type": "string",
                              "description": "'' si no tiene."
                            },
                            "taxId": {
                              "type": "string",
                              "description": "'' si no tiene."
                            }
                          },
                          "additionalProperties": true
                        },
                        "supplierName": {
                          "type": "string",
                          "description": "'' si no tiene."
                        },
                        "supplierNIT": {
                          "type": "string",
                          "description": "'' si no tiene."
                        },
                        "purchaseDate": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "tax": {
                          "type": "number"
                        },
                        "total": {
                          "type": "number"
                        },
                        "discounts": {
                          "type": [
                            "number",
                            "null"
                          ],
                          "description": "Campo heredado; la app no lo usa."
                        },
                        "description": {
                          "type": "string",
                          "description": "'' si no tiene."
                        },
                        "paymentMethod": {
                          "type": "string",
                          "description": "Texto libre; no se valida contra un catálogo."
                        },
                        "voucherType": {
                          "oneOf": [
                            {
                              "$ref": "#/paths/~1purchases/post/requestBody/content/application~1json/schema/properties/voucherType"
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "status": {
                          "$ref": "#/paths/~1purchases/get/parameters/3/schema"
                        },
                        "items": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "description": "Línea de la orden de compra tal como quedó guardada.",
                            "required": [
                              "id",
                              "name",
                              "description",
                              "quantity",
                              "unitPrice",
                              "tax",
                              "total"
                            ],
                            "properties": {
                              "id": {
                                "type": "string",
                                "description": "ID del subdocumento; lo asigna Mongo."
                              },
                              "name": {
                                "type": "string"
                              },
                              "description": {
                                "type": "string",
                                "description": "'' si no tiene."
                              },
                              "quantity": {
                                "type": "number"
                              },
                              "unitPrice": {
                                "type": "number"
                              },
                              "tax": {
                                "type": "number",
                                "description": "0 si no se envió."
                              },
                              "total": {
                                "type": "number",
                                "description": "quantity × unitPrice si no se envió."
                              }
                            }
                          },
                          "description": "[] si la compra se registró sin líneas."
                        },
                        "withholdings": {
                          "type": [
                            "object",
                            "null"
                          ],
                          "description": "Retenciones que la organización le practicó al proveedor por esta compra. `null` significa que la compra NO se ha evaluado todavía — distinto de `status: exempt`, que es \"se evaluó y no había que retener\".",
                          "required": [
                            "status",
                            "lines"
                          ],
                          "properties": {
                            "status": {
                              "type": "string",
                              "enum": [
                                "applied",
                                "exempt"
                              ]
                            },
                            "conceptCode": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "conceptLabel": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "source": {
                              "type": "string",
                              "enum": [
                                "auto",
                                "manual",
                                "mixed"
                              ],
                              "description": "mixed = algunos renglones se corrigieron a mano y otros no."
                            },
                            "configYear": {
                              "type": [
                                "integer",
                                "null"
                              ],
                              "description": "Año de la tabla de tarifas usada (sale de la fecha de la compra, no del reloj)."
                            },
                            "uvtValue": {
                              "type": [
                                "number",
                                "null"
                              ],
                              "description": "UVT congelada, para poder reproducir el cálculo años después."
                            },
                            "appliedAt": {
                              "type": [
                                "string",
                                "null"
                              ],
                              "format": "date-time"
                            },
                            "lines": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "description": "Un renglón de retención. Se conservan también los renglones en CERO con su `skippedReason`: poder explicar por qué NO se retuvo (la base no alcanzó el mínimo en UVT, el proveedor es autorretenedor, la organización no practica ese tipo) es parte de lo que el proveedor puede reclamar.",
                                "required": [
                                  "kind",
                                  "base",
                                  "rate",
                                  "amount",
                                  "manual"
                                ],
                                "properties": {
                                  "kind": {
                                    "type": "string",
                                    "enum": [
                                      "rteFuente",
                                      "rteIva",
                                      "rteIca"
                                    ],
                                    "description": "21 ReteFuente, 22 ReteIVA y 23 ReteICA del catálogo DIAN."
                                  },
                                  "conceptCode": {
                                    "type": [
                                      "string",
                                      "null"
                                    ]
                                  },
                                  "conceptLabel": {
                                    "type": [
                                      "string",
                                      "null"
                                    ],
                                    "description": "Rótulo CONGELADO al aplicar; renombrar el concepto no altera una retención vieja."
                                  },
                                  "base": {
                                    "type": "number",
                                    "description": "Sobre qué se aplicó la tarifa. ReteFuente e ICA van sobre el subtotal (total − tax); **ReteIVA va sobre el IVA**."
                                  },
                                  "rate": {
                                    "type": "number"
                                  },
                                  "rateType": {
                                    "type": "string",
                                    "enum": [
                                      "percent",
                                      "perMil"
                                    ],
                                    "default": "percent",
                                    "description": "ICA suele expresarse por mil."
                                  },
                                  "amount": {
                                    "type": "number"
                                  },
                                  "manual": {
                                    "type": "boolean",
                                    "description": "Lo decide el SERVIDOR comparando el monto recibido contra el calculado; lo que mande el cliente en este campo se ignora. Un renglón manual no lo pisa el recálculo en lote."
                                  },
                                  "skippedReason": {
                                    "type": [
                                      "string",
                                      "null"
                                    ],
                                    "description": "BELOW_MIN_UVT, SELF_WITHHOLDER, NOT_AGENT, NOT_RESPONSIBLE_IVA, ICA_EXEMPT, NO_RATE, NO_BASE."
                                  }
                                }
                              }
                            }
                          }
                        },
                        "withholdingTotal": {
                          "type": "number",
                          "description": "Suma de lo retenido. **Derivado, no persistido** (misma regla que el saldo de cartera)."
                        },
                        "withheldByKind": {
                          "type": "object",
                          "description": "Lo retenido abierto por tipo; las tres claves siempre presentes.",
                          "properties": {
                            "rteFuente": {
                              "type": "number"
                            },
                            "rteIva": {
                              "type": "number"
                            },
                            "rteIca": {
                              "type": "number"
                            }
                          }
                        },
                        "netPayable": {
                          "type": "number",
                          "description": "Lo que de verdad se le gira al proveedor: `total − withholdingTotal`. Sin retenciones evaluadas es igual al total."
                        },
                        "lastModified": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "createdAt": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updatedAt": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "created": {
                    "summary": "Respuesta al registrar una compra sin proveedor vinculado y con el total del ítem calculado.",
                    "value": {
                      "success": true,
                      "message": "Purchase order created successfully",
                      "data": {
                        "id": "507f1f77bcf86cd799439102",
                        "orderNumber": "OC-2026-015",
                        "kind": "invoice",
                        "orderRef": null,
                        "invoiceRef": null,
                        "supplierId": null,
                        "supplier": null,
                        "supplierName": "Transportes del Valle",
                        "supplierNIT": "800111222",
                        "purchaseDate": "2026-09-12T00:00:00.000Z",
                        "tax": 0,
                        "total": 350000,
                        "discounts": null,
                        "description": "",
                        "paymentMethod": "Efectivo",
                        "voucherType": null,
                        "status": "pending",
                        "items": [
                          {
                            "id": "507f1f77bcf86cd799439122",
                            "name": "Flete Medellín - Bogotá",
                            "description": "",
                            "quantity": 1,
                            "unitPrice": 350000,
                            "tax": 0,
                            "total": 350000
                          }
                        ],
                        "withholdings": null,
                        "withholdingTotal": 0,
                        "withheldByKind": {
                          "rteFuente": 0,
                          "rteIva": 0,
                          "rteIca": 0
                        },
                        "netPayable": 350000,
                        "lastModified": "2026-09-12T09:30:00.000Z",
                        "createdAt": "2026-09-12T09:30:00.000Z",
                        "updatedAt": "2026-09-12T09:30:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Faltan obligatorios, montos no numéricos, status/voucherType fuera del enum, purchaseDate no parseable, ítems mal formados, proveedor ajeno a la organización, o el modelo rechazó el documento.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1purchases/get/responses/400/content/application~1json/schema"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "success": false,
                      "error": "Missing required fields",
                      "required": [
                        "orderNumber",
                        "total",
                        "paymentMethod"
                      ],
                      "missing": [
                        "paymentMethod"
                      ]
                    }
                  },
                  "total": {
                    "value": {
                      "success": false,
                      "error": "'total' must be a valid number"
                    }
                  },
                  "voucher": {
                    "value": {
                      "success": false,
                      "error": "Invalid voucherType",
                      "allowed": [
                        "Factura electrónica",
                        "Nota de crédito electrónica",
                        "Documento soporte con no obligados",
                        "Application response",
                        "Otro"
                      ]
                    }
                  },
                  "item": {
                    "value": {
                      "success": false,
                      "error": "items[0].quantity must be a number"
                    }
                  },
                  "supplier": {
                    "value": {
                      "success": false,
                      "error": "Supplier contact not found in this organization"
                    }
                  },
                  "kind": {
                    "value": {
                      "success": false,
                      "error": "Invalid kind",
                      "allowed": [
                        "order",
                        "invoice"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1purchases/get/responses/403"
          },
          "409": {
            "description": "La OC de orderRef no está abierta (ya facturada, cancelada, no es una OC o no existe en la organización).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1purchases/get/responses/400/content/application~1json/schema"
                },
                "example": {
                  "success": false,
                  "error": "La OC OC-12 ya está facturada"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1purchases/get/responses/500"
          }
        }
      }
    },
    "/purchases/open-orders": {
      "get": {
        "operationId": "listOpenPurchaseOrders",
        "tags": [
          "Purchases"
        ],
        "summary": "Órdenes de compra abiertas de un proveedor",
        "description": "Requiere purchases:read o * y el módulo de facturación en el plan. Devuelve las OC (kind=order) pendientes y sin factura enlazada de la organización del token. Es la sugerencia para conciliar: antes de registrar la factura de un proveedor, buscar si ya había una OC que la cubra y pasar su id como orderRef. El NIT se compara normalizado en memoria (900.480.268-1 = 900480268: en la base conviven las dos formas). Con total, la lista viene ordenada por cercanía y cada OC trae totalDiff; sin total, por purchaseDate descendente y totalDiff null. Sin nit devuelve todas las abiertas. No pagina: son pocas por definición.",
        "x-required-permissions": [
          "purchases:read"
        ],
        "parameters": [
          {
            "name": "nit",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "NIT del proveedor, en cualquier formato. Vacío = todas las OC abiertas.",
            "example": "900.480.268-1"
          },
          {
            "name": "total",
            "in": "query",
            "schema": {
              "type": "number"
            },
            "description": "Total de la factura, para ordenar por cercanía y calcular totalDiff. No numérico → 400.",
            "example": 1190000
          }
        ],
        "responses": {
          "200": {
            "description": "OC abiertas; data puede estar vacío.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "description": "Ordenadas por totalDiff ascendente (con total) o por purchaseDate descendente.",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/paths/~1purchases/post/responses/201/content/application~1json/schema/properties/data"
                          },
                          {
                            "type": "object",
                            "required": [
                              "totalDiff"
                            ],
                            "properties": {
                              "totalDiff": {
                                "type": [
                                  "number",
                                  "null"
                                ],
                                "description": "|OC.total − total pedido|. null si no se envió total."
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                },
                "examples": {
                  "open": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "507f1f77bcf86cd799439201",
                          "kind": "order",
                          "orderNumber": "OC-12",
                          "orderRef": null,
                          "invoiceRef": null,
                          "supplierId": null,
                          "supplier": null,
                          "supplierName": "Papelería Central SAS",
                          "supplierNIT": "901234567",
                          "purchaseDate": "2026-09-20T00:00:00.000Z",
                          "tax": 190000,
                          "total": 1190000,
                          "discounts": null,
                          "description": "Insumos del trimestre",
                          "paymentMethod": "Transferencia",
                          "voucherType": null,
                          "status": "pending",
                          "items": [],
                          "withholdings": null,
                          "withholdingTotal": 0,
                          "withheldByKind": {
                            "rteFuente": 0,
                            "rteIva": 0,
                            "rteIca": 0
                          },
                          "netPayable": 1190000,
                          "totalDiff": 0,
                          "lastModified": "2026-09-20T09:30:00.000Z",
                          "createdAt": "2026-09-20T09:30:00.000Z",
                          "updatedAt": "2026-09-20T09:30:00.000Z"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "total no numérico.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1purchases/get/responses/400/content/application~1json/schema"
                },
                "example": {
                  "success": false,
                  "error": "'total' must be a valid number"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1purchases/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1purchases/get/responses/500"
          }
        }
      }
    },
    "/purchases/withholdings/config": {
      "get": {
        "operationId": "getPurchaseWithholdingConfig",
        "tags": [
          "Purchases"
        ],
        "summary": "Consultar la tabla de retenciones de un año",
        "description": "Requiere purchases:read o * y el módulo de facturación en el plan. Devuelve las tarifas con las que el CRM calcula las retenciones de las compras de ese año. Responde **404 y no una tabla vacía** cuando el año no está configurado: el motor tiene que poder distinguir \"no hay configuración\" de \"hay una que no retiene nada\", porque un cero silencioso terminaría en un certificado falso.",
        "x-required-permissions": [
          "purchases:read"
        ],
        "parameters": [
          {
            "name": "year",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 2000,
              "maximum": 2100
            },
            "description": "Por defecto el año en curso. La configuración es POR AÑO porque la UVT y las tarifas cambian."
          }
        ],
        "responses": {
          "200": {
            "description": "Tabla del año.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "year",
                        "draft",
                        "uvt",
                        "agent",
                        "concepts",
                        "suppliers"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "year": {
                          "type": "integer"
                        },
                        "draft": {
                          "type": "boolean",
                          "description": "Mientras sea true el script de aplicación en lote se niega a escribir."
                        },
                        "uvt": {
                          "type": "number",
                          "description": "Valor de la UVT del año. **0 = sin configurar**: el motor devuelve \"no se pudo calcular\" en vez de un cero que parecería \"esta compra no retiene\"."
                        },
                        "agent": {
                          "type": "object",
                          "description": "Qué retenciones practica esta organización.",
                          "properties": {
                            "rteFuente": {
                              "type": "boolean"
                            },
                            "rteIva": {
                              "type": "boolean"
                            },
                            "rteIca": {
                              "type": "boolean"
                            }
                          }
                        },
                        "city": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "concepts": {
                          "type": "array",
                          "items": {
                            "$ref": "#/paths/~1purchases~1withholdings~1config/put/requestBody/content/application~1json/schema/properties/concepts/items"
                          }
                        },
                        "suppliers": {
                          "type": "array",
                          "items": {
                            "$ref": "#/paths/~1purchases~1withholdings~1config/put/requestBody/content/application~1json/schema/properties/suppliers/items"
                          }
                        },
                        "signer": {
                          "type": [
                            "object",
                            "null"
                          ],
                          "description": "Quien firma el certificado de retención.",
                          "properties": {
                            "name": {
                              "type": "string"
                            },
                            "idNumber": {
                              "type": "string"
                            },
                            "role": {
                              "type": "string"
                            }
                          }
                        },
                        "updatedAt": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "config": {
                    "summary": "Tabla de 2026 ya aprobada, con una excepción por proveedor.",
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439201",
                        "year": 2026,
                        "draft": false,
                        "uvt": 52374,
                        "agent": {
                          "rteFuente": true,
                          "rteIva": false,
                          "rteIca": true
                        },
                        "city": "Bogotá D.C.",
                        "concepts": [
                          {
                            "code": "compras-generales",
                            "label": "Compras generales",
                            "isDefault": true,
                            "rteFuente": {
                              "pct": 2.5,
                              "minUvt": 27
                            },
                            "rteIva": {
                              "pct": 15,
                              "minUvt": 27
                            }
                          }
                        ],
                        "suppliers": [
                          {
                            "nit": "900480268",
                            "name": "MATIPOS S.A.S",
                            "conceptCode": "compras-generales",
                            "icaExempt": true
                          }
                        ],
                        "signer": null,
                        "updatedAt": "2026-09-17T16:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Año inválido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1purchases/get/responses/400/content/application~1json/schema"
                },
                "example": {
                  "success": false,
                  "error": "Invalid year"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1purchases/get/responses/403"
          },
          "404": {
            "description": "El año no tiene configuración.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1purchases/get/responses/400/content/application~1json/schema"
                },
                "example": {
                  "success": false,
                  "error": "No withholding configuration for 2026",
                  "code": "WITHHOLDING_CONFIG_NOT_FOUND"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1purchases/get/responses/500"
          }
        }
      },
      "put": {
        "operationId": "upsertPurchaseWithholdingConfig",
        "tags": [
          "Purchases"
        ],
        "summary": "Crear o actualizar la tabla de retenciones de un año",
        "description": "Requiere purchases:write o *. Actualización parcial: solo se tocan las claves enviadas. Si el año no existe se crea en **borrador** (`draft: true`) con los conceptos sugeridos y la **UVT en 0** — el motor se niega a calcular hasta que alguien cargue la UVT real, en vez de devolver ceros. Mientras `draft` sea true el script de aplicación en lote tampoco escribe. Los NIT de `suppliers[]` se guardan **normalizados** (sin puntos, espacios ni dígito de verificación), así que la regla se encuentra venga el NIT como venga.",
        "x-required-permissions": [
          "purchases:write"
        ],
        "parameters": [
          {
            "name": "year",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 2000,
              "maximum": 2100
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Actualización parcial; solo se tocan las claves enviadas.",
                "properties": {
                  "uvt": {
                    "type": "number",
                    "minimum": 0
                  },
                  "draft": {
                    "type": "boolean"
                  },
                  "city": {
                    "type": "string"
                  },
                  "agent": {
                    "type": "object",
                    "properties": {
                      "rteFuente": {
                        "type": "boolean"
                      },
                      "rteIva": {
                        "type": "boolean"
                      },
                      "rteIca": {
                        "type": "boolean"
                      }
                    }
                  },
                  "concepts": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "code",
                        "label"
                      ],
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "label": {
                          "type": "string",
                          "description": "Se congela en la retención al aplicarla."
                        },
                        "isDefault": {
                          "type": "boolean",
                          "description": "Solo uno puede serlo."
                        },
                        "rteFuente": {
                          "$ref": "#/paths/~1purchases~1withholdings~1config/put/requestBody/content/application~1json/schema/properties/concepts/items/properties/rteIva"
                        },
                        "rteIva": {
                          "type": "object",
                          "description": "Tarifa de un tipo de retención. `pct` para porcentaje, `perMil` para ICA.",
                          "properties": {
                            "pct": {
                              "type": "number",
                              "minimum": 0,
                              "maximum": 100
                            },
                            "perMil": {
                              "type": "number",
                              "minimum": 0,
                              "description": "Por mil (6.9 = 6",
                              "9 × 1000).": null
                            },
                            "schedule": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "description": "Un tramo de vigencia dentro del año. Existe porque **la ley cambia a mitad de año**: en 2026 la base mínima de ReteFuente por compras fue 10 UVT hasta el 7 de mayo (Decreto 572 de 2025), 27 UVT del 8 de mayo al 30 de junio (suspensión del Consejo de Estado) y 10 UVT otra vez desde el 1 de julio. Lo practicado bajo la norma vigente en cada momento es válido y no se corrige hacia atrás.",
                                "required": [
                                  "from"
                                ],
                                "properties": {
                                  "from": {
                                    "type": "string",
                                    "format": "date",
                                    "description": "Desde cuándo rige",
                                    "inclusive.": null
                                  },
                                  "pct": {
                                    "type": "number",
                                    "minimum": 0,
                                    "maximum": 100
                                  },
                                  "perMil": {
                                    "type": "number",
                                    "minimum": 0
                                  },
                                  "minUvt": {
                                    "type": "number",
                                    "minimum": 0
                                  },
                                  "note": {
                                    "type": "string",
                                    "description": "Para dejar escrito por qué cambió: decreto, auto, resolución."
                                  }
                                }
                              },
                              "description": "Tramos de vigencia. Gana el de `from` más reciente que sea <= **la fecha de la compra** (no la de hoy); lo que el tramo no diga se hereda de la tarifa de arriba."
                            },
                            "minUvt": {
                              "type": "number",
                              "minimum": 0,
                              "description": "Base mínima en UVT. 0 = sin mínimo. Se evalúa POR RENGLÓN: en muchos municipios el ICA no tiene mínimo pero la ReteFuente sí, así que una compra puede generar ICA y no ReteFuente."
                            }
                          }
                        },
                        "rteIca": {
                          "$ref": "#/paths/~1purchases~1withholdings~1config/put/requestBody/content/application~1json/schema/properties/concepts/items/properties/rteIva"
                        }
                      }
                    }
                  },
                  "suppliers": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "description": "Excepciones por proveedor. Se indexa por NIT porque la mayoría de las compras no tiene contacto ligado.",
                      "required": [
                        "nit"
                      ],
                      "properties": {
                        "nit": {
                          "type": "string",
                          "description": "Se guarda normalizado (sin puntos",
                          "espacios ni DV).": null
                        },
                        "name": {
                          "type": "string"
                        },
                        "conceptCode": {
                          "type": "string"
                        },
                        "selfWithholder": {
                          "type": "boolean",
                          "description": "Autorretenedor de renta (código 15 del RUT) → no se le practica ReteFuente."
                        },
                        "simpleRegime": {
                          "type": "boolean",
                          "description": "Régimen simple → no se le practica ReteFuente."
                        },
                        "notResponsibleIva": {
                          "type": "boolean",
                          "description": "No responsable de IVA → no hay IVA que retener."
                        },
                        "icaExempt": {
                          "type": "boolean",
                          "description": "Típico cuando el proveedor está en otro municipio: el ICA es territorial."
                        },
                        "overrides": {
                          "type": "object",
                          "properties": {
                            "rteFuentePct": {
                              "type": "number"
                            },
                            "rteIvaPct": {
                              "type": "number"
                            },
                            "icaPerMil": {
                              "type": "number"
                            }
                          }
                        }
                      }
                    }
                  },
                  "signer": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "idNumber": {
                        "type": "string"
                      },
                      "role": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "examples": {
                "tarifas": {
                  "summary": "Cargar la UVT del año y la tarifa de compras.",
                  "value": {
                    "uvt": 52374,
                    "agent": {
                      "rteFuente": true,
                      "rteIva": false,
                      "rteIca": true
                    },
                    "city": "Bogotá D.C.",
                    "concepts": [
                      {
                        "code": "compras-generales",
                        "label": "Compras generales",
                        "isDefault": true,
                        "rteFuente": {
                          "pct": 2.5,
                          "minUvt": 27
                        }
                      }
                    ]
                  }
                },
                "aprobar": {
                  "summary": "Sacar la tabla de borrador para poder aplicarla.",
                  "value": {
                    "draft": false
                  }
                },
                "proveedor": {
                  "summary": "Excepción por proveedor (autorretenedor, o fuera del municipio).",
                  "value": {
                    "suppliers": [
                      {
                        "nit": "900.480.268-1",
                        "name": "MATIPOS S.A.S",
                        "conceptCode": "compras-generales",
                        "icaExempt": true
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tabla guardada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1purchases~1withholdings~1config/get/responses/200/content/application~1json/schema"
                },
                "examples": {
                  "config": {
                    "$ref": "#/paths/~1purchases~1withholdings~1config/get/responses/200/content/application~1json/examples/config"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Tarifa, año, concepto duplicado o NIT faltante inválidos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1purchases/get/responses/400/content/application~1json/schema"
                },
                "examples": {
                  "uvt": {
                    "value": {
                      "success": false,
                      "error": "'uvt' must be a number >= 0"
                    }
                  },
                  "concepto": {
                    "value": {
                      "success": false,
                      "error": "Duplicate concept code: compras-generales"
                    }
                  },
                  "tarifa": {
                    "value": {
                      "success": false,
                      "error": "concepts[0].rteFuente.pct must be <= 100"
                    }
                  },
                  "nit": {
                    "value": {
                      "success": false,
                      "error": "suppliers[1].nit is required"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1purchases/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1purchases/get/responses/500"
          }
        }
      }
    },
    "/purchases/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB de la orden de compra (campo id de las respuestas).",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "507f1f77bcf86cd799439101"
        }
      ],
      "get": {
        "operationId": "getPurchaseById",
        "tags": [
          "Purchases"
        ],
        "summary": "Consultar una orden de compra por ID",
        "description": "Requiere purchases:read o * y el módulo de facturación en el plan. La orden debe pertenecer a la organización del token.",
        "x-required-permissions": [
          "purchases:read"
        ],
        "responses": {
          "200": {
            "description": "Orden de compra encontrada.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "$ref": "#/paths/~1purchases/post/responses/201/content/application~1json/schema/properties/data"
                    }
                  }
                },
                "examples": {
                  "found": {
                    "summary": "Orden de compra con proveedor vinculado a un contacto.",
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439101",
                        "orderNumber": "OC-2026-014",
                        "kind": "invoice",
                        "orderRef": null,
                        "invoiceRef": null,
                        "supplierId": "507f1f77bcf86cd799439011",
                        "supplier": {
                          "id": "507f1f77bcf86cd799439011",
                          "fullName": "Papelería Central",
                          "company": "Papelería Central SAS",
                          "taxId": "901234567"
                        },
                        "supplierName": "Papelería Central SAS",
                        "supplierNIT": "901234567",
                        "purchaseDate": "2026-09-10T00:00:00.000Z",
                        "tax": 190000,
                        "total": 1190000,
                        "discounts": null,
                        "description": "Insumos de oficina del trimestre",
                        "paymentMethod": "Transferencia",
                        "voucherType": "Factura electrónica",
                        "status": "completed",
                        "items": [
                          {
                            "id": "507f1f77bcf86cd799439121",
                            "name": "Resma carta",
                            "description": "",
                            "quantity": 50,
                            "unitPrice": 20000,
                            "tax": 19,
                            "total": 1000000
                          }
                        ],
                        "withholdings": {
                          "status": "applied",
                          "conceptCode": "compras-generales",
                          "conceptLabel": "Compras generales",
                          "source": "auto",
                          "configYear": 2026,
                          "uvtValue": 50000,
                          "appliedAt": "2026-09-17T16:00:00.000Z",
                          "lines": [
                            {
                              "kind": "rteFuente",
                              "conceptCode": "compras-generales",
                              "conceptLabel": "Compras generales",
                              "base": 1000000,
                              "rate": 2.5,
                              "rateType": "percent",
                              "amount": 25000,
                              "manual": false,
                              "skippedReason": null
                            }
                          ]
                        },
                        "withholdingTotal": 25000,
                        "withheldByKind": {
                          "rteFuente": 25000,
                          "rteIva": 0,
                          "rteIca": 0
                        },
                        "netPayable": 1165000,
                        "lastModified": "2026-09-10T15:00:00.000Z",
                        "createdAt": "2026-09-10T15:00:00.000Z",
                        "updatedAt": "2026-09-10T15:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID de orden de compra inválido (no es un ObjectId).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1purchases/get/responses/400/content/application~1json/schema"
                },
                "example": {
                  "success": false,
                  "error": "Invalid purchase order ID"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1purchases/get/responses/403"
          },
          "404": {
            "description": "Orden de compra inexistente o no perteneciente a la organización del token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1purchases/get/responses/400/content/application~1json/schema"
                },
                "example": {
                  "success": false,
                  "error": "Purchase order not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1purchases/get/responses/500"
          }
        }
      },
      "put": {
        "operationId": "updatePurchaseById",
        "tags": [
          "Purchases"
        ],
        "summary": "Actualizar parcialmente una orden de compra",
        "description": "Requiere purchases:write o * y el módulo de facturación en el plan. Solo se tocan los campos enviados. Enviar items REEMPLAZA la lista completa (no fusiona líneas). supplierId null o cadena vacía desvincula al proveedor; un supplierId ajeno a la organización responde 400. Siempre actualiza lastModified. kind NO se cambia después de crear (un valor distinto al guardado → 400; el mismo es un no-op). En una OC el orderNumber es el consecutivo del servidor (distinto → 400). orderRef e invoiceRef no se editan por acá: los escribe el servidor al enlazar. Pasar a canceled una OC que ya tiene factura enlazada → 409.",
        "x-required-permissions": [
          "purchases:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Actualización parcial: solo se tocan los campos enviados. Enviar items REEMPLAZA la lista completa. supplierId null o '' desvincula al proveedor. Siempre actualiza lastModified. kind no se cambia (distinto al guardado → 400). orderRef e invoiceRef no se editan: los escribe el servidor.",
                "properties": {
                  "kind": {
                    "$ref": "#/paths/~1purchases/get/parameters/2/schema"
                  },
                  "orderNumber": {
                    "type": "string",
                    "description": "En una OC lo asigna el servidor; un valor distinto → 400."
                  },
                  "total": {
                    "type": [
                      "number",
                      "string"
                    ]
                  },
                  "tax": {
                    "type": [
                      "number",
                      "string"
                    ]
                  },
                  "discounts": {
                    "type": [
                      "number",
                      "string"
                    ]
                  },
                  "paymentMethod": {
                    "type": "string"
                  },
                  "supplierId": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "supplierName": {
                    "type": "string"
                  },
                  "supplierNIT": {
                    "type": "string"
                  },
                  "purchaseDate": {
                    "type": "string",
                    "format": "date"
                  },
                  "description": {
                    "type": "string"
                  },
                  "voucherType": {
                    "$ref": "#/paths/~1purchases/post/requestBody/content/application~1json/schema/properties/voucherType"
                  },
                  "status": {
                    "$ref": "#/paths/~1purchases/get/parameters/3/schema"
                  },
                  "items": {
                    "type": "array",
                    "items": {
                      "$ref": "#/paths/~1purchases/post/requestBody/content/application~1json/schema/properties/items/items"
                    }
                  },
                  "applyWithholdings": {
                    "type": "boolean",
                    "description": "true = el servidor calcula las retenciones con la tabla del año de la compra. Si no hay tabla, o le falta la UVT, responde 400 explicando qué falta en vez de guardar un cero que parecería \"esta compra no retiene\"."
                  },
                  "withholdingConcept": {
                    "type": "string",
                    "description": "Código del concepto a aplicar; si se omite gana el del proveedor y luego el marcado por defecto."
                  },
                  "withholdings": {
                    "description": "Montos explícitos. El servidor recalcula y **decide él** qué renglón queda como `manual` (el que difiera de lo calculado): la bandera que mande el cliente se ignora. `null` quita las retenciones.",
                    "oneOf": [
                      {
                        "type": "null"
                      },
                      {
                        "type": "object",
                        "properties": {
                          "conceptCode": {
                            "type": "string"
                          },
                          "notes": {
                            "type": "string"
                          },
                          "lines": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "required": [
                                "kind",
                                "amount"
                              ],
                              "properties": {
                                "kind": {
                                  "type": "string",
                                  "enum": [
                                    "rteFuente",
                                    "rteIva",
                                    "rteIca"
                                  ]
                                },
                                "amount": {
                                  "type": "number",
                                  "minimum": 0
                                },
                                "base": {
                                  "type": "number"
                                },
                                "rate": {
                                  "type": "number"
                                },
                                "rateType": {
                                  "type": "string",
                                  "enum": [
                                    "percent",
                                    "perMil"
                                  ]
                                }
                              }
                            }
                          }
                        }
                      }
                    ]
                  }
                }
              },
              "examples": {
                "status": {
                  "summary": "Marcar la compra como completada y anotar el comprobante.",
                  "value": {
                    "status": "completed",
                    "voucherType": "Otro",
                    "description": "Flete pagado en efectivo"
                  }
                },
                "unlink": {
                  "summary": "Desvincular el contacto proveedor dejando solo el nombre.",
                  "value": {
                    "supplierId": null,
                    "supplierName": "Transportes del Valle"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Orden de compra actualizada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1purchases~1{id}/get/responses/200/content/application~1json/schema"
                },
                "examples": {
                  "updated": {
                    "summary": "Tras PUT marcando la compra como completada.",
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439102",
                        "orderNumber": "OC-2026-015",
                        "kind": "invoice",
                        "orderRef": null,
                        "invoiceRef": null,
                        "supplierId": null,
                        "supplier": null,
                        "supplierName": "Transportes del Valle",
                        "supplierNIT": "800111222",
                        "purchaseDate": "2026-09-12T00:00:00.000Z",
                        "tax": 0,
                        "total": 350000,
                        "discounts": null,
                        "description": "Flete pagado en efectivo",
                        "paymentMethod": "Efectivo",
                        "voucherType": "Otro",
                        "status": "completed",
                        "items": [
                          {
                            "id": "507f1f77bcf86cd799439122",
                            "name": "Flete Medellín - Bogotá",
                            "description": "",
                            "quantity": 1,
                            "unitPrice": 350000,
                            "tax": 0,
                            "total": 350000
                          }
                        ],
                        "withholdings": {
                          "status": "exempt",
                          "conceptCode": "compras-generales",
                          "conceptLabel": "Compras generales",
                          "source": "auto",
                          "configYear": 2026,
                          "uvtValue": 50000,
                          "appliedAt": "2026-09-17T16:00:00.000Z",
                          "lines": [
                            {
                              "kind": "rteFuente",
                              "conceptCode": "compras-generales",
                              "conceptLabel": "Compras generales",
                              "base": 350000,
                              "rate": 0,
                              "rateType": "percent",
                              "amount": 0,
                              "manual": false,
                              "skippedReason": "BELOW_MIN_UVT"
                            }
                          ]
                        },
                        "withholdingTotal": 0,
                        "withheldByKind": {
                          "rteFuente": 0,
                          "rteIva": 0,
                          "rteIca": 0
                        },
                        "netPayable": 350000,
                        "lastModified": "2026-09-13T11:00:00.000Z",
                        "createdAt": "2026-09-12T09:30:00.000Z",
                        "updatedAt": "2026-09-13T11:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID inválido, montos no numéricos, enums o fecha inválidos, ítems mal formados o proveedor ajeno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1purchases/get/responses/400/content/application~1json/schema"
                },
                "examples": {
                  "id": {
                    "value": {
                      "success": false,
                      "error": "Invalid purchase order ID"
                    }
                  },
                  "status": {
                    "value": {
                      "success": false,
                      "error": "Invalid status",
                      "allowed": [
                        "pending",
                        "completed",
                        "canceled"
                      ]
                    }
                  },
                  "amount": {
                    "value": {
                      "success": false,
                      "error": "'tax' must be a valid number"
                    }
                  },
                  "kind": {
                    "value": {
                      "success": false,
                      "error": "'kind' cannot be changed after creation"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1purchases/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1purchases~1{id}/get/responses/404"
          },
          "409": {
            "description": "Se intentó cancelar una OC que ya tiene factura enlazada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1purchases/get/responses/400/content/application~1json/schema"
                },
                "example": {
                  "success": false,
                  "error": "La OC OC-12 ya está facturada y no se puede cancelar"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1purchases/get/responses/500"
          }
        }
      }
    },
    "/shipments": {
      "get": {
        "operationId": "listShipments",
        "tags": [
          "Shipments"
        ],
        "summary": "Listar envíos",
        "description": "Requiere shipments:read o * y el módulo envios en el plan (PLAN_MODULE_REQUIRED → 403 cuando su enforcement está activo). Lista los envíos de la organización del token, más recientes primero. contactId que no es ObjectId o status fuera del enum → 400 VALIDATION_ERROR (la respuesta trae allowed).",
        "x-required-permissions": [
          "shipments:read"
        ],
        "parameters": [
          {
            "name": "contactId",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Solo los envíos de ese contacto."
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "created",
                "picked_up",
                "in_transit",
                "out_for_delivery",
                "delivered",
                "delivery_failed",
                "returned",
                "lost",
                "address_error",
                "at_office",
                "canceled",
                "exception"
              ],
              "description": "Estado interno (13 valores; los 28 estados de Envia.com se mapean a estos). draft = se intentó generar y falló (queda con errorLog); created = guía generada, único estado cancelable. Terminales: delivered, returned, lost, canceled."
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            },
            "description": "Distinto del estándar de la API (100/1000): por defecto 20 y máximo 100. Un valor mayor se recorta a 100 sin responder 400."
          }
        ],
        "responses": {
          "200": {
            "description": "Envíos y paginación; data puede estar vacío.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data",
                    "pagination"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/paths/~1shipments/post/responses/200/content/application~1json/schema/properties/data"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Cuerpo o query inválidos (paquetes, medidas, ids, status, ventana de recogida…).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1shipments/post/responses/400/content/application~1json/schema"
                },
                "examples": {
                  "packages": {
                    "value": {
                      "success": false,
                      "code": "VALIDATION_ERROR",
                      "message": "El paquete 1 necesita un peso de al menos 0.01 kg."
                    }
                  },
                  "status": {
                    "value": {
                      "success": false,
                      "code": "VALIDATION_ERROR",
                      "message": "status no es válido.",
                      "allowed": [
                        "draft",
                        "created",
                        "picked_up",
                        "in_transit",
                        "out_for_delivery",
                        "delivered",
                        "delivery_failed",
                        "returned",
                        "lost",
                        "address_error",
                        "at_office",
                        "canceled",
                        "exception"
                      ]
                    }
                  },
                  "target": {
                    "value": {
                      "success": false,
                      "code": "VALIDATION_ERROR",
                      "message": "Indica contactId o destination."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "description": "Permiso insuficiente (INSUFFICIENT_API_PERMISSIONS) o plan sin el módulo envios (PLAN_MODULE_REQUIRED, cuando ENVIOS_MODULE_ENFORCE está activo).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "examples": {
                  "permission": {
                    "value": {
                      "message": "Permiso insuficiente. Se requiere: shipments:read",
                      "code": "INSUFFICIENT_API_PERMISSIONS",
                      "requiredPermission": "shipments:read",
                      "availablePermissions": []
                    }
                  },
                  "module": {
                    "value": {
                      "message": "Los envíos son un módulo adicional de tu plan. Contáctanos para activarlo.",
                      "code": "PLAN_MODULE_REQUIRED",
                      "module": "envios"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Error interno. Nunca incluye el detalle (puede traer datos del proveedor).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1shipments/post/responses/400/content/application~1json/schema"
                },
                "example": {
                  "success": false,
                  "code": "INTERNAL_ERROR",
                  "message": "Error interno procesando el envío."
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createShipment",
        "tags": [
          "Shipments"
        ],
        "summary": "Generar un envío (guía) en Envia.com",
        "description": "Requiere shipments:write o * y el módulo envios. Crea el envío en draft, llama a generate en Envia UNA sola vez (cobra saldo de la cuenta de Envia; nunca se reintenta) y lo deja en created con guía, etiqueta y saldo restante. carrier y service vienen de una tarifa de POST /shipments/rates. Idempotencia: manda idempotencyKey (1-100 caracteres, única por organización). Si ya existe un envío con esa clave y guía, se devuelve con 200 y reused:true sin llamar a Envia. Si existe pero quedó en draft porque generate falló, el reintento vuelve a intentar generate sobre ese mismo envío (mismo id, datos refrescados con el body, incluidos dealId/quotationId/invoiceId): 201 reused:false si esta vez sale, 422 ENVIA_GENERATE_FAILED si vuelve a fallar. Eso aplica solo a fallos SEGUROS (Envia respondió y rechazó la guía). Si el resultado es desconocido (timeout de 25 s, red o 5xx del gateway, excepción del cliente, respuesta ok sin número de guía, o un generate colgado más de 120 s) la guía pudo cobrarse: responde 409 SHIPMENT_NEEDS_REVIEW y ese envío nunca se regenera solo; revisa el panel de Envia y reintenta con una clave nueva. Mientras un generate con esa clave sigue en curso (candado de hasta 120 s), la misma clave responde 409 SHIPMENT_IN_PROGRESS (nunca se llama a Envia dos veces). La misma clave con otro contactId → 400 IDEMPOTENCY_KEY_REUSED. reused:true garantiza guía. La clave gana sobre la ventana de 30 s. Sin clave la protección es solo best-effort: un segundo POST del mismo contacto responde 409 SHIPMENT_IN_PROGRESS si hay un draft suyo sin errores de hace menos de 30 s o con el candado de 120 s vivo, y 409 SHIPMENT_NEEDS_REVIEW si tiene un envío con resultado desconocido de hace menos de 10 min. Un error CARRIER_ERROR (422) se puede reintentar, pero revisa antes el panel de Envia: la transportadora pudo haber creado la guía. 408 y 429 cuentan como resultado desconocido. dealId/quotationId/invoiceId y packages[].presetId deben ser de la organización del token (400 VALIDATION_ERROR con field si no). El destino sale del contacto (contactId) y lo que venga en destination lo pisa; la ciudad se resuelve contra el catálogo DANE (422 INVALID_CITY con candidatos si es ambigua). El origen es el de Ajustes → Envíos de la app: no se manda por API (422 ORIGIN_NOT_CONFIGURED si falta). Si Envia rechaza la guía, el envío queda guardado en draft con el error y se responde 422 ENVIA_GENERATE_FAILED con error normalizado y shipmentId del borrador. La forma exacta de la respuesta de generate y del saldo queda sin verificar contra producción hasta X13.",
        "x-required-permissions": [
          "shipments:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "carrier y service salen de una tarifa de POST /shipments/rates. Genera la guía en Envia en la misma llamada (cobra saldo de la cuenta). Manda idempotencyKey para que un reintento del mismo POST devuelva el envío ya creado (200, reused:true) en vez de generar otra guía; si el intento anterior falló, el reintento vuelve a generar sobre el mismo envío. dealId/quotationId/invoiceId y packages[].presetId deben pertenecer a la organización del token (si no, 400 VALIDATION_ERROR con field).",
                "required": [
                  "contactId",
                  "packages",
                  "carrier",
                  "service"
                ],
                "properties": {
                  "contactId": {
                    "type": "string"
                  },
                  "destination": {
                    "type": "object",
                    "description": "Destino para cotizar o generar. Todo es opcional: lo que se envíe PISA lo que se lee del contacto (contactId). La ciudad se resuelve contra el catálogo DANE: manda cityCode (DANE de 5 dígitos, el que devuelve /catalogs) o city + state inequívocos; si es ambigua o no se reconoce → 422 INVALID_CITY con candidatos. País distinto de Colombia → 422 INVALID_CITY (reason unsupported_country).",
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "company": {
                        "type": "string"
                      },
                      "email": {
                        "type": "string"
                      },
                      "phone": {
                        "type": "string"
                      },
                      "street": {
                        "type": "string"
                      },
                      "number": {
                        "type": "string"
                      },
                      "district": {
                        "type": "string"
                      },
                      "reference": {
                        "type": "string"
                      },
                      "identificationNumber": {
                        "type": "string"
                      },
                      "city": {
                        "type": "string",
                        "description": "Texto libre (\"Medellín\", \"Lerida\")."
                      },
                      "cityCode": {
                        "type": "string",
                        "description": "DANE de municipio, 5 dígitos (\"05001\"). Gana sobre city."
                      },
                      "state": {
                        "type": "string",
                        "description": "Texto libre del departamento (\"Tolima\")."
                      },
                      "stateCode": {
                        "type": "string",
                        "description": "DANE de departamento, 2 dígitos (\"73\")."
                      },
                      "country": {
                        "type": "string",
                        "default": "CO"
                      },
                      "postalCode": {
                        "type": "string"
                      }
                    }
                  },
                  "packages": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "object",
                      "required": [
                        "content",
                        "weight",
                        "dimensions"
                      ],
                      "properties": {
                        "content": {
                          "type": "string",
                          "description": "Descripción del contenido."
                        },
                        "amount": {
                          "type": "integer",
                          "minimum": 1,
                          "default": 1,
                          "description": "Cantidad de bultos iguales."
                        },
                        "weight": {
                          "type": "number",
                          "minimum": 0.01,
                          "description": "Kilogramos (mínimo 0.01)."
                        },
                        "dimensions": {
                          "type": "object",
                          "required": [
                            "length",
                            "width",
                            "height"
                          ],
                          "description": "Centímetros, todas de al menos 1.",
                          "properties": {
                            "length": {
                              "type": "number",
                              "minimum": 1
                            },
                            "width": {
                              "type": "number",
                              "minimum": 1
                            },
                            "height": {
                              "type": "number",
                              "minimum": 1
                            }
                          }
                        },
                        "declaredValue": {
                          "type": "number",
                          "minimum": 0,
                          "description": "Valor declarado en COP."
                        },
                        "presetId": {
                          "type": "string",
                          "description": "Preset de paquete de la app (opcional); debe ser de la organización (si no, 400 con field)."
                        }
                      }
                    }
                  },
                  "carrier": {
                    "type": "string",
                    "example": "serviEntrega"
                  },
                  "service": {
                    "type": "string",
                    "example": "premier"
                  },
                  "insurance": {
                    "description": "Seguro. Un booleano equivale a { enabled }.",
                    "oneOf": [
                      {
                        "type": "boolean"
                      },
                      {
                        "type": "object",
                        "required": [
                          "enabled"
                        ],
                        "properties": {
                          "enabled": {
                            "type": "boolean"
                          },
                          "amount": {
                            "type": "number",
                            "exclusiveMinimum": 0,
                            "description": "Valor asegurado; si falta se usa el declarado."
                          }
                        }
                      }
                    ]
                  },
                  "cod": {
                    "type": "object",
                    "description": "Pago contra entrega (solo si el servicio lo soporta).",
                    "required": [
                      "amount"
                    ],
                    "properties": {
                      "amount": {
                        "type": "number",
                        "exclusiveMinimum": 0
                      }
                    }
                  },
                  "pickup": {
                    "type": "object",
                    "description": "Recogida al generar. date YYYY-MM-DD, horas HH:MM, timeFrom < timeTo.",
                    "required": [
                      "date",
                      "timeFrom",
                      "timeTo"
                    ],
                    "properties": {
                      "date": {
                        "type": "string",
                        "format": "date",
                        "example": "2026-10-01"
                      },
                      "timeFrom": {
                        "type": "string",
                        "example": "09:00"
                      },
                      "timeTo": {
                        "type": "string",
                        "example": "13:00"
                      },
                      "instructions": {
                        "type": "string",
                        "maxLength": 300
                      }
                    }
                  },
                  "dealId": {
                    "type": "string",
                    "description": "Negocio de la organización (400 con field si es ajeno)."
                  },
                  "quotationId": {
                    "type": "string",
                    "description": "Cotización de la organización (400 con field si es ajena)."
                  },
                  "invoiceId": {
                    "type": "string",
                    "description": "Factura de la organización (400 con field si es ajena)."
                  },
                  "idempotencyKey": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 100,
                    "description": "Clave del cliente, única por organización. Un reintento con la misma clave devuelve el envío existente si ya tiene guía (200, reused:true) sin volver a llamar a Envia; si el intento anterior falló (draft), vuelve a intentar generate sobre el mismo envío (201 reused:false, o 422 si falla otra vez). Mientras hay un generate en curso con esa clave (candado de hasta 120 s), la misma clave responde 409 SHIPMENT_IN_PROGRESS sin llamar a Envia; si el resultado quedó desconocido (timeout, red/5xx, respuesta sin guía, candado vencido) responde siempre 409 SHIPMENT_NEEDS_REVIEW; con otro contactId, 400 IDEMPOTENCY_KEY_REUSED. Sin clave la protección es solo best-effort. Va una clave por intento de envío (pedido-4521-1), no por pedido: con reused:true el body se ignora, y para generar otra guía tras cancelar se usa una clave nueva. Sin clave, un segundo POST del mismo contacto mientras el anterior sigue en draft (<30 s) responde 409 SHIPMENT_IN_PROGRESS."
                  }
                }
              },
              "examples": {
                "minimal": {
                  "summary": "Destino del contacto, tarifa elegida de /rates.",
                  "value": {
                    "contactId": "66c74b4f66baaf700d2f13e4",
                    "carrier": "serviEntrega",
                    "service": "premier",
                    "packages": [
                      {
                        "content": "manillas",
                        "weight": 1,
                        "dimensions": {
                          "length": 20,
                          "width": 15,
                          "height": 10
                        },
                        "declaredValue": 50000
                      }
                    ]
                  }
                },
                "full": {
                  "summary": "Con dirección confirmada, seguro, recogida y negocio asociado.",
                  "value": {
                    "contactId": "66c74b4f66baaf700d2f13e4",
                    "destination": {
                      "cityCode": "73408",
                      "street": "Calle 5 # 3-20",
                      "phone": "3001234567"
                    },
                    "carrier": "coordinadora",
                    "service": "standard",
                    "packages": [
                      {
                        "content": "manillas tyvek",
                        "amount": 2,
                        "weight": 1.5,
                        "dimensions": {
                          "length": 30,
                          "width": 20,
                          "height": 10
                        },
                        "declaredValue": 120000
                      }
                    ],
                    "insurance": {
                      "enabled": true,
                      "amount": 120000
                    },
                    "pickup": {
                      "date": "2026-10-01",
                      "timeFrom": "09:00",
                      "timeTo": "13:00"
                    },
                    "dealId": "68d0000000000000000000a1",
                    "idempotencyKey": "pedido-4521-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Ya existía un envío CON GUÍA para esa idempotencyKey; se devuelve sin volver a generar (reused:true).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data",
                    "reused"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "description": "Envío tal como sale por la API. Sin errorLog (interno). Los ids son cadenas.",
                      "required": [
                        "id",
                        "organizationId",
                        "contactId",
                        "provider",
                        "carrier",
                        "service",
                        "labelFormat",
                        "status",
                        "events",
                        "origin",
                        "destination",
                        "packages",
                        "pricing",
                        "createdBy",
                        "createdAt",
                        "updatedAt"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "organizationId": {
                          "type": "string"
                        },
                        "contactId": {
                          "type": "string"
                        },
                        "dealId": {
                          "type": "string"
                        },
                        "quotationId": {
                          "type": "string"
                        },
                        "invoiceId": {
                          "type": "string"
                        },
                        "provider": {
                          "type": "string",
                          "const": "envia"
                        },
                        "environment": {
                          "type": "string",
                          "enum": [
                            "production",
                            "sandbox"
                          ]
                        },
                        "carrier": {
                          "type": "string",
                          "example": "serviEntrega"
                        },
                        "service": {
                          "type": "string",
                          "example": "premier"
                        },
                        "carrierDescription": {
                          "type": "string"
                        },
                        "serviceDescription": {
                          "type": "string"
                        },
                        "providerShipmentId": {
                          "type": "string"
                        },
                        "trackingNumber": {
                          "type": "string"
                        },
                        "trackUrl": {
                          "type": "string",
                          "format": "uri"
                        },
                        "labelUrl": {
                          "type": "string",
                          "format": "uri",
                          "description": "Etiqueta (PDF o ZPL según labelFormat)."
                        },
                        "labelFormat": {
                          "type": "string",
                          "enum": [
                            "PDF",
                            "ZPL"
                          ]
                        },
                        "idempotencyKey": {
                          "type": "string",
                          "description": "La clave que mandó el cliente al crearlo",
                          "si la hubo.": null
                        },
                        "generatingSince": {
                          "type": "string",
                          "format": "date-time",
                          "description": "Hay un generate en vuelo desde esta fecha (candado de hasta 120 s). Ausente si no. Un valor de más de 120 s equivale a resultado desconocido (el proceso se cayó a mitad de generate): se trata como generateOutcome=unknown (409 SHIPMENT_NEEDS_REVIEW)."
                        },
                        "generateOutcome": {
                          "type": "string",
                          "enum": [
                            "unknown"
                          ],
                          "description": "unknown = el generate terminó sin saber si Envia creó (y cobró) la guía. El envío no se regenera solo (409 SHIPMENT_NEEDS_REVIEW); revisa el panel de Envia."
                        },
                        "status": {
                          "$ref": "#/paths/~1shipments/get/parameters/1/schema"
                        },
                        "statusRaw": {
                          "type": "string"
                        },
                        "statusUpdatedAt": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "events": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "required": [
                              "at",
                              "status",
                              "raw",
                              "source"
                            ],
                            "properties": {
                              "at": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "status": {
                                "oneOf": [
                                  {
                                    "$ref": "#/paths/~1shipments/get/parameters/1/schema"
                                  },
                                  {
                                    "type": "null"
                                  }
                                ],
                                "description": "Estado interno tras el evento; null si fue informativo (no cambió el estado)."
                              },
                              "raw": {
                                "type": "string",
                                "description": "Estado tal como lo reporta Envia."
                              },
                              "description": {
                                "type": "string"
                              },
                              "location": {
                                "type": "string"
                              },
                              "source": {
                                "type": "string",
                                "enum": [
                                  "generate",
                                  "poll",
                                  "webhook",
                                  "manual"
                                ]
                              }
                            }
                          }
                        },
                        "origin": {
                          "type": "object",
                          "description": "Dirección tal como se mandó a Envia (no se vuelve a resolver). cityCode es DANE de 8 dígitos y stateCode la sigla de 2 letras que exige Envia (AN, DC, VC…), no el ISO 3166-2.",
                          "required": [
                            "name",
                            "phone",
                            "street",
                            "cityCode",
                            "stateCode",
                            "country"
                          ],
                          "properties": {
                            "name": {
                              "type": "string"
                            },
                            "company": {
                              "type": "string"
                            },
                            "phone": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string"
                            },
                            "street": {
                              "type": "string",
                              "description": "Dirección de calle (\"Cra 43A # 1-50\")."
                            },
                            "number": {
                              "type": "string"
                            },
                            "district": {
                              "type": "string",
                              "description": "Barrio."
                            },
                            "cityText": {
                              "type": "string",
                              "description": "Texto que tenía el CRM (\"Lérida\")."
                            },
                            "cityCode": {
                              "type": "string",
                              "pattern": "^\\d{8}$",
                              "example": "05001000"
                            },
                            "stateCode": {
                              "type": "string",
                              "pattern": "^[A-Z]{2}$",
                              "example": "AN"
                            },
                            "postalCode": {
                              "type": "string"
                            },
                            "country": {
                              "type": "string",
                              "example": "CO"
                            },
                            "identificationNumber": {
                              "type": "string",
                              "description": "NIT o cédula."
                            },
                            "reference": {
                              "type": "string",
                              "description": "Indicaciones para el mensajero."
                            },
                            "resolvedBy": {
                              "type": "string",
                              "enum": [
                                "cityCode",
                                "exact",
                                "alias",
                                "fuzzy",
                                "manual",
                                "geocode"
                              ],
                              "description": "Cómo se resolvió la ciudad del snapshot contra el catálogo DANE."
                            }
                          }
                        },
                        "destination": {
                          "$ref": "#/paths/~1shipments/post/responses/200/content/application~1json/schema/properties/data/properties/origin"
                        },
                        "packages": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "description": "Paquete tal como quedó guardado.",
                            "required": [
                              "content",
                              "amount",
                              "weight",
                              "dimensions"
                            ],
                            "properties": {
                              "content": {
                                "type": "string"
                              },
                              "amount": {
                                "type": "integer",
                                "minimum": 1
                              },
                              "weight": {
                                "type": "number"
                              },
                              "dimensions": {
                                "type": "object",
                                "properties": {
                                  "length": {
                                    "type": "number"
                                  },
                                  "width": {
                                    "type": "number"
                                  },
                                  "height": {
                                    "type": "number"
                                  }
                                }
                              },
                              "declaredValue": {
                                "type": "number"
                              },
                              "presetId": {
                                "type": "string"
                              }
                            }
                          }
                        },
                        "declaredValue": {
                          "type": "number"
                        },
                        "insurance": {
                          "type": "object",
                          "properties": {
                            "enabled": {
                              "type": "boolean"
                            },
                            "amount": {
                              "type": "number"
                            }
                          }
                        },
                        "cod": {
                          "type": "object",
                          "properties": {
                            "amount": {
                              "type": "number"
                            },
                            "commission": {
                              "type": "number"
                            }
                          }
                        },
                        "pickup": {
                          "type": "object",
                          "properties": {
                            "requested": {
                              "type": "boolean"
                            },
                            "number": {
                              "type": "string"
                            },
                            "date": {
                              "type": "string"
                            },
                            "timeFrom": {
                              "type": "string"
                            },
                            "timeTo": {
                              "type": "string"
                            },
                            "fee": {
                              "type": "number"
                            }
                          }
                        },
                        "pricing": {
                          "type": "object",
                          "required": [
                            "base",
                            "insurance",
                            "charges",
                            "taxes",
                            "total",
                            "currency"
                          ],
                          "properties": {
                            "base": {
                              "type": "number"
                            },
                            "insurance": {
                              "type": "number"
                            },
                            "charges": {
                              "type": "number"
                            },
                            "taxes": {
                              "type": "number"
                            },
                            "total": {
                              "type": "number"
                            },
                            "currency": {
                              "type": "string",
                              "example": "COP"
                            },
                            "deliveryEstimate": {
                              "type": "string",
                              "example": "1 a 2 días"
                            },
                            "deliveryDate": {
                              "type": "string"
                            }
                          }
                        },
                        "balanceAfter": {
                          "type": "number",
                          "description": "Saldo en Envia después de generar (sin verificar hasta X13)."
                        },
                        "createdBy": {
                          "type": "string",
                          "description": "Usuario dueño del token."
                        },
                        "canceledAt": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "canceledBy": {
                          "type": "string"
                        },
                        "balanceReturned": {
                          "type": "number",
                          "description": "Saldo devuelto al cancelar (sin verificar hasta X13)."
                        },
                        "lastPolledAt": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "createdAt": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updatedAt": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "reused": {
                      "type": "boolean",
                      "description": "true = ya existía un envío con guía para esa idempotencyKey y no se llamó a Envia (status 200). false = guía recién generada (201), incluido el reintento con clave sobre un draft cuyo generate anterior falló."
                    },
                    "pickupError": {
                      "type": "string",
                      "description": "Solo si se pidió pickup: la guía se generó pero Envia rechazó la recogida (se pide en una llamada aparte, después de la guía). El envío queda con pickup.requested=false."
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Guía generada; el envío queda en created (reused:false).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1shipments/post/responses/200/content/application~1json/schema"
                },
                "examples": {
                  "created": {
                    "summary": "Guía recién generada (reused:false).",
                    "value": {
                      "success": true,
                      "reused": false,
                      "data": {
                        "id": "68da1f0c2b7e4a5d9c1a0f11",
                        "organizationId": "6814ef02e3de1af46109d105",
                        "contactId": "66c74b4f66baaf700d2f13e4",
                        "provider": "envia",
                        "environment": "production",
                        "carrier": "serviEntrega",
                        "service": "premier",
                        "trackingNumber": "2104567890",
                        "trackUrl": "https://www.servientrega.com/wps/portal/rastreo-envio?guia=2104567890",
                        "labelUrl": "https://s3.amazonaws.com/envia-labels/2104567890.pdf",
                        "labelFormat": "PDF",
                        "idempotencyKey": "pedido-4521-1",
                        "status": "created",
                        "events": [
                          {
                            "at": "2026-09-29T15:00:00.000Z",
                            "status": "created",
                            "raw": "Created",
                            "source": "generate"
                          }
                        ],
                        "origin": {
                          "name": "Manillas de Control",
                          "phone": "3132925094",
                          "street": "Cra 43A # 1-50",
                          "cityCode": "05001000",
                          "stateCode": "AN",
                          "country": "CO"
                        },
                        "destination": {
                          "name": "Cliente Lérida",
                          "phone": "3001234567",
                          "street": "Calle 5 # 3-20",
                          "cityCode": "73408000",
                          "stateCode": "TO",
                          "country": "CO",
                          "resolvedBy": "exact"
                        },
                        "packages": [
                          {
                            "content": "manillas",
                            "amount": 1,
                            "weight": 1,
                            "dimensions": {
                              "length": 20,
                              "width": 15,
                              "height": 10
                            },
                            "declaredValue": 50000
                          }
                        ],
                        "declaredValue": 50000,
                        "insurance": {
                          "enabled": false
                        },
                        "pricing": {
                          "base": 12000,
                          "insurance": 0,
                          "charges": 0,
                          "taxes": 2280,
                          "total": 14280,
                          "currency": "COP"
                        },
                        "createdBy": "68fa9280f6634d7b288fdc75",
                        "createdAt": "2026-09-29T15:00:00.000Z",
                        "updatedAt": "2026-09-29T15:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Cuerpo inválido (paquetes, ids, idempotencyKey fuera de 1-100 caracteres), referencia ajena a la organización (field), o idempotencyKey ya usada con otro contacto (IDEMPOTENCY_KEY_REUSED).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Error del módulo: success:false + code + message, y detalles según el código (candidates en INVALID_CITY; error + shipmentId en ENVIA_GENERATE_FAILED; status en SHIPMENT_NOT_CANCELABLE; allowed en status inválido).",
                  "required": [
                    "success",
                    "code",
                    "message"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": false
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "VALIDATION_ERROR",
                        "ENVIA_NOT_CONFIGURED",
                        "ORIGIN_NOT_CONFIGURED",
                        "INVALID_CITY",
                        "CONTACT_NOT_FOUND",
                        "SHIPMENT_NOT_FOUND",
                        "SHIPMENT_IN_PROGRESS",
                        "SHIPMENT_NEEDS_REVIEW",
                        "IDEMPOTENCY_KEY_REUSED",
                        "SHIPMENT_NOT_CANCELABLE",
                        "SHIPMENT_NOT_TRACKABLE",
                        "CARRIERS_UNAVAILABLE",
                        "ENVIA_GENERATE_FAILED",
                        "ENVIA_TRACK_FAILED",
                        "ENVIA_CANCEL_FAILED",
                        "INTERNAL_ERROR"
                      ]
                    },
                    "message": {
                      "type": "string"
                    },
                    "error": {
                      "type": "object",
                      "description": "Error del proveedor normalizado. Nunca el body crudo ni el token.",
                      "required": [
                        "code",
                        "message"
                      ],
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "INVALID_CITY",
                            "AUTH",
                            "INSUFFICIENT_BALANCE",
                            "NOT_FOUND",
                            "NO_COVERAGE",
                            "CARRIER_ERROR",
                            "TIMEOUT",
                            "NETWORK",
                            "UNKNOWN"
                          ],
                          "description": "NO_COVERAGE solo aparece al cotizar (la transportadora no tiene tarifa para la ruta); NOT_FOUND es de rastreo."
                        },
                        "message": {
                          "type": "string"
                        },
                        "providerCode": {
                          "type": "integer",
                          "description": "Código numérico de Envia (1300, 1125…)."
                        }
                      }
                    },
                    "shipmentId": {
                      "type": "string",
                      "description": "En ENVIA_GENERATE_FAILED, el borrador que quedó guardado; en SHIPMENT_IN_PROGRESS, el draft en curso; en SHIPMENT_NEEDS_REVIEW, el envío con resultado desconocido; en IDEMPOTENCY_KEY_REUSED, el envío que ya usa la clave."
                    },
                    "field": {
                      "type": "string",
                      "description": "En VALIDATION_ERROR por referencia ajena: dealId, quotationId, invoiceId o presetId."
                    },
                    "reason": {
                      "type": "string",
                      "description": "En INVALID_CITY: fuzzy, ambiguous, none o unsupported_country."
                    },
                    "candidates": {
                      "type": "array",
                      "items": {}
                    },
                    "status": {
                      "$ref": "#/paths/~1shipments/get/parameters/1/schema"
                    },
                    "allowed": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  },
                  "additionalProperties": true
                },
                "examples": {
                  "packages": {
                    "value": {
                      "success": false,
                      "code": "VALIDATION_ERROR",
                      "message": "El paquete 1 necesita un peso de al menos 0.01 kg."
                    }
                  },
                  "ref": {
                    "value": {
                      "success": false,
                      "code": "VALIDATION_ERROR",
                      "message": "dealId no pertenece a la organización.",
                      "field": "dealId"
                    }
                  },
                  "key": {
                    "value": {
                      "success": false,
                      "code": "VALIDATION_ERROR",
                      "message": "idempotencyKey debe tener entre 1 y 100 caracteres."
                    }
                  },
                  "keyReused": {
                    "value": {
                      "success": false,
                      "code": "IDEMPOTENCY_KEY_REUSED",
                      "message": "Esa idempotencyKey ya se usó para un envío de otro contacto. Usa una clave nueva por intento de envío.",
                      "shipmentId": "68da1f0c2b7e4a5d9c1a0f11"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1shipments/get/responses/403"
          },
          "404": {
            "description": "contactId no existe en la organización del token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1shipments/post/responses/400/content/application~1json/schema"
                },
                "example": {
                  "success": false,
                  "code": "CONTACT_NOT_FOUND",
                  "message": "El contacto no existe en esta organización."
                }
              }
            }
          },
          "409": {
            "description": "Con idempotencyKey: otro POST con la misma clave todavía está generando la guía (SHIPMENT_IN_PROGRESS, candado de hasta 120 s), o el generate anterior terminó con resultado desconocido y la guía pudo cobrarse (SHIPMENT_NEEDS_REVIEW: no reintentes con la misma clave; revisa el panel de Envia y usa una clave nueva). Sin clave: hay un draft del mismo contacto de hace menos de 30 s o con el candado vivo (SHIPMENT_IN_PROGRESS), o un envío suyo con resultado desconocido de hace menos de 10 min (SHIPMENT_NEEDS_REVIEW); best-effort.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1shipments/post/responses/400/content/application~1json/schema"
                },
                "examples": {
                  "inProgress": {
                    "value": {
                      "success": false,
                      "code": "SHIPMENT_IN_PROGRESS",
                      "message": "Ya hay un envío generándose para este contacto. Espera unos segundos.",
                      "shipmentId": "68da1f0c2b7e4a5d9c1a0f11"
                    }
                  },
                  "needsReview": {
                    "value": {
                      "success": false,
                      "code": "SHIPMENT_NEEDS_REVIEW",
                      "message": "No sabemos si Envia generó la guía; revisa tu panel de Envia antes de reintentar con una clave nueva.",
                      "shipmentId": "68da1f0c2b7e4a5d9c1a0f11",
                      "error": {
                        "code": "TIMEOUT",
                        "message": "Envia.com no respondió a tiempo."
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Integración u origen sin configurar, ciudad no resuelta, o Envia rechazó la guía (saldo insuficiente, transportadora caída…). En ENVIA_GENERATE_FAILED el borrador queda guardado (shipmentId).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1shipments/post/responses/400/content/application~1json/schema"
                },
                "examples": {
                  "generate": {
                    "value": {
                      "success": false,
                      "code": "ENVIA_GENERATE_FAILED",
                      "message": "Saldo insuficiente",
                      "error": {
                        "code": "INSUFFICIENT_BALANCE",
                        "message": "Saldo insuficiente",
                        "providerCode": 1200
                      },
                      "shipmentId": "68da1f0c2b7e4a5d9c1a0f11"
                    }
                  },
                  "city": {
                    "value": {
                      "success": false,
                      "code": "INVALID_CITY",
                      "message": "La ciudad \"Armenia\" es ambigua; indica el departamento o el cityCode.",
                      "reason": "ambiguous",
                      "input": {
                        "city": "Armenia"
                      },
                      "candidates": [
                        {
                          "code": "63001",
                          "dane8": "63001000",
                          "name": "Armenia",
                          "stateCode": "63",
                          "stateIso": "QU",
                          "stateName": "Quindío"
                        },
                        {
                          "code": "05059",
                          "dane8": "05059000",
                          "name": "Armenia",
                          "stateCode": "05",
                          "stateIso": "AN",
                          "stateName": "Antioquia"
                        }
                      ]
                    }
                  },
                  "origin": {
                    "value": {
                      "success": false,
                      "code": "ORIGIN_NOT_CONFIGURED",
                      "message": "Configura la dirección de origen en Ajustes → Envíos antes de cotizar."
                    }
                  },
                  "notConfigured": {
                    "value": {
                      "success": false,
                      "code": "ENVIA_NOT_CONFIGURED",
                      "message": "La organización no tiene configurada la integración con Envia.com. Agrega la API key en Integraciones."
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1shipments/get/responses/500"
          }
        }
      }
    },
    "/shipments/rates": {
      "post": {
        "operationId": "quoteShipmentRates",
        "tags": [
          "Shipments"
        ],
        "summary": "Cotizar tarifas",
        "description": "Requiere shipments:read o * (es una consulta aunque sea POST; no escribe nada ni cobra saldo) y el módulo envios. Consulta en paralelo cada transportadora de la cuenta de Envia (hasta 20 s por carrier, con un reintento): las que responden van en rates ordenadas por precio y las que fallan en failed, sin tumbar la cotización. Manda contactId (destino = dirección del contacto) o destination; con ambos, destination pisa. Ciudad ambigua o no reconocida → 422 INVALID_CITY con candidatos.",
        "x-required-permissions": [
          "shipments:read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "contactId o destination, al menos uno. Con ambos, destination pisa lo del contacto.",
                "required": [
                  "packages"
                ],
                "properties": {
                  "contactId": {
                    "type": "string",
                    "description": "Contacto de la organización; su dirección es el destino."
                  },
                  "destination": {
                    "$ref": "#/paths/~1shipments/post/requestBody/content/application~1json/schema/properties/destination"
                  },
                  "packages": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "$ref": "#/paths/~1shipments/post/requestBody/content/application~1json/schema/properties/packages/items"
                    }
                  },
                  "insurance": {
                    "$ref": "#/paths/~1shipments/post/requestBody/content/application~1json/schema/properties/insurance"
                  },
                  "cod": {
                    "$ref": "#/paths/~1shipments/post/requestBody/content/application~1json/schema/properties/cod"
                  },
                  "carriers": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Códigos de transportadora a cotizar; sin él, todas las disponibles en la cuenta."
                  }
                }
              },
              "examples": {
                "contact": {
                  "summary": "Cotizar al contacto con un paquete.",
                  "value": {
                    "contactId": "66c74b4f66baaf700d2f13e4",
                    "packages": [
                      {
                        "content": "manillas",
                        "weight": 1,
                        "dimensions": {
                          "length": 20,
                          "width": 15,
                          "height": 10
                        },
                        "declaredValue": 50000
                      }
                    ]
                  }
                },
                "destination": {
                  "summary": "Sin contacto, con ciudad por cityCode y dos transportadoras.",
                  "value": {
                    "destination": {
                      "name": "Cliente",
                      "phone": "3001234567",
                      "street": "Calle 5 # 3-20",
                      "cityCode": "73408"
                    },
                    "packages": [
                      {
                        "content": "manillas",
                        "weight": 1,
                        "dimensions": {
                          "length": 20,
                          "width": 15,
                          "height": 10
                        }
                      }
                    ],
                    "carriers": [
                      "serviEntrega",
                      "coordinadora"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tarifas por transportadora (rates puede estar vacío si todas fallaron).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "rates",
                        "failed",
                        "tookMs",
                        "carriers"
                      ],
                      "properties": {
                        "rates": {
                          "type": "array",
                          "description": "Ordenadas por totalPrice ascendente. Puede estar vacía si todas fallaron.",
                          "items": {
                            "type": "object",
                            "description": "Tarifa de Envia.com para un carrier/servicio, tal como la devuelve el proveedor (pueden venir claves adicionales). carrier + service son lo que se manda a POST /shipments.",
                            "required": [
                              "carrier",
                              "service",
                              "totalPrice",
                              "currency"
                            ],
                            "properties": {
                              "carrier": {
                                "type": "string",
                                "example": "serviEntrega"
                              },
                              "carrierDescription": {
                                "type": "string",
                                "example": "Servientrega"
                              },
                              "carrierId": {
                                "type": "integer"
                              },
                              "serviceId": {
                                "type": "integer"
                              },
                              "service": {
                                "type": "string",
                                "example": "premier"
                              },
                              "serviceDescription": {
                                "type": "string"
                              },
                              "dropOff": {
                                "type": "integer",
                                "description": "1 si hay que llevarlo a una sucursal."
                              },
                              "dropOffDescription": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "deliveryEstimate": {
                                "type": "string"
                              },
                              "deliveryDate": {
                                "type": [
                                  "object",
                                  "null"
                                ],
                                "properties": {
                                  "date": {
                                    "type": "string"
                                  },
                                  "dateDifference": {
                                    "type": "integer"
                                  },
                                  "timeUnit": {
                                    "type": "string"
                                  },
                                  "time": {
                                    "type": "string"
                                  }
                                }
                              },
                              "basePrice": {
                                "type": "number"
                              },
                              "insurance": {
                                "type": "number"
                              },
                              "additionalServices": {
                                "type": "number"
                              },
                              "additionalCharges": {
                                "type": "number"
                              },
                              "taxes": {
                                "type": "number"
                              },
                              "totalPrice": {
                                "type": "number"
                              },
                              "currency": {
                                "type": "string",
                                "example": "COP"
                              },
                              "cashOnDeliveryCommission": {
                                "type": "number"
                              },
                              "cashOnDeliveryAmount": {
                                "type": "number"
                              },
                              "branches": {
                                "type": "array",
                                "items": {}
                              }
                            },
                            "additionalProperties": true
                          }
                        },
                        "failed": {
                          "type": "array",
                          "description": "Transportadoras sin tarifa (una por carrier). error.code NO_COVERAGE = sin cobertura para la ruta (no es una falla); TIMEOUT/NETWORK = no respondieron a tiempo; el resto, error.",
                          "items": {
                            "type": "object",
                            "required": [
                              "carrier",
                              "error"
                            ],
                            "properties": {
                              "carrier": {
                                "type": "string"
                              },
                              "error": {
                                "$ref": "#/paths/~1shipments/post/responses/400/content/application~1json/schema/properties/error"
                              }
                            }
                          }
                        },
                        "tookMs": {
                          "type": "integer"
                        },
                        "carriers": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "Transportadoras consultadas."
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "rates": {
                    "summary": "Dos tarifas y una transportadora caída.",
                    "value": {
                      "success": true,
                      "data": {
                        "rates": [
                          {
                            "carrier": "serviEntrega",
                            "carrierDescription": "Servientrega",
                            "service": "premier",
                            "serviceDescription": "Premier",
                            "deliveryEstimate": "2 a 3 días",
                            "basePrice": 12000,
                            "insurance": 0,
                            "additionalServices": 0,
                            "additionalCharges": 0,
                            "taxes": 2280,
                            "totalPrice": 14280,
                            "currency": "COP",
                            "dropOff": 0,
                            "branches": []
                          },
                          {
                            "carrier": "coordinadora",
                            "carrierDescription": "Coordinadora",
                            "service": "standard",
                            "serviceDescription": "Estándar",
                            "deliveryEstimate": "3 a 4 días",
                            "basePrice": 13500,
                            "insurance": 0,
                            "additionalServices": 0,
                            "additionalCharges": 0,
                            "taxes": 2565,
                            "totalPrice": 16065,
                            "currency": "COP",
                            "dropOff": 0,
                            "branches": []
                          }
                        ],
                        "failed": [
                          {
                            "carrier": "tcc",
                            "error": {
                              "code": "TIMEOUT",
                              "message": "Envia no respondió a tiempo."
                            }
                          }
                        ],
                        "tookMs": 4210,
                        "carriers": [
                          "serviEntrega",
                          "coordinadora",
                          "tcc"
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1shipments/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1shipments/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1shipments/post/responses/404"
          },
          "422": {
            "description": "Integración u origen sin configurar, o ciudad de destino no resuelta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1shipments/post/responses/400/content/application~1json/schema"
                },
                "examples": {
                  "city": {
                    "value": {
                      "success": false,
                      "code": "INVALID_CITY",
                      "message": "No se reconoce la ciudad \"Medellpin\".",
                      "reason": "none",
                      "input": {
                        "city": "Medellpin"
                      },
                      "candidates": []
                    }
                  },
                  "origin": {
                    "value": {
                      "success": false,
                      "code": "ORIGIN_NOT_CONFIGURED",
                      "message": "Configura la dirección de origen en Ajustes → Envíos antes de cotizar."
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1shipments/get/responses/500"
          },
          "502": {
            "description": "No se pudo obtener el catálogo de transportadoras de Envia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1shipments/post/responses/400/content/application~1json/schema"
                },
                "example": {
                  "success": false,
                  "code": "CARRIERS_UNAVAILABLE",
                  "message": "No se pudo obtener el catálogo de transportadoras de Envia.com.",
                  "error": {
                    "code": "NETWORK",
                    "message": "ECONNRESET"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/shipments/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB del envío (campo id de las respuestas).",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "68da1f0c2b7e4a5d9c1a0f11"
        }
      ],
      "get": {
        "operationId": "getShipmentById",
        "tags": [
          "Shipments"
        ],
        "summary": "Consultar un envío",
        "description": "Requiere shipments:read o * y el módulo envios. El envío debe pertenecer a la organización del token; si no, 404 (igual que uno inexistente). No consulta a Envia: devuelve el último estado guardado (usa POST /shipments/{id}/track para refrescarlo).",
        "x-required-permissions": [
          "shipments:read"
        ],
        "responses": {
          "200": {
            "description": "Envío encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "$ref": "#/paths/~1shipments/post/responses/200/content/application~1json/schema/properties/data"
                    }
                  }
                },
                "examples": {
                  "found": {
                    "summary": "Envío generado con Servientrega.",
                    "value": {
                      "success": true,
                      "data": {
                        "id": "68da1f0c2b7e4a5d9c1a0f11",
                        "organizationId": "6814ef02e3de1af46109d105",
                        "contactId": "66c74b4f66baaf700d2f13e4",
                        "provider": "envia",
                        "environment": "production",
                        "carrier": "serviEntrega",
                        "service": "premier",
                        "carrierDescription": "Servientrega",
                        "serviceDescription": "Premier",
                        "trackingNumber": "2104567890",
                        "trackUrl": "https://www.servientrega.com/wps/portal/rastreo-envio?guia=2104567890",
                        "labelUrl": "https://s3.amazonaws.com/envia-labels/2104567890.pdf",
                        "labelFormat": "PDF",
                        "status": "created",
                        "statusRaw": "Created",
                        "statusUpdatedAt": "2026-09-29T15:00:00.000Z",
                        "events": [
                          {
                            "at": "2026-09-29T15:00:00.000Z",
                            "status": "created",
                            "raw": "Created",
                            "source": "generate"
                          }
                        ],
                        "origin": {
                          "name": "Manillas de Control",
                          "phone": "3132925094",
                          "street": "Cra 43A # 1-50",
                          "cityText": "Medellín",
                          "cityCode": "05001000",
                          "stateCode": "AN",
                          "country": "CO"
                        },
                        "destination": {
                          "name": "Cliente Lérida",
                          "phone": "3001234567",
                          "street": "Calle 5 # 3-20",
                          "cityText": "Lérida",
                          "cityCode": "73408000",
                          "stateCode": "TO",
                          "country": "CO",
                          "resolvedBy": "exact"
                        },
                        "packages": [
                          {
                            "content": "manillas",
                            "amount": 1,
                            "weight": 1,
                            "dimensions": {
                              "length": 20,
                              "width": 15,
                              "height": 10
                            },
                            "declaredValue": 50000
                          }
                        ],
                        "declaredValue": 50000,
                        "insurance": {
                          "enabled": false
                        },
                        "pricing": {
                          "base": 12000,
                          "insurance": 0,
                          "charges": 0,
                          "taxes": 2280,
                          "total": 14280,
                          "currency": "COP",
                          "deliveryEstimate": "2 a 3 días"
                        },
                        "balanceAfter": 185720,
                        "createdBy": "68fa9280f6634d7b288fdc75",
                        "createdAt": "2026-09-29T15:00:00.000Z",
                        "updatedAt": "2026-09-29T15:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "El id no es un ObjectId.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1shipments/post/responses/400/content/application~1json/schema"
                },
                "example": {
                  "success": false,
                  "code": "VALIDATION_ERROR",
                  "message": "id no es un id válido."
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1shipments/get/responses/403"
          },
          "404": {
            "description": "Envío inexistente o de otra organización (misma respuesta en ambos casos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1shipments/post/responses/400/content/application~1json/schema"
                },
                "example": {
                  "success": false,
                  "code": "SHIPMENT_NOT_FOUND",
                  "message": "Envío no encontrado."
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1shipments/get/responses/500"
          }
        }
      }
    },
    "/shipments/{id}/track": {
      "parameters": [
        {
          "$ref": "#/paths/~1shipments~1{id}/parameters/0"
        }
      ],
      "post": {
        "operationId": "trackShipment",
        "tags": [
          "Shipments"
        ],
        "summary": "Refrescar el rastreo de un envío",
        "description": "Requiere shipments:write o * y el módulo envios. Consulta el rastreo en Envia, agrega los eventos nuevos (sin duplicar) y actualiza status/statusRaw. Devuelve el envío actualizado. Un envío sin guía (draft) → 409 SHIPMENT_NOT_TRACKABLE. Si Envia falla, el error queda en el errorLog interno y se responde 502 ENVIA_TRACK_FAILED.",
        "x-required-permissions": [
          "shipments:write"
        ],
        "responses": {
          "200": {
            "description": "Envío con el rastreo actualizado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1shipments~1{id}/get/responses/200/content/application~1json/schema"
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1shipments~1{id}/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1shipments/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1shipments~1{id}/get/responses/404"
          },
          "409": {
            "description": "El envío no tiene guía generada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1shipments/post/responses/400/content/application~1json/schema"
                },
                "example": {
                  "success": false,
                  "code": "SHIPMENT_NOT_TRACKABLE",
                  "message": "El envío no tiene guía generada."
                }
              }
            }
          },
          "422": {
            "description": "La organización no tiene configurada la integración con Envia.com.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1shipments/post/responses/400/content/application~1json/schema"
                },
                "example": {
                  "success": false,
                  "code": "ENVIA_NOT_CONFIGURED",
                  "message": "La organización no tiene configurada la integración con Envia.com. Agrega la API key en Integraciones."
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1shipments/get/responses/500"
          },
          "502": {
            "description": "Envia no respondió al rastreo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1shipments/post/responses/400/content/application~1json/schema"
                },
                "example": {
                  "success": false,
                  "code": "ENVIA_TRACK_FAILED",
                  "message": "Envia no respondió a tiempo.",
                  "error": {
                    "code": "TIMEOUT",
                    "message": "Envia no respondió a tiempo."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/shipments/{id}/cancel": {
      "parameters": [
        {
          "$ref": "#/paths/~1shipments~1{id}/parameters/0"
        }
      ],
      "post": {
        "operationId": "cancelShipment",
        "tags": [
          "Shipments"
        ],
        "summary": "Cancelar un envío",
        "description": "Requiere shipments:write o * y el módulo envios. Solo se cancela un envío en created (guía generada y aún no recogida): cualquier otro estado → 409 SHIPMENT_NOT_CANCELABLE con el status actual. Cancela la guía en Envia y deja el envío en canceled con canceledAt/canceledBy y, si Envia lo informa, balanceReturned (forma sin verificar hasta X13). Si Envia rechaza la cancelación → 422 ENVIA_CANCEL_FAILED y el envío no cambia.",
        "x-required-permissions": [
          "shipments:write"
        ],
        "responses": {
          "200": {
            "description": "Envío cancelado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1shipments~1{id}/get/responses/200/content/application~1json/schema"
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1shipments~1{id}/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1shipments/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1shipments~1{id}/get/responses/404"
          },
          "409": {
            "description": "El envío no está en created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1shipments/post/responses/400/content/application~1json/schema"
                },
                "example": {
                  "success": false,
                  "code": "SHIPMENT_NOT_CANCELABLE",
                  "message": "Un envío en estado \"in_transit\" no se puede cancelar.",
                  "status": "in_transit"
                }
              }
            }
          },
          "422": {
            "description": "Envia rechazó la cancelación, o la integración no está configurada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1shipments/post/responses/400/content/application~1json/schema"
                },
                "examples": {
                  "cancel": {
                    "value": {
                      "success": false,
                      "code": "ENVIA_CANCEL_FAILED",
                      "message": "La guía ya fue recolectada.",
                      "error": {
                        "code": "CARRIER_ERROR",
                        "message": "La guía ya fue recolectada.",
                        "providerCode": 1125
                      }
                    }
                  },
                  "notConfigured": {
                    "value": {
                      "success": false,
                      "code": "ENVIA_NOT_CONFIGURED",
                      "message": "La organización no tiene configurada la integración con Envia.com. Agrega la API key en Integraciones."
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1shipments/get/responses/500"
          }
        }
      }
    },
    "/whatsapp/send": {
      "post": {
        "operationId": "sendWhatsAppMessage",
        "tags": [
          "WhatsApp"
        ],
        "summary": "Enviar un mensaje de sesión (ventana de 24 h)",
        "description": "Requiere whatsapp:send o *. Envía texto, imagen, documento, audio o video por la Cloud API de Meta con la integración de WhatsApp de la organización (POST /{phone-number-id}/messages). Solo sirve dentro de la ventana de 24 h abierta por el último mensaje del cliente; para iniciar conversación o fuera de ventana usar /whatsapp/send-template. Al enviar: busca la conversación del número (o la crea en la primera etapa del pipeline default, con metadata origen=api y el asignado por defecto de la organización), guarda el mensaje como outgoing atribuido al primer empleado de la organización, actualiza lastMessage y notifica por socket al chat. La ventana de 24 h se valida ANTES de llamar a Meta (desde el 05-sep-2026) y se mide desde el último mensaje ENTRANTE del contacto (lastInboundTimestamp; conversaciones antiguas sin ese campo caen a lastMessageTimestamp): si pasaron más de 24 h responde 400 y no se envía nada. Un saliente reciente NO reabre la ventana. Sin conversación previa, o sin timestamp, se permite el envío. Si Meta rechaza, su status HTTP y su cuerpo de error se reenvían tal cual (ver respuesta default).",
        "x-required-permissions": [
          "whatsapp:send"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WhatsAppSendRequest"
              },
              "examples": {
                "text": {
                  "summary": "Texto dentro de la ventana de 24 h.",
                  "value": {
                    "to": "573001234567",
                    "message": "Hola",
                    "organizationId": "6814ef02e3de1af46109d105"
                  }
                },
                "imageReply": {
                  "summary": "Imagen con pie, citando un mensaje entrante.",
                  "value": {
                    "to": "573001234567",
                    "organizationId": "6814ef02e3de1af46109d105",
                    "type": "image",
                    "mediaUrl": "https://example.com/pedido.jpg",
                    "caption": "Así quedaría tu manilla",
                    "replyToMessageId": "507f1f77bcf86cd799439072"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Mensaje aceptado por Meta y registrado en la conversación. No responde 201.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WhatsAppSendResult"
                },
                "examples": {
                  "sent": {
                    "value": {
                      "success": true,
                      "message": "Mensaje enviado exitosamente",
                      "data": {
                        "messageId": "wamid.HBgMNTczMDAxMjM0NTY3FQIAERgSMUExQjJDM0Q0RTVGNkc3SDhJAA==",
                        "conversationId": "507f1f77bcf86cd799439051",
                        "message": {
                          "_id": "507f1f77bcf86cd799439073",
                          "user": "507f1f77bcf86cd799439001",
                          "organization": "6814ef02e3de1af46109d105",
                          "from": "573100000000",
                          "to": "573001234567",
                          "message": "Hola",
                          "direction": "outgoing",
                          "type": "text",
                          "mediaUrl": "",
                          "timestamp": "2026-09-05T16:40:00.000Z",
                          "messageId": "wamid.HBgMNTczMDAxMjM0NTY3FQIAERgSMUExQjJDM0Q0RTVGNkc3SDhJAA==",
                          "conversation": "507f1f77bcf86cd799439051",
                          "__v": 0
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Falta to, falta message en text, tipo no soportado, falta mediaUrl en medios, URL de Google Drive, organización sin empleados, credenciales de WhatsApp incompletas, mensaje citado sin wamid, conversación fuera de la ventana de 24 h, o pipeline default ausente/sin etapas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WhatsAppError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "success": false,
                      "message": "Faltan parámetros obligatorios: to"
                    }
                  },
                  "textRequired": {
                    "value": {
                      "success": false,
                      "message": "El mensaje de texto es requerido para tipo 'text'"
                    }
                  },
                  "unsupportedType": {
                    "value": {
                      "success": false,
                      "message": "Tipo de mensaje no soportado. Tipos válidos: text, image, document, audio, video"
                    }
                  },
                  "mediaRequired": {
                    "value": {
                      "success": false,
                      "message": "URL de imagen requerida para tipo 'image'"
                    }
                  },
                  "googleDrive": {
                    "value": {
                      "success": false,
                      "message": "WhatsApp API no acepta URLs de Google Drive directamente. Usa una URL directa al archivo.",
                      "suggestion": "Sube el archivo a un servidor web o usa un servicio como AWS S3, Cloudinary, etc."
                    }
                  },
                  "noEmployees": {
                    "value": {
                      "success": false,
                      "message": "La organización no tiene empleados; no hay usuario al cual atribuir el mensaje"
                    }
                  },
                  "credentials": {
                    "value": {
                      "success": false,
                      "message": "Credenciales de WhatsApp incompletas"
                    }
                  },
                  "replyNoWamid": {
                    "value": {
                      "success": false,
                      "message": "El mensaje original no tiene un messageId de WhatsApp válido"
                    }
                  },
                  "windowClosed": {
                    "summary": "Fuera de la ventana de 24 h (usar send-template).",
                    "value": {
                      "success": false,
                      "message": "No se puede enviar mensajes después de 24 horas del último mensaje. Meta WhatsApp Business API no permite mensajes fuera de la ventana de 24 horas.",
                      "details": {
                        "lastMessageTime": "2026-09-03T10:00:00.000Z",
                        "hoursDifference": 54.67,
                        "maxAllowedHours": 24
                      }
                    }
                  },
                  "noDefaultPipeline": {
                    "value": {
                      "success": false,
                      "message": "No se encontró un pipeline por defecto para la organización"
                    }
                  },
                  "noStages": {
                    "value": {
                      "success": false,
                      "message": "El pipeline por defecto no tiene etapas configuradas"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "description": "Token sin el permiso whatsapp:send, organizationId del body distinto al del token, o el contacto pidió no ser contactado por WhatsApp (`WHATSAPP_OPTED_OUT`; se reactiva desde la ficha del contacto en el CRM).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WhatsAppError"
                },
                "examples": {
                  "orgMismatch": {
                    "value": {
                      "success": false,
                      "message": "organizationId no corresponde a la organización del token"
                    }
                  },
                  "optedOut": {
                    "value": {
                      "success": false,
                      "message": "El contacto pidió no ser contactado por WhatsApp",
                      "code": "WHATSAPP_OPTED_OUT"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Organización inexistente, sin integración de WhatsApp, o replyToMessageId que no existe en esa organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WhatsAppError"
                },
                "examples": {
                  "organization": {
                    "value": {
                      "success": false,
                      "message": "Organización no encontrada"
                    }
                  },
                  "integration": {
                    "value": {
                      "success": false,
                      "message": "Integración de WhatsApp no encontrada para esta organización"
                    }
                  },
                  "replyTo": {
                    "value": {
                      "success": false,
                      "message": "El mensaje al que intentas responder no existe o no pertenece a esta organización"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Meta respondió sin messages[0].id, falló la lectura del mensaje citado, o error interno (error lleva el texto de la excepción).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WhatsAppError"
                },
                "examples": {
                  "noMessageId": {
                    "value": {
                      "success": false,
                      "message": "Error al enviar mensaje a WhatsApp"
                    }
                  },
                  "replyLookup": {
                    "value": {
                      "success": false,
                      "message": "Error al obtener el mensaje original"
                    }
                  },
                  "internal": {
                    "value": {
                      "success": false,
                      "message": "Error interno del servidor",
                      "error": "connect ECONNREFUSED"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "description": "Error devuelto por la Cloud API de Meta: se reenvía su mismo status HTTP (400 en la mayoría de los casos; 401 si el access token de la integración venció) con el cuerpo de Meta en error y el request enviado en details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WhatsAppError"
                },
                "examples": {
                  "meta": {
                    "summary": "Meta rechazó el envío; su status HTTP se reenvía tal cual.",
                    "value": {
                      "success": false,
                      "message": "Error de WhatsApp API",
                      "error": {
                        "error": {
                          "message": "(#131030) Recipient phone number not in allowed list",
                          "type": "OAuthException",
                          "code": 131030,
                          "fbtrace_id": "AbCdEfGhIjK"
                        }
                      },
                      "details": {
                        "status": 400,
                        "url": "https://graph.facebook.com/v25.0/123456789012345/messages",
                        "payload": "{\"messaging_product\":\"whatsapp\",\"to\":\"573001234567\",\"text\":{\"body\":\"Hola\"}}"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/whatsapp/send-template": {
      "post": {
        "operationId": "sendWhatsAppTemplate",
        "tags": [
          "WhatsApp"
        ],
        "summary": "Enviar una plantilla aprobada (marketing / fuera de ventana)",
        "description": "Requiere whatsapp:send o *. Envía una plantilla aprobada en Meta (type template) con la integración de la organización del token; es la única forma de escribirle a un número sin ventana de 24 h abierta. Valida to, assignTo y stageId ANTES de llamar a Meta. Después del envío, todo es best-effort y nunca revierte el mensaje: (1) resuelve el texto real del BODY de la plantilla consultando Meta y sustituyendo {{n}} con los parameters de body, y lo guarda como mensaje outgoing en la conversación del número (creándola en el pipeline default con metadata origen=outbound_template, en stageId o la primera etapa, asignada al dueño del token o al agente IA auto-asignable si assignTo=agent); (2) crea el contacto si no existe (mínimo con mobile) y aplica el bloque contact; (3) registra el MarketingEvent whatsapp_sent en el contacto. Si Meta rechaza, registra whatsapp_failed en el contacto (si existe) y reenvía el status de Meta. Los warnings de la respuesta dicen qué faltó (segmentación, etapa, agente IA). El tool MCP send_whatsapp_template antepone un preview sin efectos (confirm=false) antes de llamar a esta ruta.",
        "x-required-permissions": [
          "whatsapp:send"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WhatsAppSendTemplateRequest"
              },
              "examples": {
                "minimal": {
                  "summary": "Solo plantilla e idioma por defecto (es).",
                  "value": {
                    "to": "573001234567",
                    "templateName": "promo_bienvenida"
                  }
                },
                "full": {
                  "summary": "Con variables, etapa inicial, sin agente IA y segmentando el contacto.",
                  "value": {
                    "to": "573001234567",
                    "templateName": "promo_bienvenida",
                    "language": "es",
                    "components": [
                      {
                        "type": "body",
                        "parameters": [
                          {
                            "type": "text",
                            "text": "Ana"
                          }
                        ]
                      }
                    ],
                    "stageId": "507f1f77bcf86cd799439061",
                    "assignTo": "human",
                    "contact": {
                      "firstName": "Ana",
                      "lastName": "Ejemplo",
                      "company": "Microm S.A.S",
                      "companyType": "Eventos",
                      "source": "feria-corferias",
                      "city": "Medellín"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Plantilla aceptada por Meta. data resume qué se registró en el CRM y warnings lo que conviene revisar.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WhatsAppSendTemplateResult"
                },
                "examples": {
                  "sent": {
                    "value": {
                      "success": true,
                      "message": "Plantilla enviada exitosamente",
                      "data": {
                        "messageId": "wamid.HBgMNTczMDAxMjM0NTY3FQIAERgSNEY1RzZIN0k4SjlLMEwxTQA=",
                        "to": "573001234567",
                        "templateName": "promo_bienvenida",
                        "language": "es",
                        "contactFound": true,
                        "contactId": "507f1f77bcf86cd799439011",
                        "contactCreated": true,
                        "contactUpdatedFields": [
                          "firstName",
                          "lastName",
                          "company",
                          "companyType",
                          "source",
                          "city"
                        ],
                        "contactMissingFields": [],
                        "marketingEventLogged": true,
                        "conversationMessageLogged": true,
                        "conversation": {
                          "id": "507f1f77bcf86cd799439052",
                          "created": true,
                          "stageId": "507f1f77bcf86cd799439061",
                          "stageName": "Lead",
                          "assignedAgent": null
                        }
                      },
                      "warnings": [
                        "Contacto nuevo creado con el número 573001234567.",
                        "Conversación nueva creada en la etapa \"Lead\" y asignada al dueño del token (sin agente IA)."
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Faltan to/templateName, número inválido, assignTo fuera de human/agent, stageId inválido o fuera del pipeline default, o credenciales de WhatsApp incompletas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WhatsAppError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "success": false,
                      "message": "Faltan parámetros obligatorios: to, templateName",
                      "code": "MISSING_PARAMS"
                    }
                  },
                  "phone": {
                    "value": {
                      "success": false,
                      "message": "Número de teléfono inválido. Debe tener al menos 10 dígitos y no debe empezar con '+'.",
                      "code": "INVALID_PHONE",
                      "hint": "Ejemplo válido: 573001234567"
                    }
                  },
                  "assignTo": {
                    "value": {
                      "success": false,
                      "message": "assignTo inválido. Valores permitidos: 'human', 'agent'.",
                      "code": "INVALID_ASSIGN_TO"
                    }
                  },
                  "stageNotObjectId": {
                    "value": {
                      "success": false,
                      "message": "stageId inválido (no es un ObjectId).",
                      "code": "INVALID_STAGE"
                    }
                  },
                  "stageNotInPipeline": {
                    "value": {
                      "success": false,
                      "message": "stageId no pertenece al pipeline default de conversaciones.",
                      "code": "INVALID_STAGE",
                      "hint": "Usa GET /api/conversations/config (o list_pipeline_stages en MCP) para ver las etapas disponibles."
                    }
                  },
                  "credentials": {
                    "value": {
                      "success": false,
                      "message": "Credenciales de WhatsApp incompletas (numberIdIdentifier o accessToken faltantes)",
                      "code": "INCOMPLETE_CREDENTIALS"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "description": "Token sin el permiso whatsapp:send, o el contacto pidió no ser contactado por WhatsApp (`WHATSAPP_OPTED_OUT`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WhatsAppError"
                },
                "examples": {
                  "optedOut": {
                    "value": {
                      "success": false,
                      "message": "El contacto pidió no ser contactado por WhatsApp",
                      "code": "WHATSAPP_OPTED_OUT"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "La organización del token no tiene integración de WhatsApp.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WhatsAppError"
                },
                "example": {
                  "success": false,
                  "message": "Integración de WhatsApp no encontrada para esta organización",
                  "code": "WHATSAPP_INTEGRATION_NOT_FOUND"
                }
              }
            }
          },
          "500": {
            "description": "Error interno (error lleva el texto de la excepción).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WhatsAppError"
                },
                "example": {
                  "success": false,
                  "message": "Error interno del servidor",
                  "error": "connect ECONNREFUSED"
                }
              }
            }
          },
          "default": {
            "description": "Meta rechazó la plantilla (nombre/idioma inexistente, variables que no cuadran, número no permitido, token vencido…): se reenvía su mismo status HTTP con code META_API_ERROR y el cuerpo de Meta en error. contactFound indica si se registró whatsapp_failed en el contacto.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WhatsAppError"
                },
                "examples": {
                  "meta": {
                    "value": {
                      "success": false,
                      "message": "Error al enviar la plantilla en Meta WhatsApp API",
                      "code": "META_API_ERROR",
                      "error": {
                        "error": {
                          "message": "(#132001) Template name does not exist in the translation",
                          "type": "OAuthException",
                          "code": 132001,
                          "fbtrace_id": "AbCdEfGhIjK"
                        }
                      },
                      "contactFound": true
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/whatsapp/templates": {
      "get": {
        "operationId": "listWhatsAppTemplates",
        "tags": [
          "WhatsApp"
        ],
        "summary": "Listar plantillas de la cuenta de WhatsApp Business",
        "description": "Requiere whatsapp:send o * (no existe permiso de solo lectura para este módulo). Proxy sin caché a GET /{waba-id}/message_templates de la Graph API de Meta con las credenciales de la integración: devuelve todas las plantillas de la WABA (aprobadas, pendientes, rechazadas) tal como las entrega Meta, envueltas en data. No acepta parámetros de filtro ni paginación; la primera página de Meta (por defecto 25) es lo que se devuelve. Con esto se obtienen templateName, language y la estructura de components para /whatsapp/send-template. Ojo: sin integración responde 400, no 404.",
        "x-required-permissions": [
          "whatsapp:send"
        ],
        "responses": {
          "200": {
            "description": "Plantillas de la WABA (respuesta cruda de Meta en data).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WhatsAppTemplateListResult"
                },
                "examples": {
                  "templates": {
                    "value": {
                      "success": true,
                      "data": {
                        "data": [
                          {
                            "name": "promo_bienvenida",
                            "parameter_format": "POSITIONAL",
                            "components": [
                              {
                                "type": "HEADER",
                                "format": "TEXT",
                                "text": "Hola {{1}}",
                                "example": {
                                  "header_text": [
                                    "Ana"
                                  ]
                                }
                              },
                              {
                                "type": "BODY",
                                "text": "Gracias por escribirnos, {{1}}. ¿Te enviamos la cotización de tus manillas?",
                                "example": {
                                  "body_text": [
                                    [
                                      "Ana"
                                    ]
                                  ]
                                }
                              },
                              {
                                "type": "FOOTER",
                                "text": "Manillas de Control"
                              },
                              {
                                "type": "BUTTONS",
                                "buttons": [
                                  {
                                    "type": "QUICK_REPLY",
                                    "text": "Sí, envíamela"
                                  }
                                ]
                              }
                            ],
                            "language": "es",
                            "status": "APPROVED",
                            "category": "MARKETING",
                            "id": "1234567890123456"
                          }
                        ],
                        "paging": {
                          "cursors": {
                            "before": "MAZDZD",
                            "after": "MjQZD"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "La organización no tiene integración de WhatsApp o le faltan whatsappAccountBusinessIdentifier/accessToken.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WhatsAppError"
                },
                "examples": {
                  "integration": {
                    "value": {
                      "success": false,
                      "message": "Integración de WhatsApp no encontrada",
                      "code": "WHATSAPP_INTEGRATION_NOT_FOUND"
                    }
                  },
                  "credentials": {
                    "value": {
                      "success": false,
                      "message": "Credenciales de WhatsApp incompletas",
                      "code": "WHATSAPP_CREDENTIALS_INCOMPLETE"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "description": "Error interno (no proveniente de Meta).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WhatsAppError"
                },
                "example": {
                  "success": false,
                  "message": "Error interno del servidor",
                  "code": "INTERNAL_SERVER_ERROR"
                }
              }
            }
          },
          "default": {
            "description": "Meta respondió con error (token vencido, WABA inválida…): se reenvía su status HTTP con code META_API_ERROR y el cuerpo de Meta en details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WhatsAppError"
                },
                "examples": {
                  "meta": {
                    "value": {
                      "success": false,
                      "message": "Error obteniendo plantillas de WhatsApp",
                      "code": "META_API_ERROR",
                      "details": {
                        "error": {
                          "message": "Error validating access token",
                          "type": "OAuthException",
                          "code": 190,
                          "fbtrace_id": "AbCdEfGhIjK"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/projects": {
      "get": {
        "operationId": "listProjects",
        "tags": [
          "Projects"
        ],
        "summary": "Listar proyectos",
        "description": "Requiere projects:read o *. Filtra por la organización del API Token. No incluye taskCount (solo GET /projects/{id}). search se trata como literal (escapado). status es coincidencia exacta con el texto guardado. createdFrom/createdTo aceptan cualquier fecha parseable; un valor inválido responde 400 (desde el 05-sep-2026).",
        "x-required-permissions": [
          "projects:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Coincidencia exacta con el status guardado (texto libre)."
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Coincidencia parcial en name, sin distinguir mayúsculas (regex sin escapar)."
          },
          {
            "name": "createdFrom",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "createdAt ≥ este valor (inclusive)."
          },
          {
            "name": "createdTo",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "createdAt ≤ este valor (inclusive)."
          },
          {
            "name": "sortBy",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "createdAt",
                "updatedAt",
                "name",
                "startDate",
                "endDate"
              ],
              "default": "createdAt"
            },
            "description": "Un valor fuera del enum cae a createdAt sin error."
          },
          {
            "name": "sortOrder",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            },
            "description": "Cualquier valor distinto de asc ordena desc."
          }
        ],
        "responses": {
          "200": {
            "description": "Proyectos y paginación; data puede estar vacío.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectListResult"
                },
                "examples": {
                  "list": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "507f1f77bcf86cd799439101",
                          "name": "Evento Feria Medellín 2026",
                          "description": "Manillas y acreditación para la feria",
                          "status": "En Progreso",
                          "color": "#3b82f6",
                          "budget": 1500000,
                          "startDate": "2026-09-01T00:00:00.000Z",
                          "endDate": "2026-10-15T00:00:00.000Z",
                          "owner": {
                            "id": "507f1f77bcf86cd799439001",
                            "firstName": "Ana",
                            "lastName": "Ejemplo",
                            "email": "ana@example.com"
                          },
                          "createdAt": "2026-09-05T15:00:00.000Z",
                          "updatedAt": "2026-09-05T15:00:00.000Z"
                        },
                        {
                          "id": "507f1f77bcf86cd799439102",
                          "name": "Rediseño web",
                          "description": "",
                          "status": null,
                          "color": null,
                          "budget": null,
                          "startDate": null,
                          "endDate": null,
                          "owner": null,
                          "createdAt": "2026-08-20T13:45:00.000Z",
                          "updatedAt": "2026-08-20T13:45:00.000Z"
                        }
                      ],
                      "pagination": {
                        "total": 2,
                        "page": 1,
                        "limit": 100,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "description": "Error interno (incluye fallos de cast del modelo y fechas de filtro no parseables).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectError"
                },
                "example": {
                  "success": false,
                  "error": "Internal server error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createProject",
        "tags": [
          "Projects"
        ],
        "summary": "Crear un proyecto",
        "description": "Requiere projects:write o *. NO aplica límite de plan (no hay planLimitsMiddleware). Solo name es obligatorio. budget acepta número o cadena numérica ≥ 0; null o '' se guardan como 0. status y color son texto libre. Responde 201 con el proyecto en el formato de GET lista (sin taskCount) y owner = usuario dueño del token.",
        "x-required-permissions": [
          "projects:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectCreate"
              },
              "examples": {
                "full": {
                  "summary": "Proyecto completo con fechas y presupuesto.",
                  "value": {
                    "name": "Evento Feria Medellín 2026",
                    "description": "Manillas y acreditación para la feria",
                    "status": "En Progreso",
                    "color": "#3b82f6",
                    "budget": 1500000,
                    "startDate": "2026-09-01T00:00:00.000Z",
                    "endDate": "2026-10-15T00:00:00.000Z"
                  }
                },
                "minimal": {
                  "summary": "Solo el nombre.",
                  "value": {
                    "name": "Rediseño web"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Proyecto creado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectResult"
                },
                "examples": {
                  "created": {
                    "summary": "Respuesta al crear solo con name (defaults aplicados).",
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439102",
                        "name": "Rediseño web",
                        "description": "",
                        "status": null,
                        "color": null,
                        "budget": null,
                        "startDate": null,
                        "endDate": null,
                        "owner": {
                          "id": "507f1f77bcf86cd799439001",
                          "firstName": "Ana",
                          "lastName": "Ejemplo",
                          "email": "ana@example.com"
                        },
                        "createdAt": "2026-09-05T15:00:00.000Z",
                        "updatedAt": "2026-09-05T15:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "name vacío o ausente, fecha no parseable, startDate posterior a endDate, o budget negativo/no numérico.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectError"
                },
                "examples": {
                  "missingName": {
                    "value": {
                      "success": false,
                      "error": "El campo 'name' es requerido y no puede estar vacío"
                    }
                  },
                  "invalidDate": {
                    "value": {
                      "success": false,
                      "error": "El campo 'startDate' no es una fecha válida"
                    }
                  },
                  "dateOrder": {
                    "value": {
                      "success": false,
                      "error": "'startDate' no puede ser posterior a 'endDate'"
                    }
                  },
                  "budget": {
                    "value": {
                      "success": false,
                      "error": "El campo 'budget' debe ser un número positivo"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1projects/get/responses/500"
          }
        }
      }
    },
    "/projects/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB del proyecto (campo id de las respuestas).",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "507f1f77bcf86cd799439101"
        }
      ],
      "get": {
        "operationId": "getProjectById",
        "tags": [
          "Projects"
        ],
        "summary": "Consultar un proyecto por ID",
        "description": "Requiere projects:read o *. El proyecto debe pertenecer a la organización del token. Es la única operación que incluye taskCount (tareas de la organización con este projectId).",
        "x-required-permissions": [
          "projects:read"
        ],
        "responses": {
          "200": {
            "description": "Proyecto encontrado, con taskCount.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectDetailResult"
                },
                "examples": {
                  "found": {
                    "summary": "Proyecto con owner poblado y 3 tareas.",
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439101",
                        "name": "Evento Feria Medellín 2026",
                        "description": "Manillas y acreditación para la feria",
                        "status": "En Progreso",
                        "color": "#3b82f6",
                        "budget": 1500000,
                        "startDate": "2026-09-01T00:00:00.000Z",
                        "endDate": "2026-10-15T00:00:00.000Z",
                        "owner": {
                          "id": "507f1f77bcf86cd799439001",
                          "firstName": "Ana",
                          "lastName": "Ejemplo",
                          "email": "ana@example.com"
                        },
                        "createdAt": "2026-09-05T15:00:00.000Z",
                        "updatedAt": "2026-09-05T15:00:00.000Z",
                        "taskCount": 3
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID de proyecto inválido (no es un ObjectId).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectError"
                },
                "example": {
                  "success": false,
                  "error": "Invalid project ID"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Proyecto inexistente o no perteneciente a la organización del token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectError"
                },
                "example": {
                  "success": false,
                  "error": "Project not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1projects/get/responses/500"
          }
        }
      },
      "put": {
        "operationId": "updateProjectById",
        "tags": [
          "Projects"
        ],
        "summary": "Actualizar parcialmente un proyecto",
        "description": "Requiere projects:write o *. Solo se tocan los campos enviados; un body sin ninguno de los campos permitidos responde 400. null limpia un campo (budget null lo deja null, no 0). La validación startDate ≤ endDate compara contra lo guardado cuando solo se envía una de las dos (desde el 05-sep-2026). No hay validación de tipo para description/status/color: un objeto donde va una cadena falla en el modelo y responde 500. Responde el proyecto en el formato de GET lista (sin taskCount).",
        "x-required-permissions": [
          "projects:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectUpdate"
              },
              "examples": {
                "status": {
                  "summary": "Cambiar estado y presupuesto.",
                  "value": {
                    "status": "Completado",
                    "budget": 1800000
                  }
                },
                "clear": {
                  "summary": "Limpiar fechas y color.",
                  "value": {
                    "startDate": null,
                    "endDate": null,
                    "color": null
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Proyecto actualizado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectResult"
                },
                "examples": {
                  "updated": {
                    "summary": "Tras PUT con status y budget nuevos; updatedAt avanza.",
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439101",
                        "name": "Evento Feria Medellín 2026",
                        "description": "Manillas y acreditación para la feria",
                        "status": "Completado",
                        "color": "#3b82f6",
                        "budget": 1800000,
                        "startDate": "2026-09-01T00:00:00.000Z",
                        "endDate": "2026-10-15T00:00:00.000Z",
                        "owner": {
                          "id": "507f1f77bcf86cd799439001",
                          "firstName": "Ana",
                          "lastName": "Ejemplo",
                          "email": "ana@example.com"
                        },
                        "createdAt": "2026-09-05T15:00:00.000Z",
                        "updatedAt": "2026-09-06T10:30:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID inválido, body sin campos permitidos, name vacío, fecha inválida, orden de fechas o budget inválido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectError"
                },
                "examples": {
                  "invalidId": {
                    "value": {
                      "success": false,
                      "error": "Invalid project ID"
                    }
                  },
                  "noFields": {
                    "value": {
                      "success": false,
                      "error": "No se proporcionaron campos para actualizar"
                    }
                  },
                  "emptyName": {
                    "value": {
                      "success": false,
                      "error": "El campo 'name' no puede estar vacío"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1projects~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1projects/get/responses/500"
          }
        }
      },
      "delete": {
        "operationId": "deleteProjectById",
        "tags": [
          "Projects"
        ],
        "summary": "Eliminar un proyecto y sus tareas",
        "description": "Requiere projects:delete o *. Borra el proyecto Y TODAS sus tareas en cascada (las de la organización con este projectId); no es reversible. Responde 200 con el número de tareas borradas, no con el id ni 204.",
        "x-required-permissions": [
          "projects:delete"
        ],
        "responses": {
          "200": {
            "description": "Proyecto eliminado; data.deletedTasksCount indica cuántas tareas cayeron con él.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectDeleteResult"
                },
                "examples": {
                  "deleted": {
                    "summary": "Proyecto borrado junto con sus 3 tareas.",
                    "value": {
                      "success": true,
                      "data": {
                        "deletedTasksCount": 3
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1projects~1{id}/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1projects~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1projects/get/responses/500"
          }
        }
      }
    },
    "/projects/{id}/tasks": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB del proyecto al que pertenecen las tareas.",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "507f1f77bcf86cd799439101"
        }
      ],
      "get": {
        "operationId": "listProjectTasks",
        "tags": [
          "Tasks"
        ],
        "summary": "Listar las tareas de un proyecto",
        "description": "Requiere tasks:read o * (NO projects:read). Es el mismo controller que GET /tasks con projectId fijado por la ruta; acepta los mismos filtros. NO comprueba que el proyecto exista ni que sea de la organización: un id ajeno o inexistente responde 200 con data vacío, nunca 404. Solo el ObjectId inválido responde 400.",
        "x-required-permissions": [
          "tasks:read"
        ],
        "parameters": [
          {
            "$ref": "#/paths/~1tasks/get/parameters/1"
          },
          {
            "$ref": "#/paths/~1tasks/get/parameters/2"
          },
          {
            "$ref": "#/paths/~1tasks/get/parameters/3"
          },
          {
            "$ref": "#/paths/~1tasks/get/parameters/4"
          },
          {
            "$ref": "#/paths/~1tasks/get/parameters/5"
          },
          {
            "$ref": "#/paths/~1tasks/get/parameters/6"
          },
          {
            "$ref": "#/paths/~1tasks/get/parameters/7"
          },
          {
            "$ref": "#/paths/~1tasks/get/parameters/8"
          },
          {
            "$ref": "#/paths/~1tasks/get/parameters/9"
          },
          {
            "$ref": "#/paths/~1tasks/get/parameters/10"
          }
        ],
        "responses": {
          "200": {
            "description": "Tareas del proyecto y paginación; data vacío si el proyecto no tiene tareas o no existe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskListResult"
                },
                "examples": {
                  "list": {
                    "$ref": "#/paths/~1tasks/get/responses/200/content/application~1json/examples/list"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1tasks/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1tasks/get/responses/500"
          }
        }
      },
      "post": {
        "operationId": "createProjectTask",
        "tags": [
          "Tasks"
        ],
        "summary": "Crear una tarea dentro de un proyecto",
        "description": "Requiere tasks:write o * (NO projects:write). Mismo controller que POST /tasks con projectId tomado de la ruta (el projectId del body se ignora). Aquí sí se valida que el proyecto exista en la organización: si no, responde 400 (no 404).",
        "x-required-permissions": [
          "tasks:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskCreate"
              },
              "examples": {
                "inProject": {
                  "value": {
                    "title": "Diseñar arte de la manilla",
                    "dueDate": "2026-09-15T12:00:00.000Z",
                    "responsibleId": "507f1f77bcf86cd799439001",
                    "priority": "Media",
                    "tags": [
                      "diseño"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Tarea creada en el proyecto.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskResult"
                },
                "examples": {
                  "created": {
                    "summary": "Creada por POST /projects/{id}/tasks; project viene de la ruta.",
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439203",
                        "title": "Diseñar arte de la manilla",
                        "status": "No Iniciado",
                        "priority": "Media",
                        "dueDate": "2026-09-15T12:00:00.000Z",
                        "notes": null,
                        "description": null,
                        "budget": null,
                        "timeline": null,
                        "tags": [
                          "diseño"
                        ],
                        "project": {
                          "id": "507f1f77bcf86cd799439101",
                          "name": "Evento Feria Medellín 2026",
                          "color": "#3b82f6"
                        },
                        "responsible": {
                          "id": "507f1f77bcf86cd799439001",
                          "firstName": "Ana",
                          "lastName": "Ejemplo",
                          "email": "ana@example.com"
                        },
                        "contact": null,
                        "createdAt": "2026-09-05T15:00:00.000Z",
                        "updatedAt": "2026-09-05T15:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1tasks/post/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1tasks/get/responses/500"
          }
        }
      }
    },
    "/tasks": {
      "get": {
        "operationId": "listTasks",
        "tags": [
          "Tasks"
        ],
        "summary": "Listar tareas",
        "description": "Requiere tasks:read o *. Filtra por la organización del API Token y pobla project, responsible y contact. search se trata como literal (escapado). createdFrom/createdTo aceptan cualquier fecha parseable; un valor inválido responde 400. projectId (en query o en /projects/{id}/tasks) debe ser un proyecto de la organización: ajeno o inexistente responde 404 (desde el 05-sep-2026).",
        "x-required-permissions": [
          "tasks:read"
        ],
        "parameters": [
          {
            "name": "projectId",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^[a-fA-F0-9]{24}$"
            },
            "description": "Tareas del proyecto; un ObjectId inválido responde 400 y un proyecto inexistente o ajeno 404."
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "No Iniciado",
                "En Progreso",
                "Revisión",
                "Completado"
              ]
            },
            "description": "Un valor fuera del enum responde 400."
          },
          {
            "name": "priority",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "Alta",
                "Media",
                "Baja"
              ]
            },
            "description": "Un valor fuera del enum responde 400."
          },
          {
            "name": "contactId",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^[a-fA-F0-9]{24}$"
            },
            "description": "Tareas asociadas a ese contacto; un ObjectId inválido responde 400."
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Coincidencia parcial en title, sin distinguir mayúsculas (regex sin escapar)."
          },
          {
            "name": "createdFrom",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "createdAt ≥ este valor (inclusive)."
          },
          {
            "name": "createdTo",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "createdAt ≤ este valor (inclusive)."
          },
          {
            "name": "sortBy",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "createdAt",
                "updatedAt",
                "dueDate",
                "title",
                "priority"
              ],
              "default": "createdAt"
            },
            "description": "Un valor fuera del enum cae a createdAt sin error. priority ordena por importancia (Alta > Media > Baja, desde el 05-sep-2026) con desempate por createdAt desc."
          },
          {
            "name": "sortOrder",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            },
            "description": "Cualquier valor distinto de asc ordena desc."
          }
        ],
        "responses": {
          "200": {
            "description": "Tareas y paginación; data puede estar vacío.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskListResult"
                },
                "examples": {
                  "list": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "507f1f77bcf86cd799439201",
                          "title": "Enviar cotización de manillas",
                          "status": "En Progreso",
                          "priority": "Alta",
                          "dueDate": "2026-09-10T15:00:00.000Z",
                          "notes": "Confirmar cantidades antes de enviar",
                          "description": "Cotizar 5.000 manillas Tyvek",
                          "budget": 200000,
                          "timeline": "Semana 2",
                          "tags": [
                            "ventas",
                            "feria"
                          ],
                          "project": {
                            "id": "507f1f77bcf86cd799439101",
                            "name": "Evento Feria Medellín 2026",
                            "color": "#3b82f6"
                          },
                          "responsible": {
                            "id": "507f1f77bcf86cd799439001",
                            "firstName": "Ana",
                            "lastName": "Ejemplo",
                            "email": "ana@example.com"
                          },
                          "contact": {
                            "id": "507f1f77bcf86cd799439011",
                            "fullName": "Carlos Pérez",
                            "company": "Eventos SAS",
                            "email": "carlos@eventos.example.com",
                            "mobile": "573000000000"
                          },
                          "createdAt": "2026-09-05T15:00:00.000Z",
                          "updatedAt": "2026-09-05T15:00:00.000Z"
                        },
                        {
                          "id": "507f1f77bcf86cd799439202",
                          "title": "Llamar al proveedor",
                          "status": "No Iniciado",
                          "priority": "Baja",
                          "dueDate": "2026-09-12T12:00:00.000Z",
                          "notes": null,
                          "description": null,
                          "budget": null,
                          "timeline": null,
                          "tags": [],
                          "project": null,
                          "responsible": null,
                          "contact": null,
                          "createdAt": "2026-08-20T13:45:00.000Z",
                          "updatedAt": "2026-08-20T13:45:00.000Z"
                        }
                      ],
                      "pagination": {
                        "total": 2,
                        "page": 1,
                        "limit": 100,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "projectId/contactId no son ObjectId, o status/priority fuera del enum.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskError"
                },
                "examples": {
                  "project": {
                    "value": {
                      "success": false,
                      "error": "Invalid project ID"
                    }
                  },
                  "status": {
                    "value": {
                      "success": false,
                      "error": "Estado inválido. Valores permitidos: No Iniciado, En Progreso, Revisión, Completado"
                    }
                  },
                  "priority": {
                    "value": {
                      "success": false,
                      "error": "Prioridad inválida. Valores permitidos: Alta, Media, Baja"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "description": "Error interno (incluye fallos de cast del modelo y fechas de filtro no parseables).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskError"
                },
                "example": {
                  "success": false,
                  "error": "Internal server error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createTask",
        "tags": [
          "Tasks"
        ],
        "summary": "Crear una tarea",
        "description": "Requiere tasks:write o *. NO aplica límite de plan. title, dueDate y responsibleId son obligatorios. projectId (opcional) debe existir en la organización; responsibleId y contactId solo se validan como ObjectId, no se comprueba que existan (responsible/contact vuelven null en la respuesta si no existen). budget null o '' se guarda como 0. Responde 201 con la tarea poblada.",
        "x-required-permissions": [
          "tasks:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskCreate"
              },
              "examples": {
                "full": {
                  "summary": "Tarea completa con proyecto y contacto.",
                  "value": {
                    "title": "Enviar cotización de manillas",
                    "dueDate": "2026-09-10T15:00:00.000Z",
                    "responsibleId": "507f1f77bcf86cd799439001",
                    "projectId": "507f1f77bcf86cd799439101",
                    "contactId": "507f1f77bcf86cd799439011",
                    "status": "En Progreso",
                    "priority": "Alta",
                    "description": "Cotizar 5.000 manillas Tyvek",
                    "notes": "Confirmar cantidades antes de enviar",
                    "budget": 200000,
                    "timeline": "Semana 2",
                    "tags": [
                      "ventas",
                      "feria"
                    ]
                  }
                },
                "minimal": {
                  "summary": "Solo los obligatorios (dueDate sin hora también pasa).",
                  "value": {
                    "title": "Llamar al proveedor",
                    "dueDate": "2026-09-12",
                    "responsibleId": "507f1f77bcf86cd799439001"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Tarea creada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskResult"
                },
                "examples": {
                  "created": {
                    "summary": "Respuesta al crear solo con title, dueDate y responsibleId (defaults aplicados).",
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439202",
                        "title": "Llamar al proveedor",
                        "status": "No Iniciado",
                        "priority": "Baja",
                        "dueDate": "2026-09-12T12:00:00.000Z",
                        "notes": null,
                        "description": null,
                        "budget": null,
                        "timeline": null,
                        "tags": [],
                        "project": null,
                        "responsible": {
                          "id": "507f1f77bcf86cd799439001",
                          "firstName": "Ana",
                          "lastName": "Ejemplo",
                          "email": "ana@example.com"
                        },
                        "contact": null,
                        "createdAt": "2026-09-05T15:00:00.000Z",
                        "updatedAt": "2026-09-05T15:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Falta title/dueDate/responsibleId, status/priority fuera del enum, proyecto inexistente, contactId inválido o budget inválido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskError"
                },
                "examples": {
                  "title": {
                    "value": {
                      "success": false,
                      "error": "El campo 'title' es requerido y no puede estar vacío"
                    }
                  },
                  "dueDate": {
                    "value": {
                      "success": false,
                      "error": "El campo 'dueDate' es requerido y debe ser una fecha válida"
                    }
                  },
                  "responsible": {
                    "value": {
                      "success": false,
                      "error": "El campo 'responsibleId' es requerido y debe ser un ID válido"
                    }
                  },
                  "project": {
                    "value": {
                      "success": false,
                      "error": "El proyecto especificado no existe o no pertenece a esta organización"
                    }
                  },
                  "budget": {
                    "value": {
                      "success": false,
                      "error": "El campo 'budget' debe ser un número positivo"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1tasks/get/responses/500"
          }
        }
      }
    },
    "/tasks/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB de la tarea (campo id de las respuestas).",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "507f1f77bcf86cd799439201"
        }
      ],
      "get": {
        "operationId": "getTaskById",
        "tags": [
          "Tasks"
        ],
        "summary": "Consultar una tarea por ID",
        "description": "Requiere tasks:read o *. La tarea debe pertenecer a la organización del token. Pobla project, responsible y contact.",
        "x-required-permissions": [
          "tasks:read"
        ],
        "responses": {
          "200": {
            "description": "Tarea encontrada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskResult"
                },
                "examples": {
                  "found": {
                    "summary": "Tarea con proyecto, responsable y contacto poblados.",
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439201",
                        "title": "Enviar cotización de manillas",
                        "status": "En Progreso",
                        "priority": "Alta",
                        "dueDate": "2026-09-10T15:00:00.000Z",
                        "notes": "Confirmar cantidades antes de enviar",
                        "description": "Cotizar 5.000 manillas Tyvek",
                        "budget": 200000,
                        "timeline": "Semana 2",
                        "tags": [
                          "ventas",
                          "feria"
                        ],
                        "project": {
                          "id": "507f1f77bcf86cd799439101",
                          "name": "Evento Feria Medellín 2026",
                          "color": "#3b82f6"
                        },
                        "responsible": {
                          "id": "507f1f77bcf86cd799439001",
                          "firstName": "Ana",
                          "lastName": "Ejemplo",
                          "email": "ana@example.com"
                        },
                        "contact": {
                          "id": "507f1f77bcf86cd799439011",
                          "fullName": "Carlos Pérez",
                          "company": "Eventos SAS",
                          "email": "carlos@eventos.example.com",
                          "mobile": "573000000000"
                        },
                        "createdAt": "2026-09-05T15:00:00.000Z",
                        "updatedAt": "2026-09-05T15:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID de tarea inválido (no es un ObjectId).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskError"
                },
                "example": {
                  "success": false,
                  "error": "Invalid task ID"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Tarea inexistente o no perteneciente a la organización del token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskError"
                },
                "example": {
                  "success": false,
                  "error": "Task not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1tasks/get/responses/500"
          }
        }
      },
      "put": {
        "operationId": "updateTaskById",
        "tags": [
          "Tasks"
        ],
        "summary": "Actualizar parcialmente una tarea",
        "description": "Requiere tasks:write o *. Solo se tocan los campos enviados; sin campos permitidos responde 400. status/priority se validan contra el enum siempre que la clave esté presente (null → 400). responsibleId no se puede limpiar (null → 400); projectId y contactId sí (null desvincula). dueDate es obligatoria: null, vacía o inválida responde 400. Cambiar dueDate o status borra dueDateReminderNotifiedAt: la tarea vuelve a entrar al recordatorio de vencimiento. Un valor de tipo incorrecto en un campo sin validación (p. ej. budget como objeto) responde 500.",
        "x-required-permissions": [
          "tasks:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskUpdate"
              },
              "examples": {
                "complete": {
                  "summary": "Marcar como completada.",
                  "value": {
                    "status": "Completado"
                  }
                },
                "reassign": {
                  "summary": "Cambiar responsable, fecha y desvincular del proyecto.",
                  "value": {
                    "responsibleId": "507f1f77bcf86cd799439002",
                    "dueDate": "2026-09-20T12:00:00.000Z",
                    "projectId": null
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tarea actualizada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskResult"
                },
                "examples": {
                  "updated": {
                    "summary": "Tras PUT con status Completado; updatedAt avanza.",
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439201",
                        "title": "Enviar cotización de manillas",
                        "status": "Completado",
                        "priority": "Alta",
                        "dueDate": "2026-09-10T15:00:00.000Z",
                        "notes": "Confirmar cantidades antes de enviar",
                        "description": "Cotizar 5.000 manillas Tyvek",
                        "budget": 200000,
                        "timeline": "Semana 2",
                        "tags": [
                          "ventas",
                          "feria"
                        ],
                        "project": {
                          "id": "507f1f77bcf86cd799439101",
                          "name": "Evento Feria Medellín 2026",
                          "color": "#3b82f6"
                        },
                        "responsible": {
                          "id": "507f1f77bcf86cd799439001",
                          "firstName": "Ana",
                          "lastName": "Ejemplo",
                          "email": "ana@example.com"
                        },
                        "contact": {
                          "id": "507f1f77bcf86cd799439011",
                          "fullName": "Carlos Pérez",
                          "company": "Eventos SAS",
                          "email": "carlos@eventos.example.com",
                          "mobile": "573000000000"
                        },
                        "createdAt": "2026-09-05T15:00:00.000Z",
                        "updatedAt": "2026-09-06T10:30:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID inválido, body sin campos permitidos, o validación de title/status/priority/dueDate/responsibleId/projectId/contactId/budget.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskError"
                },
                "examples": {
                  "invalidId": {
                    "value": {
                      "success": false,
                      "error": "Invalid task ID"
                    }
                  },
                  "noFields": {
                    "value": {
                      "success": false,
                      "error": "No se proporcionaron campos para actualizar"
                    }
                  },
                  "status": {
                    "value": {
                      "success": false,
                      "error": "Estado inválido. Valores permitidos: No Iniciado, En Progreso, Revisión, Completado"
                    }
                  },
                  "responsible": {
                    "value": {
                      "success": false,
                      "error": "Invalid responsibleId"
                    }
                  },
                  "project": {
                    "value": {
                      "success": false,
                      "error": "El proyecto especificado no existe o no pertenece a esta organización"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1tasks~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1tasks/get/responses/500"
          }
        }
      },
      "delete": {
        "operationId": "deleteTaskById",
        "tags": [
          "Tasks"
        ],
        "summary": "Eliminar una tarea",
        "description": "Requiere tasks:delete o *. Borra solo la tarea. Responde 200 con el id, no 204.",
        "x-required-permissions": [
          "tasks:delete"
        ],
        "responses": {
          "200": {
            "description": "Tarea eliminada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskDeleteResult"
                },
                "examples": {
                  "deleted": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439201"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1tasks~1{id}/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1tasks~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1tasks/get/responses/500"
          }
        }
      }
    },
    "/lists": {
      "get": {
        "operationId": "listContactLists",
        "tags": [
          "Lists"
        ],
        "summary": "Listar listas de contactos",
        "description": "Requiere contacts:read o * (las listas usan los permisos de contactos, no hay lists:*). Ordena por createdAt desc, sin sortBy. search se trata como literal (escapado). type con un valor distinto de static/dynamic se ignora. contactCount es real desde el 05-sep-2026 (se calcula en el servidor sin enviar los ids); en dinámicas es 0 porque no guardan contactIds.",
        "x-required-permissions": [
          "contacts:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          },
          {
            "name": "type",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "static",
                "dynamic"
              ]
            },
            "description": "Otro valor no filtra."
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Coincidencia parcial literal en name, sin distinguir mayúsculas."
          }
        ],
        "responses": {
          "200": {
            "description": "Listas y paginación; data puede estar vacío.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListPageResult"
                },
                "examples": {
                  "list": {
                    "summary": "Listado con contactCount real por lista.",
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "507f1f77bcf86cd799439301",
                          "name": "Clientes feria 2026",
                          "description": "Contactos captados en la feria",
                          "type": "static",
                          "filters": [],
                          "contactCount": 0,
                          "createdAt": "2026-09-05T15:00:00.000Z",
                          "updatedAt": "2026-09-05T15:00:00.000Z"
                        },
                        {
                          "id": "507f1f77bcf86cd799439302",
                          "name": "Contactos de Medellín",
                          "description": null,
                          "type": "dynamic",
                          "filters": [
                            {
                              "id": "filter-0",
                              "key": "city",
                              "operator": "equals",
                              "value": "Medellín"
                            }
                          ],
                          "contactCount": 0,
                          "createdAt": "2026-08-20T13:45:00.000Z",
                          "updatedAt": "2026-08-20T13:45:00.000Z"
                        }
                      ],
                      "pagination": {
                        "total": 2,
                        "page": 1,
                        "limit": 100,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "description": "Error interno del servidor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListError"
                },
                "example": {
                  "success": false,
                  "error": "Internal server error"
                }
              }
            }
          }
        }
      }
    },
    "/lists/static": {
      "post": {
        "operationId": "createStaticList",
        "tags": [
          "Lists"
        ],
        "summary": "Crear una lista estática",
        "description": "Requiere contacts:write o *. Aplica el límite contactGroups del plan (403 PLAN_LIMIT_REACHED con el contrato message/code de los middlewares). contactIds es opcional (máx. 2000, deduplicados); cada id debe ser un ObjectId de un contacto existente de la organización del token — si alguno no lo es responde 400 con invalidContactIds y no crea nada (desde el 05-sep-2026; antes se guardaba cualquier id). Responde 201 con contactCount real.",
        "x-required-permissions": [
          "contacts:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StaticListCreate"
              },
              "examples": {
                "withContacts": {
                  "value": {
                    "name": "Clientes feria 2026",
                    "description": "Contactos captados en la feria",
                    "contactIds": [
                      "507f1f77bcf86cd799439011",
                      "507f1f77bcf86cd799439012"
                    ]
                  }
                },
                "empty": {
                  "value": {
                    "name": "Lista vacía"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Lista creada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListDetailResult"
                },
                "examples": {
                  "created": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439301",
                        "name": "Clientes feria 2026",
                        "description": "Contactos captados en la feria",
                        "type": "static",
                        "filters": [],
                        "contactCount": 2,
                        "createdAt": "2026-09-05T15:00:00.000Z",
                        "updatedAt": "2026-09-05T15:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "name ausente/vacío, más de 2000 ids, un id que no es ObjectId, o ids que no son contactos de la organización (invalidContactIds).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListError"
                },
                "examples": {
                  "name": {
                    "value": {
                      "success": false,
                      "error": "El campo 'name' es requerido"
                    }
                  },
                  "tooMany": {
                    "value": {
                      "success": false,
                      "error": "Máximo 2000 contactos por lista"
                    }
                  },
                  "invalidId": {
                    "value": {
                      "success": false,
                      "error": "ID de contacto inválido: abc"
                    }
                  },
                  "foreign": {
                    "value": {
                      "success": false,
                      "error": "Contactos inexistentes o de otra organización",
                      "invalidContactIds": [
                        "507f1f77bcf86cd799439099"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/lists/dynamic": {
      "post": {
        "operationId": "createDynamicList",
        "tags": [
          "Lists"
        ],
        "summary": "Crear una lista dinámica",
        "description": "Requiere contacts:write o *. Aplica el límite contactGroups del plan. Exige al menos un filtro; a cada uno se le asigna id filter-N si no lo trae y value '' si falta. Un filtro sin key, sin operator o con operator fuera del catálogo responde 400 (desde el 05-sep-2026). La lista no materializa contactos: se evalúan en cada GET /lists/{id}/contacts.",
        "x-required-permissions": [
          "contacts:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DynamicListCreate"
              },
              "examples": {
                "byCity": {
                  "value": {
                    "name": "Contactos de Medellín",
                    "filters": [
                      {
                        "key": "city",
                        "operator": "equals",
                        "value": "Medellín"
                      }
                    ]
                  }
                },
                "hotLeads": {
                  "summary": "leadScore mayor a 50 y con email.",
                  "value": {
                    "name": "Leads calientes",
                    "filters": [
                      {
                        "key": "leadScore",
                        "operator": "greater_than",
                        "value": "50"
                      },
                      {
                        "key": "email",
                        "operator": "is not empty"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Lista creada (contactCount siempre 0).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListDetailResult"
                },
                "examples": {
                  "created": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439302",
                        "name": "Contactos de Medellín",
                        "description": null,
                        "type": "dynamic",
                        "filters": [
                          {
                            "id": "filter-0",
                            "key": "city",
                            "operator": "equals",
                            "value": "Medellín"
                          }
                        ],
                        "contactCount": 0,
                        "createdAt": "2026-09-05T15:00:00.000Z",
                        "updatedAt": "2026-09-05T15:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "name ausente/vacío o filters vacío/ausente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListError"
                },
                "examples": {
                  "name": {
                    "value": {
                      "success": false,
                      "error": "El campo 'name' es requerido"
                    }
                  },
                  "filters": {
                    "value": {
                      "success": false,
                      "error": "Se requiere al menos un filtro para listas dinámicas"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/lists/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB de la lista (campo id de las respuestas).",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "507f1f77bcf86cd799439301"
        }
      ],
      "get": {
        "operationId": "getContactList",
        "tags": [
          "Lists"
        ],
        "summary": "Consultar una lista por ID",
        "description": "Requiere contacts:read o *. La lista debe pertenecer a la organización del token. Aquí contactCount sí es la longitud real de contactIds (en estáticas). No devuelve los ids de los contactos: usar GET /lists/{id}/contacts.",
        "x-required-permissions": [
          "contacts:read"
        ],
        "responses": {
          "200": {
            "description": "Lista encontrada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListDetailResult"
                },
                "examples": {
                  "static": {
                    "summary": "Lista estática con 2 contactos.",
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439301",
                        "name": "Clientes feria 2026",
                        "description": "Contactos captados en la feria",
                        "type": "static",
                        "filters": [],
                        "contactCount": 2,
                        "createdAt": "2026-09-05T15:00:00.000Z",
                        "updatedAt": "2026-09-05T15:00:00.000Z"
                      }
                    }
                  },
                  "dynamic": {
                    "summary": "Lista dinámica con un filtro por ciudad.",
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439302",
                        "name": "Contactos de Medellín",
                        "description": null,
                        "type": "dynamic",
                        "filters": [
                          {
                            "id": "filter-0",
                            "key": "city",
                            "operator": "equals",
                            "value": "Medellín"
                          }
                        ],
                        "contactCount": 0,
                        "createdAt": "2026-08-20T13:45:00.000Z",
                        "updatedAt": "2026-08-20T13:45:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID de lista inválido (no es un ObjectId). Nota — GET /lists/static o /lists/dynamic caen aquí (solo existen como POST).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListError"
                },
                "example": {
                  "success": false,
                  "error": "Invalid list ID"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Lista inexistente o no perteneciente a la organización del token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListError"
                },
                "example": {
                  "success": false,
                  "error": "List not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1lists/get/responses/500"
          }
        }
      },
      "put": {
        "operationId": "updateContactList",
        "tags": [
          "Lists"
        ],
        "summary": "Actualizar nombre, descripción o filtros de una lista",
        "description": "Requiere contacts:write o *. Solo name, description y filters; sin ninguno responde 400. filters se valida y normaliza como en POST /lists/dynamic (400 si un filtro es inválido), incluso en listas estáticas donde no tiene efecto. name no puede ser vacío ni de otro tipo (400). No permite cambiar type ni contactIds.",
        "x-required-permissions": [
          "contacts:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ListUpdate"
              },
              "examples": {
                "rename": {
                  "value": {
                    "name": "Clientes feria 2026 (VIP)"
                  }
                },
                "filters": {
                  "summary": "Reemplazar los filtros de una dinámica (conviene enviar id).",
                  "value": {
                    "filters": [
                      {
                        "id": "filter-0",
                        "key": "city",
                        "operator": "contains",
                        "value": "Medell"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Lista actualizada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListDetailResult"
                },
                "examples": {
                  "updated": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439301",
                        "name": "Clientes feria 2026 (VIP)",
                        "description": "Contactos captados en la feria",
                        "type": "static",
                        "filters": [],
                        "contactCount": 3,
                        "createdAt": "2026-09-05T15:00:00.000Z",
                        "updatedAt": "2026-09-06T10:30:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID inválido, body sin campos permitidos o name vacío.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListError"
                },
                "examples": {
                  "invalidId": {
                    "value": {
                      "success": false,
                      "error": "Invalid list ID"
                    }
                  },
                  "noFields": {
                    "value": {
                      "success": false,
                      "error": "No se proporcionaron campos para actualizar"
                    }
                  },
                  "emptyName": {
                    "value": {
                      "success": false,
                      "error": "El campo 'name' no puede estar vacío"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1lists~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1lists/get/responses/500"
          }
        }
      },
      "delete": {
        "operationId": "deleteContactList",
        "tags": [
          "Lists"
        ],
        "summary": "Eliminar una lista",
        "description": "Requiere contacts:delete o *. Borra solo la lista; los contactos no se tocan. Responde 200 con el id, no 204.",
        "x-required-permissions": [
          "contacts:delete"
        ],
        "responses": {
          "200": {
            "description": "Lista eliminada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListDeleteResult"
                },
                "examples": {
                  "deleted": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439301"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1lists~1{id}/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1lists~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1lists/get/responses/500"
          }
        }
      }
    },
    "/lists/{id}/contacts": {
      "parameters": [
        {
          "$ref": "#/paths/~1lists~1{id}/parameters/0"
        }
      ],
      "get": {
        "operationId": "getContactListMembers",
        "tags": [
          "Lists"
        ],
        "summary": "Listar los contactos de una lista",
        "description": "Requiere contacts:read o *. Estáticas: devuelve los contactos de contactIds que existen en la organización del token (un id ajeno o borrado simplemente no aparece) y total cuenta esos contactos reales, no la longitud del array (desde el 05-sep-2026; antes no filtraba por organización). Dinámicas: reconstruye la consulta desde los filtros en cada llamada (AND entre filtros, sobre properties del contacto o leadScore) dentro de la organización; filtros sin key/operator u operator desconocido se ignoran. Los contactos vuelven aplanados sin fullName. Sin orden definido.",
        "x-required-permissions": [
          "contacts:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Contactos de la lista y paginación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListMembersResult"
                },
                "examples": {
                  "members": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "507f1f77bcf86cd799439011",
                          "firstName": "Carlos",
                          "lastName": "Pérez",
                          "email": "carlos@eventos.example.com",
                          "mobile": "573000000000",
                          "city": "Medellín",
                          "createdAt": "2026-01-15T15:00:00.000Z",
                          "updatedAt": "2026-01-15T15:00:00.000Z"
                        },
                        {
                          "id": "507f1f77bcf86cd799439012",
                          "firstName": "Ana",
                          "lastName": "Ejemplo",
                          "email": "ana@example.com",
                          "createdAt": "2026-02-01T10:00:00.000Z",
                          "updatedAt": "2026-02-01T10:00:00.000Z"
                        }
                      ],
                      "pagination": {
                        "total": 2,
                        "page": 1,
                        "limit": 100,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1lists~1{id}/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1lists~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1lists/get/responses/500"
          }
        }
      },
      "post": {
        "operationId": "addContactsToList",
        "tags": [
          "Lists"
        ],
        "summary": "Agregar contactos a una lista estática",
        "description": "Requiere contacts:write o *. Solo listas estáticas (dinámica → 400). Los ids ya presentes se cuentan en skipped; los repetidos dentro del mismo body se agregan todos (no se deduplican entre sí). Cada id nuevo debe ser un contacto existente de la organización del token; si alguno no lo es responde 400 con invalidContactIds y no agrega ninguno (desde el 05-sep-2026). Si el total superaría 2000 responde 400 sin agregar ninguno. Para quitar contactos: DELETE /lists/{id}/contacts.",
        "x-required-permissions": [
          "contacts:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddContactsRequest"
              },
              "examples": {
                "add": {
                  "value": {
                    "contactIds": [
                      "507f1f77bcf86cd799439011",
                      "507f1f77bcf86cd799439013"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Contactos agregados (no 201).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AddContactsResult"
                },
                "examples": {
                  "added": {
                    "summary": "Dos ids enviados, uno ya estaba.",
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439301",
                        "added": 1,
                        "skipped": 1,
                        "totalContacts": 3
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID de lista inválido, lista dinámica, contactIds vacío/ausente, id no ObjectId, límite de 2000 superado, o ids que no son contactos de la organización (invalidContactIds).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListError"
                },
                "examples": {
                  "dynamic": {
                    "value": {
                      "success": false,
                      "error": "Solo se pueden agregar contactos a listas estáticas"
                    }
                  },
                  "empty": {
                    "value": {
                      "success": false,
                      "error": "Se requiere un array 'contactIds' no vacío"
                    }
                  },
                  "limit": {
                    "value": {
                      "success": false,
                      "error": "La lista superaría el límite de 2000 contactos"
                    }
                  },
                  "foreign": {
                    "value": {
                      "success": false,
                      "error": "Contactos inexistentes o de otra organización",
                      "invalidContactIds": [
                        "507f1f77bcf86cd799439099"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1lists~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1lists/get/responses/500"
          }
        }
      },
      "delete": {
        "operationId": "removeContactsFromList",
        "tags": [
          "Lists"
        ],
        "summary": "Quitar contactos de una lista estática",
        "description": "Requiere contacts:write o *. Espejo de POST: solo listas estáticas (dinámica → 400). Quita de contactIds los ids enviados; los que no estaban se cuentan en skipped y no fallan. No borra contactos, solo la pertenencia. Disponible desde el 05-sep-2026 (N1.5): antes no había forma de sacar un contacto sin borrar la lista.",
        "x-required-permissions": [
          "contacts:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddContactsRequest"
              },
              "examples": {
                "remove": {
                  "value": {
                    "contactIds": [
                      "507f1f77bcf86cd799439011",
                      "507f1f77bcf86cd799439099"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Contactos quitados.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RemoveContactsResult"
                },
                "examples": {
                  "removed": {
                    "summary": "Un id quitado, otro no estaba",
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439081",
                        "removed": 1,
                        "skipped": 1,
                        "totalContacts": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID de lista inválido, lista dinámica, contactIds vacío/ausente o id no ObjectId.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListError"
                },
                "examples": {
                  "dynamic": {
                    "value": {
                      "success": false,
                      "error": "Solo se pueden quitar contactos de listas estáticas"
                    }
                  },
                  "empty": {
                    "value": {
                      "success": false,
                      "error": "Se requiere un array 'contactIds' no vacío"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1lists~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1lists/get/responses/500"
          }
        }
      }
    },
    "/fragments": {
      "get": {
        "operationId": "listFragments",
        "tags": [
          "Fragments"
        ],
        "summary": "Listar fragments (respuestas rápidas)",
        "description": "Requiere fragments:read o *. Filtra por la organización del API Token. Orden por defecto updatedAt desc (no createdAt como en otros módulos). search busca en name, text y atajo sin escapar la expresión regular (un + o ( pueden responder 500).",
        "x-required-permissions": [
          "fragments:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Coincidencia parcial en name, text o atajo, sin distinguir mayúsculas (regex sin escapar)."
          },
          {
            "name": "sortBy",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "createdAt",
                "updatedAt",
                "name",
                "atajo"
              ],
              "default": "updatedAt"
            },
            "description": "Un valor fuera del enum cae a updatedAt sin error."
          },
          {
            "name": "sortOrder",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            },
            "description": "Cualquier valor distinto de asc ordena desc."
          }
        ],
        "responses": {
          "200": {
            "description": "Fragments y paginación; data puede estar vacío.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FragmentListResult"
                },
                "examples": {
                  "list": {
                    "summary": "Ordenado por updatedAt desc por defecto.",
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "507f1f77bcf86cd799439402",
                          "name": "Cotización por correo",
                          "text": "<p>Adjunto la cotización solicitada.</p>",
                          "atajo": "cotizacion",
                          "format": "html",
                          "asunto": "Cotización Manillas de Control",
                          "userId": "507f1f77bcf86cd799439001",
                          "createdAt": "2026-08-20T13:45:00.000Z",
                          "updatedAt": "2026-09-06T10:30:00.000Z"
                        },
                        {
                          "id": "507f1f77bcf86cd799439401",
                          "name": "Saludo inicial",
                          "text": "¡Hola! Gracias por escribir a Manillas de Control. ¿En qué te ayudamos?",
                          "atajo": "saludo",
                          "format": "text",
                          "asunto": "",
                          "userId": "507f1f77bcf86cd799439001",
                          "createdAt": "2026-09-05T15:00:00.000Z",
                          "updatedAt": "2026-09-05T15:00:00.000Z"
                        }
                      ],
                      "pagination": {
                        "total": 2,
                        "page": 1,
                        "limit": 100,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "description": "Error interno (incluye regex inválida en search).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FragmentError"
                },
                "example": {
                  "success": false,
                  "error": "Internal server error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createFragment",
        "tags": [
          "Fragments"
        ],
        "summary": "Crear un fragment",
        "description": "Requiere fragments:write o *. NO aplica límite de plan. name, text y atajo obligatorios. format solo acepta html; cualquier otro valor se guarda como text sin avisar. El atajo debe ser único dentro de la organización (409 si ya existe; otra organización puede usar el mismo, desde el 05-sep-2026). Responde 201.",
        "x-required-permissions": [
          "fragments:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FragmentCreate"
              },
              "examples": {
                "text": {
                  "value": {
                    "name": "Saludo inicial",
                    "text": "¡Hola! Gracias por escribir a Manillas de Control. ¿En qué te ayudamos?",
                    "atajo": "saludo"
                  }
                },
                "html": {
                  "value": {
                    "name": "Cotización por correo",
                    "text": "<p>Adjunto la cotización solicitada.</p>",
                    "atajo": "cotizacion",
                    "format": "html",
                    "asunto": "Cotización Manillas de Control"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Fragment creado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FragmentResult"
                },
                "examples": {
                  "created": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439401",
                        "name": "Saludo inicial",
                        "text": "¡Hola! Gracias por escribir a Manillas de Control. ¿En qué te ayudamos?",
                        "atajo": "saludo",
                        "format": "text",
                        "asunto": "",
                        "userId": "507f1f77bcf86cd799439001",
                        "createdAt": "2026-09-05T15:00:00.000Z",
                        "updatedAt": "2026-09-05T15:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Falta name, text o atajo (o son cadenas vacías).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FragmentError"
                },
                "examples": {
                  "name": {
                    "value": {
                      "success": false,
                      "error": "El campo 'name' es requerido"
                    }
                  },
                  "text": {
                    "value": {
                      "success": false,
                      "error": "El campo 'text' es requerido"
                    }
                  },
                  "atajo": {
                    "value": {
                      "success": false,
                      "error": "El campo 'atajo' es requerido"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "409": {
            "description": "Ya existe un fragment con ese atajo en la organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FragmentError"
                },
                "example": {
                  "success": false,
                  "error": "Ya existe un fragment con ese atajo"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1fragments/get/responses/500"
          }
        }
      }
    },
    "/fragments/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB del fragment (campo id de las respuestas).",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "507f1f77bcf86cd799439401"
        }
      ],
      "get": {
        "operationId": "getFragmentById",
        "tags": [
          "Fragments"
        ],
        "summary": "Consultar un fragment por ID",
        "description": "Requiere fragments:read o *. El fragment debe pertenecer a la organización del token.",
        "x-required-permissions": [
          "fragments:read"
        ],
        "responses": {
          "200": {
            "description": "Fragment encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FragmentResult"
                },
                "examples": {
                  "found": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439401",
                        "name": "Saludo inicial",
                        "text": "¡Hola! Gracias por escribir a Manillas de Control. ¿En qué te ayudamos?",
                        "atajo": "saludo",
                        "format": "text",
                        "asunto": "",
                        "userId": "507f1f77bcf86cd799439001",
                        "createdAt": "2026-09-05T15:00:00.000Z",
                        "updatedAt": "2026-09-05T15:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID de fragment inválido (no es un ObjectId).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FragmentError"
                },
                "example": {
                  "success": false,
                  "error": "Invalid fragment ID"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Fragment inexistente o no perteneciente a la organización del token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FragmentError"
                },
                "example": {
                  "success": false,
                  "error": "Fragment not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1fragments/get/responses/500"
          }
        }
      },
      "put": {
        "operationId": "updateFragmentById",
        "tags": [
          "Fragments"
        ],
        "summary": "Actualizar parcialmente un fragment",
        "description": "Requiere fragments:write o *. Solo name, text, atajo, format y asunto; sin ninguno responde 400. name, text y atajo no pueden quedar vacíos; format debe ser text o html (400). Cambiar atajo a uno que ya exista en la organización responde 409 (índice único org_atajo_unique).",
        "x-required-permissions": [
          "fragments:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FragmentUpdate"
              },
              "examples": {
                "text": {
                  "value": {
                    "text": "<p>Adjunto la cotización solicitada. Quedo atento.</p>"
                  }
                },
                "rename": {
                  "value": {
                    "name": "Cotización formal",
                    "atajo": "cotizacion-formal"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Fragment actualizado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FragmentResult"
                },
                "examples": {
                  "updated": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439402",
                        "name": "Cotización por correo",
                        "text": "<p>Adjunto la cotización solicitada. Quedo atento.</p>",
                        "atajo": "cotizacion",
                        "format": "html",
                        "asunto": "Cotización Manillas de Control",
                        "userId": "507f1f77bcf86cd799439001",
                        "createdAt": "2026-08-20T13:45:00.000Z",
                        "updatedAt": "2026-09-06T10:30:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID inválido, body sin campos permitidos, campo vacío o format inválido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FragmentError"
                },
                "examples": {
                  "invalidId": {
                    "value": {
                      "success": false,
                      "error": "Invalid fragment ID"
                    }
                  },
                  "noFields": {
                    "value": {
                      "success": false,
                      "error": "No se proporcionaron campos para actualizar"
                    }
                  },
                  "format": {
                    "value": {
                      "success": false,
                      "error": "El campo 'format' debe ser 'text' o 'html'"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1fragments~1{id}/get/responses/404"
          },
          "409": {
            "$ref": "#/paths/~1fragments/post/responses/409"
          },
          "500": {
            "$ref": "#/paths/~1fragments/get/responses/500"
          }
        }
      },
      "delete": {
        "operationId": "deleteFragmentById",
        "tags": [
          "Fragments"
        ],
        "summary": "Eliminar un fragment",
        "description": "Requiere fragments:delete o *. Responde 200 con el id, no 204.",
        "x-required-permissions": [
          "fragments:delete"
        ],
        "responses": {
          "200": {
            "description": "Fragment eliminado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FragmentDeleteResult"
                },
                "examples": {
                  "deleted": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "507f1f77bcf86cd799439401"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1fragments~1{id}/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1fragments~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1fragments/get/responses/500"
          }
        }
      }
    },
    "/library": {
      "get": {
        "operationId": "listLibraryFiles",
        "tags": [
          "Library"
        ],
        "summary": "Listar archivos de la Biblioteca",
        "description": "Requiere library:read o *. Filtra por la organización del token. Por defecto solo devuelve archivos visibles (isVisible true); isVisible=false devuelve SOLO los invisibles (adjuntos de mensajes) e isVisible=all, todos (desde el 05-sep-2026). tag se normaliza (trim, minúsculas) y compara exacto; fileType compara exacto; search es un literal (escapado) sobre name. page no numérico cae a 1. Solo metadatos: no hay descarga ni subida por esta API (la subida multipart vive en la API interna).",
        "x-required-permissions": [
          "library:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          },
          {
            "name": "isVisible",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false",
                "all"
              ],
              "default": "true"
            },
            "description": "true (default) solo visibles; false solo invisibles (adjuntos de mensajes); all, todos."
          },
          {
            "name": "tag",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Coincidencia exacta con un tag (normalizado a minúsculas)."
          },
          {
            "name": "fileType",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Coincidencia exacta con fileType (p. ej. application/pdf)."
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Coincidencia parcial en name sin distinguir mayúsculas (regex sin escapar)."
          },
          {
            "name": "sortBy",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "createdAt",
                "updatedAt",
                "name"
              ],
              "default": "createdAt"
            },
            "description": "Un valor fuera del enum cae a createdAt sin error."
          },
          {
            "name": "sortOrder",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            },
            "description": "Solo el literal asc invierte el orden; cualquier otro valor es desc."
          }
        ],
        "responses": {
          "200": {
            "description": "Archivos y paginación; data puede estar vacío.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LibraryFileListResult"
                },
                "examples": {
                  "list": {
                    "summary": "Página 1 con un PDF y una imagen.",
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "66f1000000000000000000d1",
                          "name": "Catálogo manillas 2026.pdf",
                          "fileType": "application/pdf",
                          "mediaURL": "https://fusioncrm-bucket.s3.amazonaws.com/library/66f1000000000000000000a1/catalogo-manillas-2026.pdf",
                          "tags": [
                            "catálogo",
                            "precios"
                          ],
                          "isVisible": true,
                          "createdAt": "2026-08-20T13:45:00.000Z",
                          "updatedAt": "2026-09-05T15:00:00.000Z"
                        },
                        {
                          "id": "66f1000000000000000000d2",
                          "name": "logo-evento.png",
                          "fileType": "image/png",
                          "mediaURL": "https://fusioncrm-bucket.s3.amazonaws.com/library/66f1000000000000000000a1/logo-evento.png",
                          "tags": [],
                          "isVisible": true,
                          "createdAt": "2026-07-02T10:12:00.000Z",
                          "updatedAt": "2026-07-02T10:12:00.000Z"
                        }
                      ],
                      "pagination": {
                        "total": 2,
                        "page": 1,
                        "limit": 100,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "description": "Error interno (incluye regex inválida en search).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LibraryError"
                },
                "example": {
                  "success": false,
                  "error": "Error al listar la biblioteca"
                }
              }
            }
          }
        }
      }
    },
    "/library/tags": {
      "get": {
        "operationId": "listLibraryTags",
        "tags": [
          "Library"
        ],
        "summary": "Listar los tags en uso",
        "description": "Requiere library:read o *. Devuelve el catálogo de tags de la organización (settings.library.tags, el mismo que edita la app), ordenado alfabéticamente. Si la organización aún no tiene catálogo, cae a los tags en uso por sus archivos visibles (desde el 05-sep-2026; antes siempre era el distinct).",
        "x-required-permissions": [
          "library:read"
        ],
        "responses": {
          "200": {
            "description": "Lista de tags (puede ser []).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LibraryTagListResult"
                },
                "examples": {
                  "tags": {
                    "value": {
                      "success": true,
                      "data": [
                        "catálogo",
                        "plantillas",
                        "precios"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "description": "Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LibraryError"
                },
                "example": {
                  "success": false,
                  "error": "Error al obtener los tags"
                }
              }
            }
          }
        }
      }
    },
    "/library/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB del archivo (campo id de las respuestas). Un valor que no es ObjectId responde 400.",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "66f1000000000000000000d1"
        }
      ],
      "get": {
        "operationId": "getLibraryFileById",
        "tags": [
          "Library"
        ],
        "summary": "Consultar un archivo por ID",
        "description": "Requiere library:read o *. El archivo debe pertenecer a la organización del token (scoped anti-IDOR); devuelve también archivos invisibles si se conoce el ID. Un ID que no es ObjectId responde 400.",
        "x-required-permissions": [
          "library:read"
        ],
        "responses": {
          "200": {
            "description": "Archivo encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LibraryFileResult"
                },
                "examples": {
                  "found": {
                    "summary": "Catálogo en PDF con dos tags.",
                    "value": {
                      "success": true,
                      "data": {
                        "id": "66f1000000000000000000d1",
                        "name": "Catálogo manillas 2026.pdf",
                        "fileType": "application/pdf",
                        "mediaURL": "https://fusioncrm-bucket.s3.amazonaws.com/library/66f1000000000000000000a1/catalogo-manillas-2026.pdf",
                        "tags": [
                          "catálogo",
                          "precios"
                        ],
                        "isVisible": true,
                        "createdAt": "2026-08-20T13:45:00.000Z",
                        "updatedAt": "2026-09-05T15:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Archivo inexistente o no perteneciente a la organización del token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LibraryError"
                },
                "example": {
                  "success": false,
                  "error": "Archivo no encontrado"
                }
              }
            }
          },
          "500": {
            "description": "Error interno, incluido un ID con formato inválido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LibraryError"
                },
                "example": {
                  "success": false,
                  "error": "Error al obtener el archivo"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "updateLibraryFile",
        "tags": [
          "Library"
        ],
        "summary": "Actualizar nombre y/o tags de un archivo",
        "description": "Requiere library:write o *. Solo metadatos: name y/o tags (400 si no viene ninguno). tags se normaliza y reemplaza la lista completa; los tags nuevos se agregan al catálogo de la organización solo después de validar el body completo (un name vacío responde 400 sin tocar ni el archivo ni el catálogo). Siempre actualiza updatedAt. Un ID que no es ObjectId responde 400. Responde el archivo en el mismo formato de GET.",
        "x-required-permissions": [
          "library:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LibraryFileUpdate"
              },
              "examples": {
                "rename": {
                  "summary": "Solo renombrar.",
                  "value": {
                    "name": "Catálogo manillas 2026.pdf"
                  }
                },
                "tagsArray": {
                  "summary": "Reemplazar tags con un array (se normalizan).",
                  "value": {
                    "tags": [
                      "Catálogo",
                      " precios "
                    ]
                  }
                },
                "tagsString": {
                  "summary": "Tags como cadena separada por comas.",
                  "value": {
                    "tags": "catálogo, precios"
                  }
                },
                "clearTags": {
                  "summary": "Quitar todos los tags.",
                  "value": {
                    "tags": []
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Archivo actualizado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LibraryFileResult"
                },
                "examples": {
                  "updated": {
                    "summary": "Tras PUT con tags como cadena \"Catálogo, PRECIOS , precios\" (normalizados y sin duplicados).",
                    "value": {
                      "success": true,
                      "data": {
                        "id": "66f1000000000000000000d1",
                        "name": "Catálogo manillas 2026.pdf",
                        "fileType": "application/pdf",
                        "mediaURL": "https://fusioncrm-bucket.s3.amazonaws.com/library/66f1000000000000000000a1/catalogo-manillas-2026.pdf",
                        "tags": [
                          "catálogo",
                          "precios"
                        ],
                        "isVisible": true,
                        "createdAt": "2026-08-20T13:45:00.000Z",
                        "updatedAt": "2026-09-06T10:30:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Sin name ni tags en el body, o name vacío tras recortar.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LibraryError"
                },
                "examples": {
                  "nothing": {
                    "value": {
                      "success": false,
                      "error": "Nada que actualizar: envía 'name' y/o 'tags'. Esta API no reemplaza el archivo."
                    }
                  },
                  "emptyName": {
                    "value": {
                      "success": false,
                      "error": "'name' no puede estar vacío"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1library~1{id}/get/responses/404"
          },
          "500": {
            "description": "Error interno, incluido un ID con formato inválido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LibraryError"
                },
                "example": {
                  "success": false,
                  "error": "Error al actualizar el archivo"
                }
              }
            }
          }
        }
      }
    },
    "/rag/search": {
      "post": {
        "operationId": "searchRag",
        "tags": [
          "RAG"
        ],
        "summary": "Búsqueda semántica en la base de conocimiento",
        "description": "Requiere rag:read o *. Calcula el embedding de query (text-embedding de OpenAI con la API key de la integración OpenAI de la organización, o la variable OPENAI_API_KEY del servidor como fallback) y compara por similitud coseno, en Node, contra TODOS los chunks de los documentos ready de la organización (fuerza bruta con cursor; no hay índice vectorial). Devuelve como máximo topK fragmentos con score ≥ minScore, ordenados por score. Si la organización no tiene documentos ready (o el filtro tag / documentId no coincide) responde 200 con results [] SIN llamar a OpenAI. Sin API key de OpenAI disponible, o si OpenAI falla tras los reintentos, responde 500 \"Error en la búsqueda\". Es la misma operación que usa el tool search_knowledge_base de los agentes y el tool MCP search_knowledge_base.",
        "x-required-permissions": [
          "rag:read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RagSearchRequest"
              },
              "examples": {
                "simple": {
                  "summary": "Solo la consulta (topK 5, minScore 0.25).",
                  "value": {
                    "query": "precio manilla tyvek"
                  }
                },
                "filtered": {
                  "summary": "Acotada a un tag y con umbral más alto.",
                  "value": {
                    "query": "tiempos de entrega Bogotá",
                    "topK": 3,
                    "minScore": 0.4,
                    "tag": "envíos"
                  }
                },
                "byDocument": {
                  "summary": "Dentro de un solo documento.",
                  "value": {
                    "query": "garantía",
                    "documentId": "66f1000000000000000000e1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Fragmentos encontrados (results puede ser []).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RagSearchResult"
                },
                "examples": {
                  "hits": {
                    "summary": "Dos fragmentos del catálogo por encima del minScore.",
                    "value": {
                      "success": true,
                      "data": {
                        "query": "precio manilla tyvek",
                        "totalResults": 2,
                        "results": [
                          {
                            "content": "Manilla Tyvek 19mm impresa a 1 tinta: $400 c/u desde 100 unidades; $320 c/u desde 1.000.",
                            "score": 0.812,
                            "documentId": "66f1000000000000000000e1",
                            "documentTitle": "Lista de precios 2026",
                            "chunkIndex": 3
                          },
                          {
                            "content": "Las manillas Tyvek son de papel sintético, resistentes al agua, con adhesivo de seguridad.",
                            "score": 0.541,
                            "documentId": "66f1000000000000000000e2",
                            "documentTitle": "Catálogo de productos",
                            "chunkIndex": 0
                          }
                        ]
                      }
                    }
                  },
                  "empty": {
                    "summary": "Sin documentos ready o ningún chunk supera minScore.",
                    "value": {
                      "success": true,
                      "data": {
                        "query": "horario de atención",
                        "totalResults": 0,
                        "results": []
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "query ausente/vacío o documentId que no es ObjectId.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RagError"
                },
                "examples": {
                  "query": {
                    "value": {
                      "success": false,
                      "error": "'query' es requerido"
                    }
                  },
                  "documentId": {
                    "value": {
                      "success": false,
                      "error": "'documentId' inválido"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "description": "Sin API key de OpenAI para embeddings, fallo de OpenAI o error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RagError"
                },
                "example": {
                  "success": false,
                  "error": "Error en la búsqueda"
                }
              }
            }
          }
        }
      }
    },
    "/rag/documents": {
      "get": {
        "operationId": "listRagDocuments",
        "tags": [
          "RAG"
        ],
        "summary": "Listar documentos de la base de conocimiento",
        "description": "Requiere rag:read o *. Solo metadatos resumidos (sin sourceType, mimeType, sizeBytes, tokenCount ni updatedAt), ordenados por createdAt descendente (no configurable). Incluye documentos en processing y failed. status compara exacto y no se valida (un valor fuera del enum devuelve lista vacía). tag se recorta y pasa a minúsculas.",
        "x-required-permissions": [
          "rag:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            },
            "description": "Un valor no numérico cae a 1."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "processing",
                "ready",
                "failed"
              ]
            }
          },
          {
            "name": "tag",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Documentos y paginación; data puede estar vacío.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RagDocumentListResult"
                },
                "examples": {
                  "list": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "66f1000000000000000000e1",
                          "title": "Lista de precios 2026",
                          "fileName": "lista_de_precios_2026.txt",
                          "status": "ready",
                          "chunkCount": 12,
                          "tags": [
                            "precios"
                          ],
                          "createdAt": "2026-09-01T14:00:00.000Z"
                        },
                        {
                          "id": "66f1000000000000000000e3",
                          "title": "Políticas de envío",
                          "fileName": "politicas_de_envio.txt",
                          "status": "processing",
                          "chunkCount": 0,
                          "tags": [],
                          "createdAt": "2026-09-05T15:00:00.000Z"
                        }
                      ],
                      "pagination": {
                        "total": 2,
                        "page": 1,
                        "limit": 100,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "description": "Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RagError"
                },
                "example": {
                  "success": false,
                  "error": "Error al listar los documentos"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createRagTextDocument",
        "tags": [
          "RAG"
        ],
        "summary": "Ingestar un documento de texto plano",
        "description": "Requiere rag:write o *. Solo JSON con texto plano (sin archivos). Orden de validaciones: (1) planLimitsMiddleware verifica la cuota de documentos RAG del plan (403 con contrato message/code/details); (2) text obligatorio (400) y ≤ 2 MB UTF-8 (413); (3) el controller repite la cuota de documentos (403 con contrato success:false/error/code/details, solo alcanzable en una carrera) y verifica la cuota de almacenamiento ragStorageMb del plan (403, -1 = ilimitado; por defecto 20 MB si el plan no lo define). Luego sube el texto a S3 como text/plain bajo rag/<org>/<timestamp>_<hash>_<fileName>, crea el documento con status processing y encola la ingesta (extracción + chunks + embeddings, 3 intentos con backoff). Responde 202: el documento NO está buscable hasta que status pase a ready (consultar GET /rag/documents). Si S3 falla responde 500. Si la COLA falla, el documento queda en failed con statusError y la respuesta es 503 RAG_QUEUE_UNAVAILABLE (desde el 05-sep-2026; antes quedaba en processing para siempre): elimínelo y reintente. La ingesta necesita API key de OpenAI (integración de la organización o OPENAI_API_KEY); sin ella el documento termina en failed con statusError. Equivale al tool MCP upload_knowledge_base_text.",
        "x-required-permissions": [
          "rag:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RagDocumentCreate"
              },
              "examples": {
                "full": {
                  "summary": "Título, texto y tags.",
                  "value": {
                    "title": "Políticas de envío",
                    "text": "Despachamos a todo Colombia con Servientrega. Bogotá y Medellín: 1-2 días hábiles. Resto del país: 3-5 días hábiles. El envío es gratis desde $300.000.",
                    "tags": [
                      "envíos",
                      "políticas"
                    ]
                  }
                },
                "minimal": {
                  "summary": "Solo el texto (título \"texto-manual\").",
                  "value": {
                    "text": "Horario de atención: lunes a viernes de 8am a 5pm."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Documento creado en processing y encolado para indexar. No responde 201.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RagDocumentAcceptedResult"
                },
                "examples": {
                  "accepted": {
                    "summary": "Documento recién encolado; chunkCount y tokenCount se llenan al terminar la ingesta.",
                    "value": {
                      "success": true,
                      "message": "Documento recibido; la indexación corre en segundo plano.",
                      "data": {
                        "id": "66f1000000000000000000e3",
                        "title": "Políticas de envío",
                        "sourceType": "manual_text",
                        "fileName": "politicas_de_envio.txt",
                        "mimeType": "text/plain",
                        "sizeBytes": 1842,
                        "status": "processing",
                        "chunkCount": 0,
                        "tokenCount": 0,
                        "tags": [
                          "envíos",
                          "políticas"
                        ],
                        "createdAt": "2026-09-05T15:00:00.000Z",
                        "updatedAt": "2026-09-05T15:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "text ausente, no es cadena o está vacío.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RagError"
                },
                "example": {
                  "success": false,
                  "error": "'text' es requerido"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "description": "Permiso insuficiente, módulo API no habilitado, o límite del plan alcanzado (PLAN_LIMIT_REACHED): el de cantidad de documentos llega del middleware con message/code/details; el de almacenamiento llega del controller con success:false/error/code.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ApiError"
                    },
                    {
                      "$ref": "#/components/schemas/RagError"
                    }
                  ]
                },
                "examples": {
                  "documentsLimit": {
                    "summary": "Cuota de documentos (middleware).",
                    "value": {
                      "message": "Has alcanzado el límite de tu plan. Actualiza tu plan para continuar.",
                      "code": "PLAN_LIMIT_REACHED",
                      "details": {
                        "allowed": false,
                        "limit": 10,
                        "used": 10,
                        "remaining": 0,
                        "planName": "Starter"
                      }
                    }
                  },
                  "storageLimit": {
                    "summary": "Cuota de almacenamiento (controller).",
                    "value": {
                      "success": false,
                      "error": "Has alcanzado el límite de almacenamiento RAG de tu plan. Actualiza tu plan o elimina documentos.",
                      "code": "PLAN_LIMIT_REACHED"
                    }
                  },
                  "permission": {
                    "value": {
                      "message": "Permiso insuficiente. Se requiere: rag:write",
                      "code": "INSUFFICIENT_API_PERMISSIONS",
                      "requiredPermission": "rag:write",
                      "availablePermissions": [
                        "rag:read"
                      ]
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "El texto supera 2 MB en UTF-8.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RagError"
                },
                "example": {
                  "success": false,
                  "error": "El texto supera el máximo de 2 MB"
                }
              }
            }
          },
          "500": {
            "description": "Fallo al subir a S3, al consultar el plan, o error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RagError"
                },
                "example": {
                  "success": false,
                  "error": "Error al subir el documento"
                }
              }
            }
          },
          "503": {
            "description": "La cola de ingesta no aceptó el trabajo; el documento quedó en failed (data lo muestra).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RagError"
                },
                "example": {
                  "success": false,
                  "error": "No se pudo encolar la indexación del documento",
                  "code": "RAG_QUEUE_UNAVAILABLE",
                  "data": {
                    "id": "507f1f77bcf86cd799439301",
                    "title": "Horario",
                    "status": "failed"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/rag/documents/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB del documento (campo id de las respuestas). Un valor que no es ObjectId responde 404, no 400.",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "66f1000000000000000000e1"
        }
      ],
      "delete": {
        "operationId": "deleteRagDocument",
        "tags": [
          "RAG"
        ],
        "summary": "Eliminar un documento y sus fragmentos",
        "description": "Requiere rag:delete o *. Scoped a la organización del token. Borra en cascada los chunks (embeddings) y luego el documento; el archivo en S3 solo se borra si el RAG es dueño del objeto (documentos subidos por API o manualmente) — si el documento se indexó desde un archivo de la Biblioteca (libraryItemId) el archivo se conserva. Un fallo al borrar en S3 se ignora y la operación sigue. Un ID con formato inválido responde 404, no 400. Responde 200 con mensaje, no 204. Equivale al tool MCP delete_knowledge_base_document.",
        "x-required-permissions": [
          "rag:delete"
        ],
        "responses": {
          "200": {
            "description": "Documento y chunks eliminados.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RagDeleteResult"
                },
                "example": {
                  "success": true,
                  "message": "Documento eliminado correctamente"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "ID con formato inválido, documento inexistente o de otra organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RagError"
                },
                "example": {
                  "success": false,
                  "error": "Documento no encontrado"
                }
              }
            }
          },
          "500": {
            "description": "Error interno (fallo de base de datos; los fallos de S3 no producen 500).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RagError"
                },
                "example": {
                  "success": false,
                  "error": "Error al eliminar el documento"
                }
              }
            }
          }
        }
      }
    },
    "/reports/sales/overview": {
      "get": {
        "operationId": "getSalesOverview",
        "tags": [
          "Reports"
        ],
        "summary": "Resumen de ventas del período, serie de 13 meses y top clientes",
        "description": "Requiere deals:read o *. Mismo cálculo que la pantalla Reportes → Ventas de la app. `summary` es el período pedido y `previousSummary` el mismo rango del año anterior; `salesByMonth` trae 13 meses anclados a `toDate` con su variación interanual, y `topCustomersWithDetails` los 5 mayores clientes de los últimos 3 meses. Solo cuenta negocios GANADOS por su fecha de cierre. Las fechas se interpretan en UTC puro, igual que se guardan: un negocio cerrado el 30 a las 23:00 en Colombia cae en el día 31.",
        "x-required-permissions": [
          "deals:read"
        ],
        "parameters": [
          {
            "name": "fromDate",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
            },
            "example": "2025-09-01"
          },
          {
            "name": "toDate",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
            },
            "example": "2025-09-30"
          }
        ],
        "responses": {
          "200": {
            "description": "Resumen del período.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "summary",
                    "previousSummary",
                    "salesByMonth",
                    "topCustomersWithDetails"
                  ],
                  "properties": {
                    "summary": {
                      "type": "object",
                      "required": [
                        "totalSales",
                        "averageTicket",
                        "products",
                        "newCustomers",
                        "conversationsStarted"
                      ],
                      "properties": {
                        "totalSales": {
                          "type": "number"
                        },
                        "averageTicket": {
                          "type": "number"
                        },
                        "products": {
                          "type": [
                            "number",
                            "null"
                          ],
                          "description": "Unidades vendidas según el campo de cantidad configurado por la organización (Ajustes → Reportes). null = la organización no lo configuró."
                        },
                        "newCustomers": {
                          "type": "integer",
                          "minimum": 0,
                          "description": "Contactos únicos con negocio ganado en el período."
                        },
                        "conversationsStarted": {
                          "type": "integer",
                          "minimum": 0
                        },
                        "fromDate": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "toDate": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "previousSummary": {
                      "allOf": [
                        {
                          "$ref": "#/paths/~1reports~1sales~1overview/get/responses/200/content/application~1json/schema/properties/summary"
                        }
                      ],
                      "description": "Mismo período del año anterior, para comparar."
                    },
                    "salesByMonth": {
                      "type": "array",
                      "description": "Serie de 13 meses anclada al toDate, con su comparación interanual.",
                      "items": {
                        "type": "object",
                        "required": [
                          "month",
                          "ventas",
                          "previousVentas",
                          "yoyPct",
                          "yoyTrend"
                        ],
                        "properties": {
                          "month": {
                            "type": "string",
                            "example": "Septiembre 2025",
                            "description": "Nombre del mes en español y año."
                          },
                          "ventas": {
                            "type": "number"
                          },
                          "previousVentas": {
                            "type": "number",
                            "description": "Mismo mes del año anterior."
                          },
                          "yoyPct": {
                            "type": "number",
                            "description": "Variación interanual en %. 100 cuando el año anterior fue 0 y este no."
                          },
                          "yoyTrend": {
                            "type": "string",
                            "enum": [
                              "up",
                              "down"
                            ]
                          }
                        }
                      }
                    },
                    "topCustomersWithDetails": {
                      "type": "array",
                      "description": "Top 5 clientes de los últimos 3 meses. Cada elemento son las propiedades del contacto aplanadas (firstName, email, …) más total y count.",
                      "items": {
                        "type": "object",
                        "required": [
                          "total",
                          "count"
                        ],
                        "properties": {
                          "total": {
                            "type": "number"
                          },
                          "count": {
                            "type": "integer",
                            "minimum": 0
                          }
                        },
                        "additionalProperties": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Faltan fromDate/toDate o la organización no se pudo resolver.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/reports/sales/forecast": {
      "get": {
        "operationId": "getSalesForecast",
        "tags": [
          "Reports"
        ],
        "summary": "Forecast ponderado de negocios abiertos",
        "description": "Requiere deals:read o *. Σ (monto × probabilidad de la etapa) sobre los negocios ABIERTOS con fecha de cierre dentro del rango, con desglose por mes, por etapa y por vendedor. Los abiertos sin fecha de cierre y los ya vencidos se informan aparte en `summary` y no entran en `byMonth`; los vencidos tampoco suman a ningún total. Si `summary.stages.withProbability` es 0 el ponderado será 0: la organización no configuró probabilidades de etapa.",
        "x-required-permissions": [
          "deals:read"
        ],
        "parameters": [
          {
            "name": "fromDate",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
            },
            "example": "2026-09-01"
          },
          {
            "name": "toDate",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
            },
            "example": "2026-12-31"
          },
          {
            "name": "pipelineId",
            "in": "query",
            "required": false,
            "description": "Limita el forecast a un pipeline. Sin él, todos los pipelines de la organización.",
            "schema": {
              "type": "string",
              "pattern": "^[a-fA-F0-9]{24}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Forecast del rango.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "fromDate",
                    "toDate",
                    "pipelineId",
                    "summary",
                    "byMonth",
                    "byStage",
                    "byOwner"
                  ],
                  "properties": {
                    "fromDate": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "toDate": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "pipelineId": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "summary": {
                      "type": "object",
                      "required": [
                        "weightedTotal",
                        "openTotal",
                        "openCount",
                        "unconfigured",
                        "withoutClosingDate",
                        "stages",
                        "overdue"
                      ],
                      "properties": {
                        "weightedTotal": {
                          "type": "number",
                          "description": "Σ (monto × probabilidad de la etapa / 100)."
                        },
                        "openTotal": {
                          "type": "number",
                          "description": "Σ monto bruto de los negocios abiertos."
                        },
                        "openCount": {
                          "type": "integer",
                          "minimum": 0
                        },
                        "unconfigured": {
                          "allOf": [
                            {
                              "$ref": "#/paths/~1reports~1sales~1forecast/get/responses/200/content/application~1json/schema/properties/summary/properties/overdue/allOf/0"
                            }
                          ],
                          "description": "Abiertos cuya etapa no tiene probabilidad — fuera del ponderado."
                        },
                        "withoutClosingDate": {
                          "allOf": [
                            {
                              "$ref": "#/paths/~1reports~1sales~1forecast/get/responses/200/content/application~1json/schema/properties/summary/properties/overdue/allOf/0"
                            }
                          ],
                          "description": "Abiertos sin fecha de cierre — suman al total, no a byMonth."
                        },
                        "stages": {
                          "type": "object",
                          "description": "Salud de la configuración de etapas de la organización. Un forecast en $0 se explica acá: sin etapas abiertas o sin probabilidad no hay ponderado.",
                          "required": [
                            "open",
                            "withProbability"
                          ],
                          "properties": {
                            "open": {
                              "type": "integer",
                              "minimum": 0
                            },
                            "withProbability": {
                              "type": "integer",
                              "minimum": 0
                            }
                          }
                        },
                        "overdue": {
                          "allOf": [
                            {
                              "type": "object",
                              "required": [
                                "count",
                                "amount"
                              ],
                              "properties": {
                                "count": {
                                  "type": "integer",
                                  "minimum": 0
                                },
                                "amount": {
                                  "type": "number"
                                }
                              }
                            }
                          ],
                          "description": "Abiertos con fecha de cierre ya vencida. NO suman a ningún total."
                        }
                      }
                    },
                    "byMonth": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "year",
                          "month",
                          "weighted",
                          "amount",
                          "count"
                        ],
                        "properties": {
                          "year": {
                            "type": "integer"
                          },
                          "month": {
                            "type": "integer",
                            "minimum": 1,
                            "maximum": 12
                          },
                          "weighted": {
                            "type": "number"
                          },
                          "amount": {
                            "type": "number"
                          },
                          "count": {
                            "type": "integer",
                            "minimum": 0
                          }
                        }
                      }
                    },
                    "byStage": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "statusId",
                          "name",
                          "color",
                          "probability",
                          "weighted",
                          "amount",
                          "count"
                        ],
                        "properties": {
                          "statusId": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "name": {
                            "type": "string"
                          },
                          "color": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "probability": {
                            "type": [
                              "number",
                              "null"
                            ],
                            "description": "null = etapa sin probabilidad configurada."
                          },
                          "weighted": {
                            "type": "number"
                          },
                          "amount": {
                            "type": "number"
                          },
                          "count": {
                            "type": "integer",
                            "minimum": 0
                          }
                        }
                      }
                    },
                    "byOwner": {
                      "type": "array",
                      "description": "Corte por vendedor (Deal.assignedTo); incluye los abiertos sin fecha.",
                      "items": {
                        "type": "object",
                        "required": [
                          "userId",
                          "name",
                          "weighted",
                          "amount",
                          "count"
                        ],
                        "properties": {
                          "userId": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "null = negocios sin dueño."
                          },
                          "name": {
                            "type": "string"
                          },
                          "weighted": {
                            "type": "number"
                          },
                          "amount": {
                            "type": "number"
                          },
                          "count": {
                            "type": "integer",
                            "minimum": 0
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Faltan fechas, formato distinto de YYYY-MM-DD o pipelineId inválido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/reports/sales/{date}": {
      "parameters": [
        {
          "name": "date",
          "in": "path",
          "required": true,
          "description": "Mes en formato YYYY-MM.",
          "schema": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}$"
          },
          "example": "2025-09"
        }
      ],
      "get": {
        "operationId": "getSalesByMonth",
        "tags": [
          "Reports"
        ],
        "summary": "Detalle de los negocios ganados de un mes",
        "description": "Requiere deals:read o *. Lo mismo que muestra /reports/sales/{date} en la app: todos los negocios GANADOS cuya fecha de cierre cae en el mes, con su contacto y sus campos personalizados, más el conteo y la suma. El rango se arma en UTC puro.",
        "x-required-permissions": [
          "deals:read"
        ],
        "responses": {
          "200": {
            "description": "Negocios ganados del mes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "month",
                    "startDate",
                    "endDate",
                    "total",
                    "totalAmount",
                    "deals"
                  ],
                  "properties": {
                    "month": {
                      "type": "string",
                      "description": "\"2025-09\", o \"2025-09 a 2025-11\" en un rango.",
                      "example": "2025-09"
                    },
                    "startDate": {
                      "type": "string",
                      "example": "2025-09"
                    },
                    "endDate": {
                      "type": "string",
                      "example": "2025-09"
                    },
                    "total": {
                      "type": "integer",
                      "minimum": 0,
                      "description": "Cantidad de negocios ganados en el rango."
                    },
                    "totalAmount": {
                      "type": "number",
                      "description": "Suma de sus montos."
                    },
                    "deals": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "_id",
                          "title",
                          "amount",
                          "closingDate",
                          "createdAt",
                          "status",
                          "contact",
                          "fields"
                        ],
                        "properties": {
                          "_id": {
                            "type": "string"
                          },
                          "title": {
                            "type": "string"
                          },
                          "amount": {
                            "type": "number",
                            "description": "0 si el negocio no tiene monto."
                          },
                          "closingDate": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "status": {
                            "type": [
                              "object",
                              "null"
                            ],
                            "description": "Etapa del negocio; null si quedó sin etapa.",
                            "properties": {
                              "_id": {
                                "type": "string"
                              },
                              "name": {
                                "type": "string"
                              },
                              "color": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              }
                            }
                          },
                          "contact": {
                            "type": "object",
                            "description": "Contacto asociado al negocio, aplanado desde sus propiedades.",
                            "required": [
                              "_id",
                              "firstName",
                              "lastName",
                              "email",
                              "phone"
                            ],
                            "properties": {
                              "_id": {
                                "type": "string",
                                "description": "Vacío si el negocio no tiene contacto asociado."
                              },
                              "firstName": {
                                "type": "string",
                                "description": "\"Sin nombre\" si el contacto no tiene firstName ni nombre."
                              },
                              "lastName": {
                                "type": "string"
                              },
                              "email": {
                                "type": "string"
                              },
                              "phone": {
                                "type": "string"
                              }
                            }
                          },
                          "fields": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "description": "Campo personalizado del negocio con su valor crudo.",
                              "required": [
                                "key",
                                "name",
                                "value"
                              ],
                              "properties": {
                                "key": {
                                  "type": "string"
                                },
                                "name": {
                                  "type": "string"
                                },
                                "value": {
                                  "type": [
                                    "string",
                                    "null"
                                  ]
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "month": {
                    "summary": "Septiembre de 2025",
                    "value": {
                      "month": "2025-09",
                      "startDate": "2025-09",
                      "endDate": "2025-09",
                      "total": 93,
                      "totalAmount": 60003801,
                      "deals": [
                        {
                          "_id": "68cc0635cb3dc52fb5481928",
                          "title": "18600 - Angelica Marin - Mision Carismatica",
                          "amount": 13099993,
                          "closingDate": "2025-09-18T00:00:00.000Z",
                          "createdAt": "2025-09-01T14:22:10.000Z",
                          "status": {
                            "_id": "66f1a2b3c4d5e6f708091a2b",
                            "name": "Cierre ganado",
                            "color": "#22c55e"
                          },
                          "contact": {
                            "_id": "66f1a2b3c4d5e6f708091a2c",
                            "firstName": "Angelica",
                            "lastName": "Marin",
                            "email": "angelica@example.com",
                            "phone": "+573001112233"
                          },
                          "fields": [
                            {
                              "key": "cantidad_de_manillas",
                              "name": "Cantidad",
                              "value": "18600"
                            }
                          ]
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Formato de mes inválido (se espera YYYY-MM).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/reports/sales/{date}/{endDate}": {
      "parameters": [
        {
          "name": "date",
          "in": "path",
          "required": true,
          "description": "Mes inicial (YYYY-MM), inclusive.",
          "schema": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}$"
          },
          "example": "2025-09"
        },
        {
          "name": "endDate",
          "in": "path",
          "required": true,
          "description": "Mes final (YYYY-MM), inclusive.",
          "schema": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}$"
          },
          "example": "2025-11"
        }
      ],
      "get": {
        "operationId": "getSalesByRange",
        "tags": [
          "Reports"
        ],
        "summary": "Detalle de los negocios ganados de un rango de meses",
        "description": "Requiere deals:read o *. Igual que el detalle de un mes, del primer día de `date` al último de `endDate`. `month` viene como \"2025-09 a 2025-11\".",
        "x-required-permissions": [
          "deals:read"
        ],
        "responses": {
          "200": {
            "description": "Negocios ganados del rango.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1reports~1sales~1{date}/get/responses/200/content/application~1json/schema"
                }
              }
            }
          },
          "400": {
            "description": "Formato de mes inválido (se espera YYYY-MM).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/reports/purchases/overview": {
      "get": {
        "operationId": "getPurchasesOverview",
        "tags": [
          "Reports"
        ],
        "summary": "Resumen de compras del período, con retenciones y top proveedores",
        "description": "Requiere purchases:read o * y el módulo de facturación electrónica del plan (el mismo que cubre compras y proveedores desde el 03-sep-2026). Devuelve gasto, IVA, retenciones por tipo, neto pagado y su comparación con el año anterior.",
        "x-required-permissions": [
          "purchases:read"
        ],
        "parameters": [
          {
            "name": "fromDate",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
            },
            "example": "2025-09-01"
          },
          {
            "name": "toDate",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
            },
            "example": "2025-09-30"
          }
        ],
        "responses": {
          "200": {
            "description": "Resumen de compras del período.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "summary",
                    "previousSummary",
                    "purchasesByMonth",
                    "topSuppliersWithDetails"
                  ],
                  "properties": {
                    "summary": {
                      "type": "object",
                      "properties": {
                        "totalPurchases": {
                          "type": "number"
                        },
                        "totalTax": {
                          "type": "number"
                        },
                        "totalWithholdings": {
                          "type": "number"
                        },
                        "totalNetPaid": {
                          "type": "number"
                        },
                        "withholdingsByKind": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "number"
                          }
                        },
                        "averageTicket": {
                          "type": "number"
                        },
                        "products": {
                          "type": [
                            "number",
                            "null"
                          ]
                        },
                        "newSuppliers": {
                          "type": "integer",
                          "minimum": 0
                        },
                        "fromDate": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "toDate": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "previousSummary": {
                      "type": "object",
                      "description": "Mismo período del año anterior.",
                      "additionalProperties": true
                    },
                    "purchasesByMonth": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    },
                    "topSuppliersWithDetails": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Faltan fromDate/toDate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/reports/purchases/{date}": {
      "parameters": [
        {
          "name": "date",
          "in": "path",
          "required": true,
          "description": "Mes en formato YYYY-MM.",
          "schema": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}$"
          },
          "example": "2025-09"
        }
      ],
      "get": {
        "operationId": "getPurchasesByMonth",
        "tags": [
          "Reports"
        ],
        "summary": "Detalle de las compras de un mes",
        "description": "Requiere purchases:read o * y el módulo de facturación electrónica del plan. `purchases` trae TODAS las órdenes del mes (pending, completed y canceled) para poder filtrar; los totales de la respuesta cuentan SOLO las completed. A diferencia del reporte de ventas, el rango de fechas se arma en la zona horaria del servidor.",
        "x-required-permissions": [
          "purchases:read"
        ],
        "responses": {
          "200": {
            "description": "Compras del mes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "month",
                    "startDate",
                    "endDate",
                    "total",
                    "totalAmount",
                    "totalWithholdings",
                    "totalNetAmount",
                    "purchases"
                  ],
                  "properties": {
                    "month": {
                      "type": "string",
                      "example": "2025-09"
                    },
                    "startDate": {
                      "type": "string",
                      "example": "2025-09"
                    },
                    "endDate": {
                      "type": "string",
                      "example": "2025-09"
                    },
                    "total": {
                      "type": "integer",
                      "minimum": 0,
                      "description": "Solo compras en estado completed."
                    },
                    "totalAmount": {
                      "type": "number",
                      "description": "Solo compras completed."
                    },
                    "totalWithholdings": {
                      "type": "number",
                      "description": "Solo compras completed."
                    },
                    "totalNetAmount": {
                      "type": "number",
                      "description": "Solo compras completed."
                    },
                    "purchases": {
                      "type": "array",
                      "description": "TODAS las compras del rango, incluidas pending y canceled (los totales no las cuentan).",
                      "items": {
                        "type": "object",
                        "required": [
                          "_id",
                          "orderNumber",
                          "total",
                          "purchaseDate",
                          "status",
                          "supplier",
                          "withholdingTotal",
                          "netPayable"
                        ],
                        "properties": {
                          "_id": {
                            "type": "string"
                          },
                          "orderNumber": {
                            "type": [
                              "string",
                              "number",
                              "null"
                            ]
                          },
                          "total": {
                            "type": "number"
                          },
                          "purchaseDate": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "pending",
                              "completed",
                              "canceled"
                            ]
                          },
                          "supplier": {
                            "type": "object",
                            "properties": {
                              "_id": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "name": {
                                "type": "string"
                              },
                              "nit": {
                                "type": "string"
                              }
                            }
                          },
                          "tax": {
                            "type": "number"
                          },
                          "withholdings": {
                            "type": [
                              "object",
                              "null"
                            ],
                            "description": "Retenciones calculadas de la compra.",
                            "additionalProperties": true
                          },
                          "withholdingTotal": {
                            "type": "number"
                          },
                          "netPayable": {
                            "type": "number",
                            "description": "total − retenciones."
                          },
                          "items": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "additionalProperties": true
                            }
                          },
                          "description": {
                            "type": "string"
                          },
                          "paymentMethod": {
                            "type": "string"
                          },
                          "voucherType": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Formato de mes inválido (se espera YYYY-MM).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/reports/purchases/{date}/{endDate}": {
      "parameters": [
        {
          "name": "date",
          "in": "path",
          "required": true,
          "description": "Mes inicial (YYYY-MM), inclusive.",
          "schema": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}$"
          },
          "example": "2025-09"
        },
        {
          "name": "endDate",
          "in": "path",
          "required": true,
          "description": "Mes final (YYYY-MM), inclusive.",
          "schema": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}$"
          },
          "example": "2025-11"
        }
      ],
      "get": {
        "operationId": "getPurchasesByRange",
        "tags": [
          "Reports"
        ],
        "summary": "Detalle de las compras de un rango de meses",
        "description": "Requiere purchases:read o * y el módulo de facturación electrónica del plan. Igual que el detalle de un mes, del primer día de `date` al último de `endDate`.",
        "x-required-permissions": [
          "purchases:read"
        ],
        "responses": {
          "200": {
            "description": "Compras del rango.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1reports~1purchases~1{date}/get/responses/200/content/application~1json/schema"
                }
              }
            }
          },
          "400": {
            "description": "Formato de mes inválido (se espera YYYY-MM).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/agents": {
      "get": {
        "operationId": "listAgents",
        "tags": [
          "Agents"
        ],
        "summary": "Listar agentes IA",
        "description": "Requiere agents:read o * (desde el 05-sep-2026; antes bastaba cualquier token válido). El mismo router sirve la API interna con sesión, donde no aplica permiso de token. Devuelve todos los agentes de la organización sin paginar ni filtrar, ordenados por updatedAt descendente, con una proyección resumida (sin systemPrompt, tools ni triggers) y las constantes de validación del modelo en limits. Contrato propio: sin success ni data, y los IDs vienen como _id.",
        "x-required-permissions": [
          "agents:read"
        ],
        "responses": {
          "200": {
            "description": "Agentes y límites (agents puede ser []).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentListResult"
                },
                "examples": {
                  "list": {
                    "value": {
                      "agents": [
                        {
                          "_id": "66f1000000000000000000f1",
                          "name": "Asesor ventas WhatsApp",
                          "description": "Atiende y califica leads de manillas",
                          "status": "active",
                          "provider": "openai",
                          "aiModel": "gpt-4o-mini",
                          "stats": {
                            "totalRuns": 812,
                            "totalInputTokens": 1930400,
                            "totalOutputTokens": 96500,
                            "totalCostUsd": 0.35,
                            "lastRunAt": "2026-09-05T14:58:00.000Z",
                            "handoffsToHuman": 41,
                            "errorsCount": 3
                          },
                          "createdAt": "2026-06-10T13:00:00.000Z",
                          "updatedAt": "2026-09-05T14:58:00.000Z"
                        },
                        {
                          "_id": "66f1000000000000000000f2",
                          "name": "Recuperación de carritos",
                          "status": "draft",
                          "provider": "openai",
                          "aiModel": "gpt-4o-mini",
                          "stats": {
                            "totalRuns": 0,
                            "totalInputTokens": 0,
                            "totalOutputTokens": 0,
                            "totalCostUsd": 0,
                            "handoffsToHuman": 0,
                            "errorsCount": 0
                          },
                          "createdAt": "2026-09-01T09:00:00.000Z",
                          "updatedAt": "2026-09-01T09:00:00.000Z"
                        }
                      ],
                      "limits": {
                        "NAME_MAX": 100,
                        "DESCRIPTION_MAX": 500,
                        "SYSTEM_PROMPT_MAX": 30000,
                        "HANDOFF_MESSAGE_MAX": 400,
                        "WELCOME_MESSAGE_MAX": 600,
                        "HANDOFF_KEYWORD_MAX": 60,
                        "MAX_HANDOFF_KEYWORDS": 30,
                        "MAX_TRIGGERS": 10,
                        "MAX_ENABLED_TOOLS": 30,
                        "MAX_TOKENS_HARD_CAP": 12000,
                        "MAX_TOOL_CALLS_HARD_CAP": 15,
                        "MESSAGES_LIMIT_MIN": 1,
                        "MESSAGES_LIMIT_MAX": 50,
                        "PATTERN_WINDOW_MIN": 5,
                        "PATTERN_WINDOW_MAX": 200,
                        "DEDUPE_SIMILARITY_MIN": 0,
                        "DEDUPE_SIMILARITY_MAX": 1,
                        "BATCH_WINDOW_MIN": 0,
                        "BATCH_WINDOW_MAX": 20,
                        "BATCH_MAX_WAIT_MIN": 5,
                        "BATCH_MAX_WAIT_MAX": 60,
                        "LEAD_STATE_SCHEMA_MAX_FIELDS": 20,
                        "LEAD_STATE_FIELD_KEY_MAX": 40,
                        "LEAD_STATE_FIELD_LABEL_MAX": 80,
                        "LEAD_STATE_FIELD_DESC_MAX": 200
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "description": "Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentError"
                },
                "example": {
                  "message": "Error listando agentes"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createAgent",
        "tags": [
          "Agents"
        ],
        "summary": "Crear un agente IA",
        "description": "Requiere agents:write o *. name y systemPrompt obligatorios. Nace en status draft salvo que el body diga otra cosa (se puede crear directamente active, y con autoAssignNewConversations true empieza a tomar las conversaciones nuevas de WhatsApp). createdBy es el usuario dueño del token. El body se pasa al modelo salvo los campos de sistema (organizationId, createdBy, stats se descartan; desde el 05-sep-2026); los campos desconocidos se descartan. No valida que integrationId exista ni que enabledTools sean nombres del registro. Responde 201 con el documento completo.",
        "x-required-permissions": [
          "agents:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AgentCreate"
              },
              "examples": {
                "minimal": {
                  "summary": "Solo los obligatorios (queda en draft con defaults).",
                  "value": {
                    "name": "Recuperación de carritos",
                    "systemPrompt": "Eres un asistente que retoma conversaciones abandonadas con amabilidad."
                  }
                },
                "full": {
                  "summary": "Agente activo con trigger, tools y esquema de lead.",
                  "value": {
                    "name": "Asesor ventas WhatsApp",
                    "description": "Atiende y califica leads de manillas",
                    "systemPrompt": "Eres el asesor comercial de Manillas de Control. Hoy es {{FECHA_ACTUAL}}. Responde breve y cierra con una pregunta.",
                    "integrationId": "66f1000000000000000000b1",
                    "aiModel": "gpt-4o-mini",
                    "temperature": 0.4,
                    "maxTokens": 400,
                    "enabledTools": [
                      "update_lead_state",
                      "search_knowledge_base"
                    ],
                    "triggers": [
                      {
                        "type": "message_received",
                        "sourceFilters": [
                          "whatsapp"
                        ],
                        "excludeIfTagsPresent": [
                          "no-bot"
                        ]
                      }
                    ],
                    "autoAssignNewConversations": true,
                    "leadStateSchema": [
                      {
                        "key": "producto",
                        "label": "Producto solicitado",
                        "required": true
                      },
                      {
                        "key": "cantidad",
                        "label": "Cantidad",
                        "description": "Unidades que necesita",
                        "required": true
                      }
                    ],
                    "status": "active"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Agente creado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentResult"
                },
                "examples": {
                  "created": {
                    "summary": "Creado solo con name y systemPrompt (defaults del modelo).",
                    "value": {
                      "agent": {
                        "_id": "66f1000000000000000000f2",
                        "organizationId": "66f1000000000000000000a1",
                        "name": "Recuperación de carritos",
                        "systemPrompt": "Eres un asistente que retoma conversaciones abandonadas con amabilidad.",
                        "provider": "openai",
                        "aiModel": "gpt-4o-mini",
                        "temperature": 0.4,
                        "maxTokens": 400,
                        "enabledTools": [],
                        "maxToolCallsPerRun": 5,
                        "handoffKeywords": [
                          "hablar con humano",
                          "asesor",
                          "persona real",
                          "agente humano"
                        ],
                        "handoffMessage": "Un momento, te voy a conectar con un asesor humano que te atenderá personalmente.",
                        "triggers": [],
                        "autoAssignNewConversations": false,
                        "contextSettings": {
                          "messagesLimit": 20,
                          "patternWindow": 50,
                          "dedupeSimilarityThreshold": 0.85,
                          "batchWindowSeconds": 8,
                          "batchMaxWaitSeconds": 20
                        },
                        "leadStateSchema": [],
                        "status": "draft",
                        "stats": {
                          "totalRuns": 0,
                          "totalInputTokens": 0,
                          "totalOutputTokens": 0,
                          "totalCostUsd": 0,
                          "handoffsToHuman": 0,
                          "errorsCount": 0
                        },
                        "createdBy": "66f1000000000000000000c1",
                        "createdAt": "2026-09-01T09:00:00.000Z",
                        "updatedAt": "2026-09-01T09:00:00.000Z",
                        "__v": 0
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Falta name o systemPrompt, systemPrompt demasiado largo, o validación del modelo (errors por campo).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "message": "name y systemPrompt son requeridos"
                    }
                  },
                  "tooLong": {
                    "value": {
                      "message": "systemPrompt excede 30000 caracteres"
                    }
                  },
                  "validation": {
                    "value": {
                      "message": "Datos inválidos",
                      "errors": {
                        "temperature": "Path `temperature` (3) is more than maximum allowed value (2).",
                        "triggers": "Máximo 10 triggers por agente"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "description": "Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentError"
                },
                "example": {
                  "message": "Error creando agente"
                }
              }
            }
          }
        }
      }
    },
    "/agents/tools": {
      "get": {
        "operationId": "listAgentTools",
        "tags": [
          "Agents"
        ],
        "summary": "Listar los tools disponibles para agentes",
        "description": "Requiere agents:read o *. No consulta la organización: devuelve el registro estático de tools (nombre y descripción para el LLM) que se pueden poner en enabledTools. No incluye los parámetros de cada tool.",
        "x-required-permissions": [
          "agents:read"
        ],
        "responses": {
          "200": {
            "description": "Registro de tools.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentToolListResult"
                },
                "examples": {
                  "tools": {
                    "summary": "Los 8 tools del registro (descripciones recortadas a la primera frase).",
                    "value": {
                      "tools": [
                        {
                          "name": "move_conversation_stage",
                          "description": "Mueve la conversación actual a otra etapa del MISMO pipeline."
                        },
                        {
                          "name": "assign_to_advisor",
                          "description": "Asigna la conversación a un asesor humano y deja de responder automáticamente."
                        },
                        {
                          "name": "add_conversation_tags",
                          "description": "Agrega una o más etiquetas a la conversación actual SIN eliminar las existentes."
                        },
                        {
                          "name": "create_contact",
                          "description": "Crea un contacto nuevo en el CRM."
                        },
                        {
                          "name": "handoff_to_human",
                          "description": "Transfiere la conversación a atención humana: pausa la automatización y libera al agente IA."
                        },
                        {
                          "name": "add_conversation_note",
                          "description": "Agrega una nota interna a la conversación",
                          "visible para el equipo humano pero NO para el cliente.": null
                        },
                        {
                          "name": "update_lead_state",
                          "description": "Actualiza el ESTADO ACTUAL DEL LEAD con los datos que vas capturando del cliente."
                        },
                        {
                          "name": "search_knowledge_base",
                          "description": "Busca información en la base de conocimiento de la empresa (documentos que el negocio subió: catálogos, precios, información de productos, políticas, FAQs)."
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "description": "Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentError"
                },
                "example": {
                  "message": "Error listando herramientas"
                }
              }
            }
          }
        }
      }
    },
    "/agents/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB del agente (campo _id de las respuestas). Un valor que no es ObjectId responde 400.",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "66f1000000000000000000f1"
        }
      ],
      "get": {
        "operationId": "getAgentById",
        "tags": [
          "Agents"
        ],
        "summary": "Consultar un agente por ID",
        "description": "Requiere agents:read o *. Scoped a la organización del token. Devuelve el documento completo (incluido systemPrompt) más limits.",
        "x-required-permissions": [
          "agents:read"
        ],
        "responses": {
          "200": {
            "description": "Agente encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentDetailResult"
                },
                "examples": {
                  "found": {
                    "summary": "Agente activo con un trigger y dos tools.",
                    "value": {
                      "agent": {
                        "_id": "66f1000000000000000000f1",
                        "organizationId": "66f1000000000000000000a1",
                        "name": "Asesor ventas WhatsApp",
                        "description": "Atiende y califica leads de manillas",
                        "systemPrompt": "Eres el asesor comercial de Manillas de Control. Hoy es {{FECHA_ACTUAL}}. Responde breve y cierra con una pregunta.",
                        "integrationId": "66f1000000000000000000b1",
                        "provider": "openai",
                        "aiModel": "gpt-4o-mini",
                        "temperature": 0.4,
                        "maxTokens": 400,
                        "enabledTools": [
                          "update_lead_state",
                          "search_knowledge_base"
                        ],
                        "maxToolCallsPerRun": 5,
                        "handoffKeywords": [
                          "hablar con humano",
                          "asesor",
                          "persona real",
                          "agente humano"
                        ],
                        "handoffMessage": "Un momento, te voy a conectar con un asesor humano que te atenderá personalmente.",
                        "triggers": [
                          {
                            "_id": "66f1000000000000000000f9",
                            "type": "message_received",
                            "isActive": true,
                            "priority": 10,
                            "sourceFilters": [
                              "whatsapp"
                            ],
                            "stageIds": [],
                            "pipelineIds": [],
                            "keywords": [],
                            "excludeIfAssignedToHuman": true,
                            "excludeIfTagsPresent": [
                              "no-bot"
                            ],
                            "requireTagsPresent": []
                          }
                        ],
                        "autoAssignNewConversations": true,
                        "contextSettings": {
                          "messagesLimit": 20,
                          "patternWindow": 50,
                          "dedupeSimilarityThreshold": 0.85,
                          "batchWindowSeconds": 8,
                          "batchMaxWaitSeconds": 20
                        },
                        "leadStateSchema": [
                          {
                            "key": "producto",
                            "label": "Producto solicitado",
                            "required": true
                          },
                          {
                            "key": "cantidad",
                            "label": "Cantidad",
                            "description": "Unidades que necesita",
                            "required": true
                          },
                          {
                            "key": "ciudad",
                            "label": "Ciudad de entrega",
                            "required": false
                          }
                        ],
                        "status": "active",
                        "stats": {
                          "totalRuns": 812,
                          "totalInputTokens": 1930400,
                          "totalOutputTokens": 96500,
                          "totalCostUsd": 0.35,
                          "lastRunAt": "2026-09-05T14:58:00.000Z",
                          "handoffsToHuman": 41,
                          "errorsCount": 3
                        },
                        "createdBy": "66f1000000000000000000c1",
                        "updatedBy": "66f1000000000000000000c1",
                        "createdAt": "2026-06-10T13:00:00.000Z",
                        "updatedAt": "2026-09-05T14:58:00.000Z",
                        "__v": 0
                      },
                      "limits": {
                        "NAME_MAX": 100,
                        "DESCRIPTION_MAX": 500,
                        "SYSTEM_PROMPT_MAX": 30000,
                        "HANDOFF_MESSAGE_MAX": 400,
                        "WELCOME_MESSAGE_MAX": 600,
                        "HANDOFF_KEYWORD_MAX": 60,
                        "MAX_HANDOFF_KEYWORDS": 30,
                        "MAX_TRIGGERS": 10,
                        "MAX_ENABLED_TOOLS": 30,
                        "MAX_TOKENS_HARD_CAP": 12000,
                        "MAX_TOOL_CALLS_HARD_CAP": 15,
                        "MESSAGES_LIMIT_MIN": 1,
                        "MESSAGES_LIMIT_MAX": 50,
                        "PATTERN_WINDOW_MIN": 5,
                        "PATTERN_WINDOW_MAX": 200,
                        "DEDUPE_SIMILARITY_MIN": 0,
                        "DEDUPE_SIMILARITY_MAX": 1,
                        "BATCH_WINDOW_MIN": 0,
                        "BATCH_WINDOW_MAX": 20,
                        "BATCH_MAX_WAIT_MIN": 5,
                        "BATCH_MAX_WAIT_MAX": 60,
                        "LEAD_STATE_SCHEMA_MAX_FIELDS": 20,
                        "LEAD_STATE_FIELD_KEY_MAX": 40,
                        "LEAD_STATE_FIELD_LABEL_MAX": 80,
                        "LEAD_STATE_FIELD_DESC_MAX": 200
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID de agente inválido (no es un ObjectId).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentError"
                },
                "example": {
                  "message": "ID inválido"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Agente inexistente o no perteneciente a la organización del token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentError"
                },
                "example": {
                  "message": "Agente no encontrado"
                }
              }
            }
          },
          "500": {
            "description": "Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentError"
                },
                "example": {
                  "message": "Error obteniendo agente"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "updateAgent",
        "tags": [
          "Agents"
        ],
        "summary": "Actualizar un agente",
        "description": "Requiere agents:write o *. Actualización parcial: contextSettings se aplica clave por clave (un contextSettings parcial conserva las demás claves, desde el 05-sep-2026); el resto de claves enviadas se sobrescriben completas (triggers y leadStateSchema se reemplazan enteros). organizationId, createdBy y stats se descartan del body. Corre validadores del modelo (400 \"Datos inválidos\"). Registra updatedBy con el usuario del token. Responde el documento completo sin limits.",
        "x-required-permissions": [
          "agents:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AgentUpdate"
              },
              "examples": {
                "prompt": {
                  "summary": "Cambiar el prompt y el modelo.",
                  "value": {
                    "systemPrompt": "Eres el asesor comercial. Responde en máximo 2 frases.",
                    "aiModel": "gpt-4o"
                  }
                },
                "context": {
                  "summary": "Ajustar contexto (enviar TODAS las claves para no resetear las demás).",
                  "value": {
                    "contextSettings": {
                      "messagesLimit": 30,
                      "patternWindow": 80,
                      "dedupeSimilarityThreshold": 0.9,
                      "batchWindowSeconds": 5,
                      "batchMaxWaitSeconds": 15
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Agente actualizado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentResult"
                },
                "examples": {
                  "updated": {
                    "summary": "Tras PATCH status paused.",
                    "value": {
                      "agent": {
                        "_id": "66f1000000000000000000f1",
                        "organizationId": "66f1000000000000000000a1",
                        "name": "Asesor ventas WhatsApp",
                        "description": "Atiende y califica leads de manillas",
                        "systemPrompt": "Eres el asesor comercial de Manillas de Control. Hoy es {{FECHA_ACTUAL}}. Responde breve y cierra con una pregunta.",
                        "integrationId": "66f1000000000000000000b1",
                        "provider": "openai",
                        "aiModel": "gpt-4o-mini",
                        "temperature": 0.4,
                        "maxTokens": 400,
                        "enabledTools": [
                          "update_lead_state",
                          "search_knowledge_base"
                        ],
                        "maxToolCallsPerRun": 5,
                        "handoffKeywords": [
                          "hablar con humano",
                          "asesor",
                          "persona real",
                          "agente humano"
                        ],
                        "handoffMessage": "Un momento, te voy a conectar con un asesor humano que te atenderá personalmente.",
                        "triggers": [],
                        "autoAssignNewConversations": true,
                        "contextSettings": {
                          "messagesLimit": 20,
                          "patternWindow": 50,
                          "dedupeSimilarityThreshold": 0.85,
                          "batchWindowSeconds": 8,
                          "batchMaxWaitSeconds": 20
                        },
                        "leadStateSchema": [],
                        "status": "paused",
                        "stats": {
                          "totalRuns": 812,
                          "totalInputTokens": 1930400,
                          "totalOutputTokens": 96500,
                          "totalCostUsd": 0.35,
                          "lastRunAt": "2026-09-05T14:58:00.000Z",
                          "handoffsToHuman": 41,
                          "errorsCount": 3
                        },
                        "createdBy": "66f1000000000000000000c1",
                        "updatedBy": "66f1000000000000000000c1",
                        "createdAt": "2026-06-10T13:00:00.000Z",
                        "updatedAt": "2026-09-06T10:30:00.000Z",
                        "__v": 0
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID inválido, systemPrompt demasiado largo o validación del modelo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentError"
                },
                "examples": {
                  "invalidId": {
                    "value": {
                      "message": "ID inválido"
                    }
                  },
                  "tooLong": {
                    "value": {
                      "message": "systemPrompt excede 30000 caracteres"
                    }
                  },
                  "validation": {
                    "value": {
                      "message": "Datos inválidos",
                      "errors": {
                        "maxTokens": "Path `maxTokens` (20000) is more than maximum allowed value (12000)."
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1agents~1{id}/get/responses/404"
          },
          "500": {
            "description": "Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentError"
                },
                "example": {
                  "message": "Error actualizando agente"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteAgent",
        "tags": [
          "Agents"
        ],
        "summary": "Eliminar un agente",
        "description": "Requiere agents:write o *. Borra el documento y suelta las conversaciones que lo tenían asignado (assignedAgentId; releasedConversations dice cuántas, desde el 05-sep-2026). No borra su historial de ejecuciones (AgentRun). Responde 200 con mensaje, no 204.",
        "x-required-permissions": [
          "agents:write"
        ],
        "responses": {
          "200": {
            "description": "Agente eliminado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentDeleteResult"
                },
                "example": {
                  "message": "Agente eliminado",
                  "releasedConversations": 2
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1agents~1{id}/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1agents~1{id}/get/responses/404"
          },
          "500": {
            "description": "Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentError"
                },
                "example": {
                  "message": "Error eliminando agente"
                }
              }
            }
          }
        }
      }
    },
    "/agents/{id}/status": {
      "parameters": [
        {
          "$ref": "#/paths/~1agents~1{id}/parameters/0"
        }
      ],
      "patch": {
        "operationId": "setAgentStatus",
        "tags": [
          "Agents"
        ],
        "summary": "Activar, pausar o dejar en borrador un agente",
        "description": "Requiere agents:write o *. Solo cambia status (active | paused | draft; otro valor → 400) y updatedBy. No corre los validadores del modelo. Responde el documento completo sin limits.",
        "x-required-permissions": [
          "agents:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AgentStatusUpdate"
              },
              "examples": {
                "pause": {
                  "value": {
                    "status": "paused"
                  }
                },
                "activate": {
                  "value": {
                    "status": "active"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Estado cambiado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentResult"
                },
                "examples": {
                  "paused": {
                    "$ref": "#/paths/~1agents~1{id}/put/responses/200/content/application~1json/examples/updated"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ID inválido o status fuera del enum.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentError"
                },
                "examples": {
                  "invalidId": {
                    "value": {
                      "message": "ID inválido"
                    }
                  },
                  "invalidStatus": {
                    "value": {
                      "message": "Status inválido"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1agents~1{id}/get/responses/404"
          },
          "500": {
            "description": "Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentError"
                },
                "example": {
                  "message": "Error cambiando estado"
                }
              }
            }
          }
        }
      }
    },
    "/agent-context/by-phone/{mobile}": {
      "parameters": [
        {
          "name": "mobile",
          "in": "path",
          "required": true,
          "description": "Número de WhatsApp del cliente. Se eliminan todos los caracteres no numéricos (+57 300 123 4567 → 573001234567) y se compara EXACTO contra participants.contact.reference de la conversación, así que debe llevar el indicativo de país tal como lo guarda el CRM. Si tras limpiar no queda ningún dígito responde 400.",
          "schema": {
            "type": "string"
          },
          "example": "573001234567"
        }
      ],
      "get": {
        "operationId": "getAgentContextByPhone",
        "tags": [
          "AgentContext"
        ],
        "summary": "Contexto del cliente para un agente IA, por número de WhatsApp",
        "description": "Requiere conversations:read o * (desde el 05-sep-2026; antes bastaba cualquier token válido): devuelve historial de mensajes, contacto y negocios, lo mismo que protege ese permiso en /conversations. El router también sirve la API interna con sesión, donde no aplica permiso de token. Arma, para la organización del token, el contexto que reciben los agentes: contacto (por la propiedad mobile), conversación (por participants.contact.reference; es obligatoria: sin conversación responde 404 found:false aunque el contacto exista), últimos mensajes, negocios del contacto y heurísticas de patrón. Devuelve además el mismo contexto renderizado como bloque de texto para un system prompt. Con format=prompt responde SOLO ese bloque como text/plain. messages y deals se parsean con parseInt sin validar: un valor no numérico produce NaN y desactiva los recortes (recentMessages puede salir vacío). Solo lectura, sin efectos.",
        "x-required-permissions": [
          "conversations:read"
        ],
        "parameters": [
          {
            "name": "messages",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 30,
              "default": 6
            },
            "description": "Cuántos mensajes recientes incluir (se acota a 1..30)."
          },
          {
            "name": "deals",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 20,
              "default": 5
            },
            "description": "Cuántos negocios incluir (se acota a 0..20; 0 omite la consulta)."
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "full",
                "compact",
                "prompt"
              ],
              "default": "full"
            },
            "description": "full = JSON completo; compact = solo bloque y tokens; prompt = text/plain. Cualquier otro valor equivale a full."
          }
        ],
        "responses": {
          "200": {
            "description": "Contexto encontrado. La forma depende de format.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/AgentContextFullResult"
                    },
                    {
                      "$ref": "#/components/schemas/AgentContextCompactResult"
                    }
                  ]
                },
                "examples": {
                  "full": {
                    "summary": "Cliente recurrente con un deal y dos mensajes.",
                    "value": {
                      "found": true,
                      "data": {
                        "contact": {
                          "id": "507f1f77bcf86cd799439011",
                          "firstName": "Ana",
                          "lastName": "Ejemplo",
                          "fullName": "Ana Ejemplo",
                          "email": "ana@example.com",
                          "mobile": "573001234567",
                          "company": "Eventos Ana",
                          "position": "",
                          "leadScore": 12,
                          "tags": []
                        },
                        "conversation": {
                          "id": "66f1000000000000000000c9",
                          "title": "Ana Ejemplo",
                          "source": "whatsapp",
                          "priority": "medium",
                          "pipeline": {
                            "id": "6814ef02e3de1af46109d105",
                            "name": "Pipeline de Conversaciones"
                          },
                          "currentStage": {
                            "id": "68fa9280f6634d7b288fdc75",
                            "name": "Pedidos Medellín",
                            "order": 4
                          },
                          "assignedTo": null,
                          "tags": [
                            "recompra"
                          ],
                          "isPausedByHuman": false,
                          "unreadCount": 1,
                          "createdAt": "2026-08-20T13:45:00.000Z",
                          "lastMessageAt": "2026-09-05T14:58:00.000Z"
                        },
                        "recentMessages": [
                          {
                            "direction": "outgoing",
                            "type": "text",
                            "message": "Listo Ana, tu pedido sale mañana.",
                            "timestamp": "2026-09-04T21:10:00.000Z",
                            "hasMedia": false
                          },
                          {
                            "direction": "incoming",
                            "type": "text",
                            "message": "Parce, ¿me mandas la guía del envío?",
                            "timestamp": "2026-09-05T14:58:00.000Z",
                            "hasMedia": false
                          }
                        ],
                        "deals": {
                          "total": 1,
                          "totalRevenue": 450000,
                          "avgTicket": 450000,
                          "lastDeal": {
                            "title": "500 manillas Tyvek",
                            "amount": 450000,
                            "closingDate": "2026-09-01T05:00:00.000Z",
                            "status": "Cerrado Ganado"
                          },
                          "daysSinceLastDeal": 4,
                          "list": [
                            {
                              "title": "500 manillas Tyvek",
                              "amount": 450000,
                              "closingDate": "2026-09-01T05:00:00.000Z",
                              "status": "Cerrado Ganado"
                            }
                          ]
                        },
                        "pattern": {
                          "tone": "neutral",
                          "isRecurringCustomer": false,
                          "autoReopenCount": 0,
                          "hoursSinceLastIncoming": 1,
                          "lastIncomingHasReply": false,
                          "mentionedPendingPayment": false,
                          "mentionsPickup": false,
                          "mentionsDispatch": true
                        }
                      },
                      "formattedPromptBlock": "CONTACTO: Ana Ejemplo (id 507f1f77bcf86cd799439011, mobile 573001234567)\n  leadScore: 12 · empresa: Eventos Ana · email: ana@example.com\n  tags conversación: [recompra]\n\nCONVERSACIÓN: Pipeline de Conversaciones → \"Pedidos Medellín\"\n  canal: whatsapp · prioridad: medium\n\nPATRÓN DETECTADO:\n  - Cliente nuevo, tono neutral\n  - Habla de despacho/envío\n  - ⚠ 1h sin respuesta al último mensaje entrante\n\nHISTORIAL COMERCIAL:\n  - 1 deals · revenue total: $450.000 · ticket promedio: $450.000\n  - Último deal: \"500 manillas Tyvek\" $450.000 (2026-09-01, Cerrado Ganado)\n  - Días desde último deal: 4\n\nÚLTIMOS 2 MENSAJES (más antiguo → más reciente):\n  [2026-09-04 21:10 asesor] Listo Ana, tu pedido sale mañana.\n  [2026-09-05 14:58 cliente] Parce, ¿me mandas la guía del envío?",
                      "estimatedTokens": 172
                    }
                  },
                  "compact": {
                    "value": {
                      "found": true,
                      "estimatedTokens": 172,
                      "promptBlock": "CONTACTO: Ana Ejemplo (id 507f1f77bcf86cd799439011, mobile 573001234567)\n  leadScore: 12 · empresa: Eventos Ana · email: ana@example.com\n\nCONVERSACIÓN: Pipeline de Conversaciones → \"Pedidos Medellín\"\n  canal: whatsapp · prioridad: medium\n\nPATRÓN DETECTADO:\n  - Cliente nuevo, tono neutral\n\nÚLTIMOS 2 MENSAJES (más antiguo → más reciente):\n  [2026-09-04 21:10 asesor] Listo Ana, tu pedido sale mañana.\n  [2026-09-05 14:58 cliente] Parce, ¿me mandas la guía del envío?"
                    }
                  }
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "CONTACTO: Ana Ejemplo (id 507f1f77bcf86cd799439011, mobile 573001234567)\n  leadScore: 12 · empresa: Eventos Ana · email: ana@example.com\n\nCONVERSACIÓN: Pipeline de Conversaciones → \"Pedidos Medellín\"\n  canal: whatsapp · prioridad: medium"
              }
            }
          },
          "400": {
            "description": "mobile vacío o sin dígitos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentContextError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "message": "Falta el parámetro mobile"
                    }
                  },
                  "invalid": {
                    "value": {
                      "message": "Mobile inválido"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "No hay conversación con ese número en la organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentContextNotFound"
                },
                "examples": {
                  "notFound": {
                    "$ref": "#/paths/~1agent-context~1test/post/responses/404/content/application~1json/examples/notFound"
                  }
                }
              }
            }
          },
          "500": {
            "description": "Error interno (error solo viene en NODE_ENV=development).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentContextError"
                },
                "example": {
                  "message": "Error construyendo contexto del agente"
                }
              }
            }
          }
        }
      }
    },
    "/agent-context/test": {
      "post": {
        "operationId": "testAgentResponse",
        "tags": [
          "AgentContext"
        ],
        "summary": "Probar una respuesta de agente con OpenAI (sin enviar nada)",
        "description": "Requiere agents:write o * (desde el 05-sep-2026): es herramienta del módulo de agentes, consume créditos OpenAI de la organización y expone el mismo contexto que GET by-phone. EFECTO EXTERNO Y COSTO: construye el contexto del número (igual que GET by-phone) y llama a la API de OpenAI (chat.completions) con la API key de la integración openai de la organización — obligatoria: sin ella responde 500 \"No hay integración OpenAI activa para esta organización\". No envía la respuesta al cliente ni la guarda; es una herramienta de prueba de prompts. Si no se envía systemPromptTemplate usa una plantilla neutra de asistente de ventas con el nombre de la organización del token (desde el 05-sep-2026; antes venía fija a una empresa), y NO usa la configuración de los agentes guardados (ni tools ni leadStateSchema). Cualquier excepción (OpenAI, modelo inexistente, red) responde 500; el texto de la excepción solo viene en error con NODE_ENV=development (en producción se omite, salvo el mensaje propio de \"sin integración OpenAI\").",
        "x-required-permissions": [
          "agents:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AgentTestRequest"
              },
              "examples": {
                "minimal": {
                  "value": {
                    "mobile": "573001234567",
                    "message": "¿Me mandas la guía del envío?"
                  }
                },
                "customPrompt": {
                  "summary": "Con plantilla propia, otro modelo y más contexto.",
                  "value": {
                    "mobile": "+57 300 123 4567",
                    "message": "¿Cuánto vale la manilla Tyvek por 500?",
                    "systemPromptTemplate": "Eres el asesor comercial. Hoy es {{FECHA_ACTUAL}}. Responde en 2 frases.",
                    "model": "gpt-4o",
                    "temperature": 0.2,
                    "maxTokens": 200,
                    "messagesLimit": 10
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Respuesta generada con métricas de tiempo y tokens.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentTestResult"
                },
                "examples": {
                  "reply": {
                    "value": {
                      "ok": true,
                      "reply": "¡Claro, Ana! Te envío la guía de Servientrega en un momento. ¿La necesitas también al correo?",
                      "meta": {
                        "model": "gpt-4o-mini",
                        "contactName": "Ana Ejemplo",
                        "stage": "Pedidos Medellín",
                        "pattern": {
                          "tone": "neutral",
                          "isRecurringCustomer": false,
                          "autoReopenCount": 0,
                          "hoursSinceLastIncoming": 1,
                          "lastIncomingHasReply": false,
                          "mentionedPendingPayment": false,
                          "mentionsPickup": false,
                          "mentionsDispatch": true
                        },
                        "timings": {
                          "contextMs": 48,
                          "llmMs": 1320,
                          "totalMs": 1375
                        },
                        "tokens": {
                          "systemEstimated": 512,
                          "userEstimated": 10,
                          "contextOnlyEstimated": 172,
                          "openaiUsage": {
                            "prompt_tokens": 498,
                            "completion_tokens": 31,
                            "total_tokens": 529
                          }
                        }
                      },
                      "systemPromptPreview": "Eres un asistente de ventas de Manillas de Control, una empresa colombiana que fabrica manillas de control de acceso (Tyvek, vinilo, tela, silicona) para eventos, hospitales, conciertos y empresas.\n\n---\nCONTEXTO DEL CLIENTE:\nCONTACTO: Ana Ejemplo (id 507f1f77bcf86cd799439011, mobile 573001234567)"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "mobile o message ausentes o no son cadenas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentContextError"
                },
                "examples": {
                  "mobile": {
                    "value": {
                      "message": "mobile es requerido"
                    }
                  },
                  "message": {
                    "value": {
                      "message": "message es requerido"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "No hay conversación con ese número en la organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentContextNotFound"
                },
                "examples": {
                  "notFound": {
                    "value": {
                      "found": false,
                      "reason": "conversation_not_found",
                      "mobile": "573009999999"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Sin integración OpenAI (error siempre presente), o error de OpenAI/interno (error y stack solo en development).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentContextError"
                },
                "examples": {
                  "noIntegration": {
                    "value": {
                      "message": "Error en test del agente",
                      "error": "No hay integración OpenAI activa para esta organización"
                    }
                  },
                  "openai": {
                    "summary": "Error de OpenAI en producción (sin detalle).",
                    "value": {
                      "message": "Error en test del agente"
                    }
                  },
                  "openaiDev": {
                    "summary": "El mismo error con NODE_ENV=development.",
                    "value": {
                      "message": "Error en test del agente",
                      "error": "404 The model `gpt-5-turbo` does not exist or you do not have access to it."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/deal-pipelines": {
      "get": {
        "operationId": "listDealPipelines",
        "tags": [
          "Deal pipelines"
        ],
        "summary": "Listar pipelines de negocios con sus etapas",
        "description": "Requiere pipelines:read o *. Cada pipeline trae sus etapas ordenadas por order, con probabilidad, días máximos en la etapa (maxDaysInStage), marcas de ganada/perdida y el número de negocios en cada una. Orden de creación (el primero es el que la app abre por defecto).",
        "x-required-permissions": [
          "pipelines:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Pipelines y paginación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealPipelineListResult"
                },
                "examples": {
                  "list": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "66f10000000000000000a001",
                          "title": "Ventas mayoristas",
                          "stageGateEnabled": true,
                          "stageGateCumulative": true,
                          "dealsCount": 0,
                          "stages": [
                            {
                              "id": "66f10000000000000000b001",
                              "pipelineId": "66f10000000000000000a001",
                              "name": "Nuevo",
                              "order": 0,
                              "color": "#3b82f6",
                              "isWon": null,
                              "isLost": null,
                              "probability": 10,
                              "maxDaysInStage": 3,
                              "dealsCount": 0
                            }
                          ],
                          "createdAt": "2026-09-25T14:00:00.000Z",
                          "updatedAt": "2026-09-25T14:00:00.000Z"
                        }
                      ],
                      "pagination": {
                        "total": 1,
                        "page": 1,
                        "limit": 100,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "post": {
        "operationId": "createDealPipeline",
        "tags": [
          "Deal pipelines"
        ],
        "summary": "Crear un pipeline con sus etapas",
        "description": "Requiere pipelines:write o *. Las etapas se crean en el orden del arreglo (order 0..n-1). Máximo 50 etapas. Una etapa no puede ser ganada y perdida a la vez.",
        "x-required-permissions": [
          "pipelines:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DealPipelineCreate"
              },
              "examples": {
                "ventas": {
                  "value": {
                    "title": "Ventas mayoristas",
                    "stages": [
                      {
                        "name": "Nuevo",
                        "color": "#3b82f6",
                        "probability": 10,
                        "maxDaysInStage": 3
                      },
                      {
                        "name": "Cotizado",
                        "probability": 50
                      },
                      {
                        "name": "Ganado",
                        "isWon": true,
                        "probability": 100
                      },
                      {
                        "name": "Perdido",
                        "isLost": true,
                        "probability": 0
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Pipeline creado, con sus etapas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealPipelineResult"
                },
                "examples": {
                  "created": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "66f10000000000000000a001",
                        "title": "Ventas mayoristas",
                        "stageGateEnabled": true,
                        "stageGateCumulative": true,
                        "dealsCount": 7,
                        "stages": [
                          {
                            "id": "66f10000000000000000b001",
                            "pipelineId": "66f10000000000000000a001",
                            "name": "Nuevo",
                            "order": 0,
                            "color": "#3b82f6",
                            "isWon": null,
                            "isLost": null,
                            "probability": 10,
                            "maxDaysInStage": 3,
                            "dealsCount": 4
                          },
                          {
                            "id": "66f10000000000000000b002",
                            "pipelineId": "66f10000000000000000a001",
                            "name": "Cotizado",
                            "order": 1,
                            "color": "#000000",
                            "isWon": null,
                            "isLost": null,
                            "probability": 50,
                            "maxDaysInStage": null,
                            "dealsCount": 3
                          },
                          {
                            "id": "66f10000000000000000b003",
                            "pipelineId": "66f10000000000000000a001",
                            "name": "Ganado",
                            "order": 2,
                            "color": "#22c55e",
                            "isWon": true,
                            "isLost": false,
                            "probability": 100,
                            "maxDaysInStage": null,
                            "dealsCount": 0
                          }
                        ],
                        "createdAt": "2026-09-25T14:00:00.000Z",
                        "updatedAt": "2026-09-25T14:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Payload inválido (campo obligatorio vacío, tipo o rango incorrecto, nada que actualizar).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealPipelineError"
                },
                "examples": {
                  "title": {
                    "value": {
                      "success": false,
                      "error": "title es obligatorio"
                    }
                  },
                  "probability": {
                    "value": {
                      "success": false,
                      "error": "stages[0]: probability debe ser un número entre 0 y 100"
                    }
                  },
                  "order": {
                    "value": {
                      "success": false,
                      "error": "stageIds debe traer exactamente las 4 etapas del pipeline",
                      "cada una una vez": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/deal-pipelines/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB del pipeline de negocios (campo id de las respuestas).",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "66f10000000000000000a001"
        }
      ],
      "get": {
        "operationId": "getDealPipelineById",
        "tags": [
          "Deal pipelines"
        ],
        "summary": "Detalle de un pipeline con sus etapas",
        "description": "Requiere pipelines:read o *. Un ID que no es ObjectId o de otra organización responde 404.",
        "x-required-permissions": [
          "pipelines:read"
        ],
        "responses": {
          "200": {
            "description": "Pipeline encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealPipelineResult"
                },
                "examples": {
                  "found": {
                    "$ref": "#/paths/~1deal-pipelines/post/responses/201/content/application~1json/examples/created"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Pipeline inexistente, ID inválido o de otra organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealPipelineError"
                },
                "example": {
                  "success": false,
                  "error": "Pipeline not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "put": {
        "operationId": "updateDealPipelineById",
        "tags": [
          "Deal pipelines"
        ],
        "summary": "Renombrar el pipeline o cambiar los requisitos por etapa",
        "description": "Requiere pipelines:write o *. Solo title, stageGateEnabled y stageGateCumulative; sin ninguno responde 400. Las etapas se editan con sus propias rutas.",
        "x-required-permissions": [
          "pipelines:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DealPipelineUpdate"
              },
              "examples": {
                "rename": {
                  "value": {
                    "title": "Pedidos Medellín"
                  }
                },
                "gate": {
                  "value": {
                    "stageGateCumulative": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Pipeline actualizado, con sus etapas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealPipelineResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1deal-pipelines/post/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1deal-pipelines~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "delete": {
        "operationId": "deleteDealPipelineById",
        "tags": [
          "Deal pipelines"
        ],
        "summary": "Eliminar un pipeline sin negocios",
        "description": "Requiere pipelines:write o *. Borra el pipeline y TODAS sus etapas; es irreversible. Si el pipeline tiene negocios responde 409 con dealsCount y no borra nada (a diferencia de la app web, la API nunca deja negocios huérfanos).",
        "x-required-permissions": [
          "pipelines:write"
        ],
        "responses": {
          "200": {
            "description": "Pipeline eliminado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealPipelineDeleteResult"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "66f10000000000000000a001",
                    "deletedStages": 4
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1deal-pipelines~1{id}/get/responses/404"
          },
          "409": {
            "description": "El pipeline tiene negocios.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealPipelineConflictError"
                },
                "example": {
                  "success": false,
                  "error": "El pipeline tiene 12 negocio(s): muévelos a otro pipeline o bórralos antes de eliminarlo",
                  "dealsCount": 12
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/deal-pipelines/{id}/stages": {
      "parameters": [
        {
          "$ref": "#/paths/~1deal-pipelines~1{id}/parameters/0"
        }
      ],
      "post": {
        "operationId": "createDealStage",
        "tags": [
          "Deal pipelines"
        ],
        "summary": "Agregar una etapa al final del pipeline",
        "description": "Requiere pipelines:write o *. La etapa queda de última (order = mayor + 1); para moverla usa PUT /deal-pipelines/{id}/stages/order. Máximo 50 etapas por pipeline.",
        "x-required-permissions": [
          "pipelines:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DealStageCreate"
              },
              "example": {
                "name": "Despachado",
                "color": "#22c55e",
                "probability": 90,
                "maxDaysInStage": 5
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Etapa creada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealStageResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1deal-pipelines/post/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1deal-pipelines~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/deal-pipelines/{id}/stages/order": {
      "parameters": [
        {
          "$ref": "#/paths/~1deal-pipelines~1{id}/parameters/0"
        }
      ],
      "put": {
        "operationId": "reorderDealStages",
        "tags": [
          "Deal pipelines"
        ],
        "summary": "Reordenar las etapas del pipeline",
        "description": "Requiere pipelines:write o *. stageIds debe traer EXACTAMENTE todas las etapas del pipeline, cada una una vez, en el orden deseado (queda order 0..n-1). Faltantes, sobrantes, repetidas o etapas de otro pipeline responden 400.",
        "x-required-permissions": [
          "pipelines:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DealStageReorder"
              },
              "example": {
                "stageIds": [
                  "66f10000000000000000b002",
                  "66f10000000000000000b001",
                  "66f10000000000000000b003"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Etapas en el nuevo orden.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealStageListResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1deal-pipelines/post/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1deal-pipelines~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/deal-pipelines/{id}/stages/{stageId}": {
      "parameters": [
        {
          "$ref": "#/paths/~1deal-pipelines~1{id}/parameters/0"
        },
        {
          "name": "stageId",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB de la etapa (campo stages[].id).",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "66f10000000000000000b001"
        }
      ],
      "put": {
        "operationId": "updateDealStage",
        "tags": [
          "Deal pipelines"
        ],
        "summary": "Editar una etapa",
        "description": "Requiere pipelines:write o *. Parcial: name, color, isWon, isLost, probability y maxDaysInStage. null en isWon/isLost vuelve a \"automático por nombre\"; null en probability o maxDaysInStage deja la etapa sin configurar. La etapa debe ser de ese pipeline (si no, 404). El orden se cambia con PUT /stages/order.",
        "x-required-permissions": [
          "pipelines:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DealStageUpdate"
              },
              "examples": {
                "won": {
                  "value": {
                    "name": "Pagado",
                    "isWon": true,
                    "probability": 100
                  }
                },
                "rotting": {
                  "value": {
                    "maxDaysInStage": 7
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Etapa actualizada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealStageResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1deal-pipelines/post/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Etapa inexistente, de otro pipeline o de otra organización (o ID inválido).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealPipelineError"
                },
                "example": {
                  "success": false,
                  "error": "Stage not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "delete": {
        "operationId": "deleteDealStage",
        "tags": [
          "Deal pipelines"
        ],
        "summary": "Eliminar una etapa sin negocios",
        "description": "Requiere pipelines:write o *. Irreversible. Si la etapa tiene negocios responde 409 con dealsCount y no borra nada: muévelos antes con PUT /deals/{id}.",
        "x-required-permissions": [
          "pipelines:write"
        ],
        "responses": {
          "200": {
            "description": "Etapa eliminada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealStageDeleteResult"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "66f10000000000000000b003"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1deal-pipelines~1{id}~1stages~1{stageId}/put/responses/404"
          },
          "409": {
            "description": "La etapa tiene negocios.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealPipelineConflictError"
                },
                "example": {
                  "success": false,
                  "error": "La etapa tiene 3 negocio(s): muévelos a otra etapa antes de eliminarla",
                  "dealsCount": 3
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/conversation-pipelines": {
      "get": {
        "operationId": "listConversationPipelines",
        "tags": [
          "Conversation pipelines"
        ],
        "summary": "Listar pipelines de conversaciones con sus etapas",
        "description": "Requiere pipelines:read o *. El predeterminado primero. Cada pipeline trae sus etapas ordenadas por order, con la configuración de asignación y webhook. Las lecturas históricas de /conversations/pipelines y /conversations/pipelines/stages siguen disponibles con conversations:read.",
        "x-required-permissions": [
          "pipelines:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Pipelines y paginación.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data",
                    "pagination"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ConversationPipeline"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "post": {
        "operationId": "createConversationPipeline",
        "tags": [
          "Conversation pipelines"
        ],
        "summary": "Crear el pipeline de conversaciones",
        "description": "Requiere pipelines:write o *. La app trabaja con UN pipeline de conversaciones por organización: si ya existe uno, NO se crea otro y se responde 200 con el existente y alreadyExists true. Cada etapa se valida (campos desconocidos → 400, webhookUrl solo https, assignToTeam debe ser empleado de la organización); order es opcional (índice).",
        "x-required-permissions": [
          "pipelines:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "stages"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "isDefault": {
                    "type": "boolean"
                  },
                  "stages": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "$ref": "#/paths/~1conversation-pipelines~1{id}~1stages/post/requestBody/content/application~1json/schema"
                    }
                  }
                }
              },
              "example": {
                "name": "Pipeline Principal",
                "isDefault": true,
                "stages": [
                  {
                    "name": "Nuevas"
                  },
                  {
                    "name": "Pedidos Medellín",
                    "color": "#22c55e",
                    "webhookEnabled": true,
                    "webhookUrl": "https://n8n.example.com/webhook/pedidos"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "La organización ya tenía pipeline; se devuelve ese (alreadyExists true).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data",
                    "alreadyExists"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/ConversationPipeline"
                    },
                    "alreadyExists": {
                      "type": "boolean",
                      "description": "true = la organización ya tenía pipeline y se devolvió ese sin crear nada."
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Pipeline creado con sus etapas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1conversation-pipelines/post/responses/200/content/application~1json/schema"
                }
              }
            }
          },
          "400": {
            "description": "Validación del cuerpo (campos desconocidos, tipos, https, asesor ajeno, etapas incompletas al reordenar).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationPipelineError"
                },
                "example": {
                  "success": false,
                  "error": "webhookUrl debe usar https.",
                  "code": "VALIDATION"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/conversation-pipelines/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB del pipeline de conversaciones. Un id que no es ObjectId o de otra organización responde 404.",
          "schema": {
            "type": "string"
          },
          "example": "66f2000000000000000000a1"
        }
      ],
      "get": {
        "operationId": "getConversationPipelineById",
        "tags": [
          "Conversation pipelines"
        ],
        "summary": "Detalle de un pipeline de conversaciones (con etapas)",
        "description": "Requiere pipelines:read o *. Un ID que no es ObjectId responde 404, no 400.",
        "x-required-permissions": [
          "pipelines:read"
        ],
        "responses": {
          "200": {
            "description": "Pipeline encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/ConversationPipeline"
                    }
                  }
                },
                "examples": {
                  "found": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "66f2000000000000000000a1",
                        "name": "Pipeline Principal",
                        "isDefault": true,
                        "stages": [
                          {
                            "id": "66f2000000000000000000b1",
                            "pipelineId": "66f2000000000000000000a1",
                            "name": "Nuevas",
                            "order": 0,
                            "color": "#9ca3af",
                            "autoAssign": false,
                            "assignToTeam": null,
                            "webhookEnabled": false,
                            "webhookUrl": null,
                            "webhookLastOkAt": null,
                            "webhookLastErrorAt": null,
                            "webhookLastError": null,
                            "reopenOnNewMessage": false,
                            "createdAt": "2026-09-25T15:00:00.000Z",
                            "updatedAt": "2026-09-25T15:00:00.000Z"
                          },
                          {
                            "id": "66f2000000000000000000b2",
                            "pipelineId": "66f2000000000000000000a1",
                            "name": "Pedidos Medellín",
                            "order": 1,
                            "color": "#22c55e",
                            "autoAssign": true,
                            "assignToTeam": "66f2000000000000000000c1",
                            "webhookEnabled": true,
                            "webhookUrl": "https://n8n.example.com/webhook/pedidos",
                            "webhookLastOkAt": "2026-09-24T21:10:00.000Z",
                            "webhookLastErrorAt": null,
                            "webhookLastError": null,
                            "reopenOnNewMessage": false,
                            "createdAt": "2026-09-25T15:00:00.000Z",
                            "updatedAt": "2026-09-25T15:00:00.000Z"
                          }
                        ],
                        "createdAt": "2026-09-25T15:00:00.000Z",
                        "updatedAt": "2026-09-25T15:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Pipeline o etapa inexistente, de otra organización o con id que no es ObjectId.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationPipelineError"
                },
                "example": {
                  "success": false,
                  "error": "Pipeline not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "put": {
        "operationId": "updateConversationPipeline",
        "tags": [
          "Conversation pipelines"
        ],
        "summary": "Renombrar o marcar como predeterminado",
        "description": "Requiere pipelines:write o *. Solo name e isDefault; cualquier otro campo (incluido stages) → 400: las etapas se editan con /{id}/stages. isDefault solo acepta true y apaga el predeterminado anterior de la organización.",
        "x-required-permissions": [
          "pipelines:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "isDefault": {
                    "type": "boolean",
                    "const": true,
                    "description": "Solo true: para quitarlo, marca otro pipeline."
                  }
                }
              },
              "example": {
                "name": "Ventas WhatsApp",
                "isDefault": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Pipeline actualizado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1conversation-pipelines~1{id}/get/responses/200/content/application~1json/schema"
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1conversation-pipelines/post/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1conversation-pipelines~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "delete": {
        "operationId": "deleteConversationPipeline",
        "tags": [
          "Conversation pipelines"
        ],
        "summary": "Borrar un pipeline de conversaciones",
        "description": "Requiere pipelines:write o *. Irreversible: borra el pipeline y todas sus etapas. 409 LAST_PIPELINE si es el único de la organización y 409 PIPELINE_HAS_CONVERSATIONS si todavía tiene conversaciones. Si era el predeterminado, otro pasa a serlo (newDefaultPipelineId).",
        "x-required-permissions": [
          "pipelines:write"
        ],
        "responses": {
          "200": {
            "description": "Pipeline borrado.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "deletedPipelineId",
                        "newDefaultPipelineId"
                      ],
                      "properties": {
                        "deletedPipelineId": {
                          "type": "string"
                        },
                        "newDefaultPipelineId": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1conversation-pipelines~1{id}/get/responses/404"
          },
          "409": {
            "description": "El recurso existe pero su estado impide la operación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationPipelineError"
                },
                "examples": {
                  "target": {
                    "value": {
                      "success": false,
                      "error": "La etapa tiene 4 conversaciones: indica targetStageId para moverlas antes de borrarla.",
                      "code": "TARGET_STAGE_REQUIRED",
                      "conversations": 4
                    }
                  },
                  "lastPipeline": {
                    "value": {
                      "success": false,
                      "error": "No se puede eliminar el único pipeline existente",
                      "code": "LAST_PIPELINE"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/conversation-pipelines/{id}/stages": {
      "parameters": [
        {
          "$ref": "#/paths/~1conversation-pipelines~1{id}/parameters/0"
        }
      ],
      "post": {
        "operationId": "createConversationStage",
        "tags": [
          "Conversation pipelines"
        ],
        "summary": "Agregar una etapa",
        "description": "Requiere pipelines:write o *. Sin order va al final. webhookUrl solo https y webhookEnabled true exige webhookUrl; assignToTeam debe ser empleado de la organización; campos desconocidos → 400.",
        "x-required-permissions": [
          "pipelines:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/ConversationStageInput"
                  }
                ],
                "required": [
                  "name"
                ]
              },
              "example": {
                "name": "Seguimiento",
                "color": "#3b82f6",
                "reopenOnNewMessage": true
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Etapa creada.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/ConversationStage"
                    }
                  }
                },
                "examples": {
                  "created": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "66f2000000000000000000b3",
                        "pipelineId": "66f2000000000000000000a1",
                        "name": "Seguimiento",
                        "order": 2,
                        "color": "#3b82f6",
                        "autoAssign": false,
                        "assignToTeam": null,
                        "webhookEnabled": false,
                        "webhookUrl": null,
                        "webhookLastOkAt": null,
                        "webhookLastErrorAt": null,
                        "webhookLastError": null,
                        "reopenOnNewMessage": true,
                        "createdAt": "2026-09-25T15:05:00.000Z",
                        "updatedAt": "2026-09-25T15:05:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1conversation-pipelines/post/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1conversation-pipelines~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/conversation-pipelines/{id}/stages/order": {
      "parameters": [
        {
          "$ref": "#/paths/~1conversation-pipelines~1{id}/parameters/0"
        }
      ],
      "put": {
        "operationId": "reorderConversationStages",
        "tags": [
          "Conversation pipelines"
        ],
        "summary": "Reordenar las etapas",
        "description": "Requiere pipelines:write o *. stageIds debe traer exactamente las etapas del pipeline (todas, sin repetir); la posición en el arreglo pasa a ser su order. Si no coinciden → 400.",
        "x-required-permissions": [
          "pipelines:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "stageIds"
                ],
                "properties": {
                  "stageIds": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string"
                    },
                    "description": "TODAS las etapas del pipeline, sin repetir, en el orden nuevo."
                  }
                }
              },
              "example": {
                "stageIds": [
                  "66f2000000000000000000b2",
                  "66f2000000000000000000b1"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Etapas en el orden nuevo.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ConversationStage"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1conversation-pipelines/post/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1conversation-pipelines~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/conversation-pipelines/{id}/stages/{stageId}": {
      "parameters": [
        {
          "$ref": "#/paths/~1conversation-pipelines~1{id}/parameters/0"
        },
        {
          "name": "stageId",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB de la etapa (debe pertenecer al pipeline del path).",
          "schema": {
            "type": "string"
          },
          "example": "66f2000000000000000000b1"
        }
      ],
      "put": {
        "operationId": "updateConversationStage",
        "tags": [
          "Conversation pipelines"
        ],
        "summary": "Editar una etapa",
        "description": "Requiere pipelines:write o *. Solo cambia los campos enviados. Cambiar webhookUrl o apagar el webhook limpia webhookLastOkAt/webhookLastErrorAt/webhookLastError. null en webhookUrl o assignToTeam los quita.",
        "x-required-permissions": [
          "pipelines:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConversationStageInput"
              },
              "example": {
                "webhookEnabled": true,
                "webhookUrl": "https://n8n.example.com/webhook/pedidos-v2"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Etapa actualizada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1conversation-pipelines~1{id}~1stages/post/responses/201/content/application~1json/schema"
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1conversation-pipelines/post/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1conversation-pipelines~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "delete": {
        "operationId": "deleteConversationStage",
        "tags": [
          "Conversation pipelines"
        ],
        "summary": "Borrar una etapa",
        "description": "Requiere pipelines:write o *. Irreversible. Si la etapa tiene conversaciones, el body debe traer targetStageId (otra etapa del mismo pipeline) y se mueven ahí antes de borrar; sin él → 409 TARGET_STAGE_REQUIRED con el número de conversaciones. La última etapa del pipeline no se borra (409 LAST_STAGE). No escribe historial de cambio de etapa ni dispara webhooks.",
        "x-required-permissions": [
          "pipelines:write"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "targetStageId": {
                    "type": "string",
                    "description": "Otra etapa del mismo pipeline. Obligatoria si la etapa tiene conversaciones (se mueven ahí antes de borrar)."
                  }
                }
              },
              "example": {
                "targetStageId": "66f2000000000000000000b1"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Etapa borrada (y conversaciones movidas).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "deletedStageId",
                        "targetStageId",
                        "movedConversations"
                      ],
                      "properties": {
                        "deletedStageId": {
                          "type": "string"
                        },
                        "targetStageId": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "movedConversations": {
                          "type": "integer",
                          "minimum": 0
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1conversation-pipelines/post/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1conversation-pipelines~1{id}/get/responses/404"
          },
          "409": {
            "$ref": "#/paths/~1conversation-pipelines~1{id}/delete/responses/409"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/deal-fields": {
      "get": {
        "operationId": "listDealFields",
        "tags": [
          "Deal fields"
        ],
        "summary": "Listar campos personalizados de negocios",
        "description": "Requiere fields:read o *. Con pipelineId devuelve los campos de ese pipeline; sin él, los de todos los pipelines de la organización del token. Un pipelineId ajeno, inexistente o que no es ObjectId responde 404. No incluye los valores que cada negocio guardó.",
        "x-required-permissions": [
          "fields:read"
        ],
        "parameters": [
          {
            "name": "pipelineId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Campos y paginación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealFieldListResult"
                },
                "examples": {
                  "list": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "66f2000000000000000000a1",
                          "pipelineId": "66f2000000000000000000b1",
                          "name": "Cantidad de manillas",
                          "key": "cantidad",
                          "type": "number",
                          "options": [],
                          "required": true,
                          "requiredForStage": null,
                          "enforcement": "block",
                          "createdAt": "2026-09-25T15:00:00.000Z",
                          "updatedAt": "2026-09-25T15:00:00.000Z"
                        },
                        {
                          "id": "66f2000000000000000000a2",
                          "pipelineId": "66f2000000000000000000b1",
                          "name": "Tipo de manilla",
                          "key": "tipo",
                          "type": "select",
                          "options": [
                            "Tyvek",
                            "Tela",
                            "Silicona"
                          ],
                          "required": false,
                          "requiredForStage": "66f2000000000000000000c1",
                          "enforcement": "warn",
                          "createdAt": "2026-09-25T15:00:00.000Z",
                          "updatedAt": "2026-09-25T15:00:00.000Z"
                        }
                      ],
                      "pagination": {
                        "page": 1,
                        "limit": 100,
                        "total": 2,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Cuerpo o parámetros inválidos, clave repetida o etapa que no es del pipeline.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealFieldError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "success": false,
                      "error": "pipelineId is required"
                    }
                  },
                  "duplicate": {
                    "value": {
                      "success": false,
                      "error": "Ya existe un campo con esa clave",
                      "code": "DUPLICATE_KEY"
                    }
                  },
                  "stage": {
                    "value": {
                      "success": false,
                      "error": "La etapa del requisito no pertenece al pipeline",
                      "code": "INVALID_STAGE"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "El pipeline no existe o es de otra organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealFieldError"
                },
                "example": {
                  "success": false,
                  "error": "Pipeline not found",
                  "code": "PIPELINE_NOT_FOUND"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "post": {
        "operationId": "createDealField",
        "tags": [
          "Deal fields"
        ],
        "summary": "Crear un campo personalizado de negocios",
        "description": "Requiere fields:write o *. El pipeline debe ser de la organización (si no, 404). La key es única dentro del pipeline (400 DUPLICATE_KEY). requiredForStage debe ser una etapa del mismo pipeline (400 INVALID_STAGE). Un select exige al menos una opción.",
        "x-required-permissions": [
          "fields:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DealFieldCreate"
              },
              "examples": {
                "number": {
                  "value": {
                    "pipelineId": "66f2000000000000000000b1",
                    "name": "Cantidad de manillas",
                    "key": "cantidad",
                    "type": "number",
                    "required": true
                  }
                },
                "select": {
                  "value": {
                    "pipelineId": "66f2000000000000000000b1",
                    "name": "Tipo de manilla",
                    "key": "tipo",
                    "type": "select",
                    "options": [
                      "Tyvek",
                      "Tela",
                      "Silicona"
                    ],
                    "requiredForStage": "66f2000000000000000000c1",
                    "enforcement": "warn"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Campo creado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealFieldResult"
                },
                "examples": {
                  "created": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "66f2000000000000000000a1",
                        "pipelineId": "66f2000000000000000000b1",
                        "name": "Cantidad de manillas",
                        "key": "cantidad",
                        "type": "number",
                        "options": [],
                        "required": true,
                        "requiredForStage": "66f2000000000000000000c1",
                        "enforcement": "block",
                        "createdAt": "2026-09-25T15:00:00.000Z",
                        "updatedAt": "2026-09-25T15:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1deal-fields/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1deal-fields/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/deal-fields/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "Identificador MongoDB del campo.",
          "example": "66f2000000000000000000a1"
        }
      ],
      "get": {
        "operationId": "getDealField",
        "tags": [
          "Deal fields"
        ],
        "summary": "Detalle de un campo personalizado de negocios",
        "description": "Requiere fields:read o *. Un id de otra organización, inexistente o que no es ObjectId responde 404.",
        "x-required-permissions": [
          "fields:read"
        ],
        "responses": {
          "200": {
            "description": "Campo encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealFieldResult"
                },
                "examples": {
                  "found": {
                    "$ref": "#/paths/~1deal-fields/post/responses/201/content/application~1json/examples/created"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Campo inexistente, id inválido o de otra organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealFieldError"
                },
                "example": {
                  "success": false,
                  "error": "Deal field not found",
                  "code": "FIELD_NOT_FOUND"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "put": {
        "operationId": "updateDealField",
        "tags": [
          "Deal fields"
        ],
        "summary": "Editar un campo personalizado de negocios",
        "description": "Requiere fields:write o *. Parcial: solo cambia lo que se envía. Si cambia pipelineId y no se manda requiredForStage, la etapa actual debe existir en el nuevo pipeline (si no, 400 INVALID_STAGE; mande requiredForStage null para quitarla). Cambiar la key no migra valores: siguen ligados al campo por su id.",
        "x-required-permissions": [
          "fields:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DealFieldUpdate"
              },
              "examples": {
                "rename": {
                  "value": {
                    "name": "Unidades"
                  }
                },
                "clearStage": {
                  "value": {
                    "requiredForStage": null
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Campo actualizado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealFieldResult"
                },
                "examples": {
                  "updated": {
                    "$ref": "#/paths/~1deal-fields/post/responses/201/content/application~1json/examples/created"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1deal-fields/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1deal-fields~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "delete": {
        "operationId": "deleteDealField",
        "tags": [
          "Deal fields"
        ],
        "summary": "Borrar un campo personalizado de negocios y todos sus valores",
        "description": "Requiere fields:write o *. IRREVERSIBLE: borra el campo y los valores que ese campo tenía guardados en TODOS los negocios de la organización (deletedValues dice cuántos).",
        "x-required-permissions": [
          "fields:write"
        ],
        "responses": {
          "200": {
            "description": "Campo y valores borrados.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealFieldDeleteResult"
                },
                "examples": {
                  "deleted": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "66f2000000000000000000a1",
                        "deleted": true,
                        "deletedValues": 37
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1deal-fields~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/contact-properties": {
      "get": {
        "operationId": "listContactProperties",
        "tags": [
          "Contact properties"
        ],
        "summary": "Listar propiedades de contacto con sus grupos",
        "description": "Requiere fields:read o *. Devuelve las propiedades de contacto de la organización del token (núcleo con reserved=true y propias) y sus grupos. Antes de leer completa las propiedades núcleo que falten, igual que la app. No devuelve registros.",
        "x-required-permissions": [
          "fields:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Propiedades, grupos y paginación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PropertyListResult"
                },
                "examples": {
                  "list": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "66f3000000000000000000a1",
                          "key": "email",
                          "label": "Email",
                          "type": "email",
                          "isVisible": true,
                          "options": null,
                          "groupId": "66f3000000000000000000g1",
                          "reserved": true,
                          "required": false,
                          "optionsLocked": false,
                          "typeLocked": true,
                          "systemRequired": false
                        },
                        {
                          "id": "66f3000000000000000000a2",
                          "key": "companyType",
                          "label": "Tipo de empresa",
                          "type": "select",
                          "isVisible": true,
                          "options": [
                            "Cliente",
                            "Proveedor",
                            "Otro"
                          ],
                          "groupId": "66f3000000000000000000g2",
                          "reserved": true,
                          "required": false,
                          "optionsLocked": false,
                          "typeLocked": false,
                          "systemRequired": false
                        },
                        {
                          "id": "66f3000000000000000000a3",
                          "key": "talla",
                          "label": "Talla de camiseta",
                          "type": "select",
                          "isVisible": true,
                          "options": [
                            "S",
                            "M",
                            "L"
                          ],
                          "groupId": "66f3000000000000000000g1",
                          "reserved": false,
                          "required": false,
                          "optionsLocked": false,
                          "typeLocked": false,
                          "systemRequired": false
                        }
                      ],
                      "groups": [
                        {
                          "id": "66f3000000000000000000g1",
                          "title": "Contacto",
                          "icon": "User",
                          "order": 0
                        },
                        {
                          "id": "66f3000000000000000000g2",
                          "title": "Empresa",
                          "icon": "Building2",
                          "order": 1
                        }
                      ],
                      "pagination": {
                        "page": 1,
                        "limit": 100,
                        "total": 3,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Clave, tipo, opciones o grupo inválidos; o intento de cambiar key o el type de una núcleo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PropertyError"
                },
                "examples": {
                  "key": {
                    "value": {
                      "success": false,
                      "error": "key es obligatoria: empieza por letra y solo lleva letras, dígitos o _ (máx. 64)",
                      "code": "INVALID_KEY"
                    }
                  },
                  "type": {
                    "value": {
                      "success": false,
                      "error": "type inválido",
                      "code": "INVALID_TYPE",
                      "allowed": [
                        "input",
                        "select",
                        "textarea",
                        "number",
                        "date",
                        "email",
                        "phone"
                      ]
                    }
                  },
                  "reservedType": {
                    "value": {
                      "success": false,
                      "error": "El tipo de una propiedad núcleo no se puede cambiar",
                      "code": "RESERVED_TYPE"
                    }
                  },
                  "typeLocked": {
                    "value": {
                      "success": false,
                      "error": "El tipo de phone lo fija el registro del núcleo",
                      "code": "TYPE_LOCKED",
                      "field": "type"
                    }
                  },
                  "optionsLocked": {
                    "value": {
                      "success": false,
                      "error": "Las opciones de cityCode las fija el registro del núcleo",
                      "code": "OPTIONS_LOCKED",
                      "field": "options"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "post": {
        "operationId": "createContactProperty",
        "tags": [
          "Contact properties"
        ],
        "summary": "Crear una propiedad de contacto",
        "description": "Requiere fields:write o *. key única (409 DUPLICATE_KEY) y que no sea núcleo (409 RESERVED_KEY). Se agrega con un $push atómico al arreglo de la organización, sin reescribir el resto del documento. No existe DELETE: para retirar una propiedad se oculta con PUT isVisible=false.",
        "x-required-permissions": [
          "fields:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PropertyCreate"
              },
              "examples": {
                "select": {
                  "value": {
                    "key": "talla",
                    "label": "Talla de camiseta",
                    "type": "select",
                    "options": [
                      "S",
                      "M",
                      "L"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Propiedad creada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PropertyResult"
                },
                "examples": {
                  "created": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "66f3000000000000000000a3",
                        "key": "talla",
                        "label": "Talla de camiseta",
                        "type": "select",
                        "isVisible": true,
                        "options": [
                          "S",
                          "M",
                          "L"
                        ],
                        "groupId": "66f3000000000000000000g1",
                        "reserved": false,
                        "required": false,
                        "optionsLocked": false,
                        "typeLocked": false,
                        "systemRequired": false
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1contact-properties/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "409": {
            "description": "La key ya existe o es de una propiedad núcleo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PropertyError"
                },
                "examples": {
                  "duplicate": {
                    "value": {
                      "success": false,
                      "error": "Ya existe una propiedad con esa clave",
                      "code": "DUPLICATE_KEY"
                    }
                  },
                  "reserved": {
                    "value": {
                      "success": false,
                      "error": "La clave es de una propiedad núcleo del CRM",
                      "code": "RESERVED_KEY"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/contact-properties/{key}": {
      "parameters": [
        {
          "name": "key",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "Clave de la propiedad.",
          "example": "talla"
        }
      ],
      "put": {
        "operationId": "updateContactProperty",
        "tags": [
          "Contact properties"
        ],
        "summary": "Editar, ocultar o mostrar una propiedad de contacto",
        "description": "Requiere fields:write o *. Parcial: label, isVisible (false oculta, true muestra), options, groupId y type. La key nunca cambia (400 KEY_IMMUTABLE) y en propiedades núcleo tampoco el type (400 RESERVED_TYPE; 400 TYPE_LOCKED si la definición trae typeLocked). Con optionsLocked, cambiar options responde 400 OPTIONS_LOCKED (reenviar las mismas es idempotente). required (Épica Z) = obligatorio al crear/editar desde la web y la app; isVisible ya NO significa obligatorio. required false en una systemRequired (firstName) responde 400 SYSTEM_REQUIRED; required true en dv o en una de catálogo (cityCode/stateCode) responde 400 REQUIRED_NOT_ALLOWED. Se escribe con $set posicional sobre esa propiedad.",
        "x-required-permissions": [
          "fields:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PropertyUpdate"
              },
              "examples": {
                "hide": {
                  "value": {
                    "isVisible": false
                  }
                },
                "makeRequired": {
                  "value": {
                    "required": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Propiedad actualizada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PropertyResult"
                },
                "examples": {
                  "hidden": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "66f3000000000000000000a3",
                        "key": "talla",
                        "label": "Talla de camiseta",
                        "type": "select",
                        "isVisible": false,
                        "options": [
                          "S",
                          "M",
                          "L"
                        ],
                        "groupId": "66f3000000000000000000g1",
                        "reserved": false,
                        "required": false,
                        "optionsLocked": false,
                        "typeLocked": false,
                        "systemRequired": false
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1contact-properties/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "No existe una propiedad con esa key en la organización del token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PropertyError"
                },
                "example": {
                  "success": false,
                  "error": "Property not found",
                  "code": "PROPERTY_NOT_FOUND"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/company-properties": {
      "get": {
        "operationId": "listCompanyProperties",
        "tags": [
          "Company properties"
        ],
        "summary": "Listar propiedades de empresa con sus grupos",
        "description": "Requiere fields:read o *. Devuelve las propiedades de empresa de la organización del token (núcleo con reserved=true y propias) y sus grupos. Antes de leer completa las propiedades núcleo que falten, igual que la app. No devuelve registros.",
        "x-required-permissions": [
          "fields:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Propiedades, grupos y paginación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PropertyListResult"
                },
                "examples": {
                  "list": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "66f4000000000000000000a1",
                          "key": "name",
                          "label": "Nombre",
                          "type": "input",
                          "isVisible": true,
                          "options": null,
                          "groupId": "66f4000000000000000000g1",
                          "reserved": true,
                          "required": false,
                          "optionsLocked": false,
                          "typeLocked": false,
                          "systemRequired": false
                        },
                        {
                          "id": "66f4000000000000000000a2",
                          "key": "idNumber",
                          "label": "NIT / Número de documento",
                          "type": "input",
                          "isVisible": false,
                          "options": null,
                          "groupId": "66f4000000000000000000g1",
                          "reserved": true,
                          "required": false,
                          "optionsLocked": false,
                          "typeLocked": false,
                          "systemRequired": false
                        },
                        {
                          "id": "66f4000000000000000000a3",
                          "key": "sector",
                          "label": "Sector económico",
                          "type": "select",
                          "isVisible": true,
                          "options": [
                            "Eventos",
                            "Turismo",
                            "Salud"
                          ],
                          "groupId": "66f4000000000000000000g1",
                          "reserved": false,
                          "required": false,
                          "optionsLocked": false,
                          "typeLocked": false,
                          "systemRequired": false
                        }
                      ],
                      "groups": [
                        {
                          "id": "66f4000000000000000000g1",
                          "title": "Empresa",
                          "icon": "Building2",
                          "order": 0
                        },
                        {
                          "id": "66f4000000000000000000g2",
                          "title": "Dirección",
                          "icon": "MapPin",
                          "order": 1
                        }
                      ],
                      "pagination": {
                        "page": 1,
                        "limit": 100,
                        "total": 3,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Clave, tipo, opciones o grupo inválidos; o intento de cambiar key o el type de una núcleo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PropertyError"
                },
                "examples": {
                  "key": {
                    "value": {
                      "success": false,
                      "error": "key es obligatoria: empieza por letra y solo lleva letras, dígitos o _ (máx. 64)",
                      "code": "INVALID_KEY"
                    }
                  },
                  "type": {
                    "value": {
                      "success": false,
                      "error": "type inválido",
                      "code": "INVALID_TYPE",
                      "allowed": [
                        "input",
                        "select",
                        "textarea",
                        "number",
                        "date",
                        "email",
                        "phone"
                      ]
                    }
                  },
                  "reservedType": {
                    "value": {
                      "success": false,
                      "error": "El tipo de una propiedad núcleo no se puede cambiar",
                      "code": "RESERVED_TYPE"
                    }
                  },
                  "typeLocked": {
                    "value": {
                      "success": false,
                      "error": "El tipo de phone lo fija el registro del núcleo",
                      "code": "TYPE_LOCKED",
                      "field": "type"
                    }
                  },
                  "optionsLocked": {
                    "value": {
                      "success": false,
                      "error": "Las opciones de cityCode las fija el registro del núcleo",
                      "code": "OPTIONS_LOCKED",
                      "field": "options"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "post": {
        "operationId": "createCompanyProperty",
        "tags": [
          "Company properties"
        ],
        "summary": "Crear una propiedad de empresa",
        "description": "Requiere fields:write o *. key única (409 DUPLICATE_KEY) y que no sea núcleo (409 RESERVED_KEY). Se agrega con un $push atómico al arreglo de la organización, sin reescribir el resto del documento. No existe DELETE: para retirar una propiedad se oculta con PUT isVisible=false.",
        "x-required-permissions": [
          "fields:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PropertyCreate"
              },
              "examples": {
                "select": {
                  "value": {
                    "key": "sector",
                    "label": "Sector económico",
                    "type": "select",
                    "options": [
                      "Eventos",
                      "Turismo",
                      "Salud"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Propiedad creada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PropertyResult"
                },
                "examples": {
                  "created": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "66f4000000000000000000a3",
                        "key": "sector",
                        "label": "Sector económico",
                        "type": "select",
                        "isVisible": true,
                        "options": [
                          "Eventos",
                          "Turismo",
                          "Salud"
                        ],
                        "groupId": "66f4000000000000000000g1",
                        "reserved": false,
                        "required": false,
                        "optionsLocked": false,
                        "typeLocked": false,
                        "systemRequired": false
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1company-properties/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "409": {
            "description": "La key ya existe o es de una propiedad núcleo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PropertyError"
                },
                "examples": {
                  "duplicate": {
                    "value": {
                      "success": false,
                      "error": "Ya existe una propiedad con esa clave",
                      "code": "DUPLICATE_KEY"
                    }
                  },
                  "reserved": {
                    "value": {
                      "success": false,
                      "error": "La clave es de una propiedad núcleo del CRM",
                      "code": "RESERVED_KEY"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/company-properties/{key}": {
      "parameters": [
        {
          "name": "key",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "Clave de la propiedad.",
          "example": "sector"
        }
      ],
      "put": {
        "operationId": "updateCompanyProperty",
        "tags": [
          "Company properties"
        ],
        "summary": "Editar, ocultar o mostrar una propiedad de empresa",
        "description": "Requiere fields:write o *. Parcial: label, isVisible (false oculta, true muestra), options, groupId y type. La key nunca cambia (400 KEY_IMMUTABLE) y en propiedades núcleo tampoco el type (400 RESERVED_TYPE; 400 TYPE_LOCKED si la definición trae typeLocked). Con optionsLocked, cambiar options responde 400 OPTIONS_LOCKED (reenviar las mismas es idempotente). required (Épica Z) = obligatorio al crear/editar desde la web y la app; isVisible ya NO significa obligatorio. required false en una systemRequired (firstName) responde 400 SYSTEM_REQUIRED; required true en dv o en una de catálogo (cityCode/stateCode) responde 400 REQUIRED_NOT_ALLOWED. Se escribe con $set posicional sobre esa propiedad.",
        "x-required-permissions": [
          "fields:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PropertyUpdate"
              },
              "examples": {
                "hide": {
                  "value": {
                    "isVisible": false
                  }
                },
                "makeRequired": {
                  "value": {
                    "required": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Propiedad actualizada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PropertyResult"
                },
                "examples": {
                  "hidden": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "66f4000000000000000000a3",
                        "key": "sector",
                        "label": "Sector económico",
                        "type": "select",
                        "isVisible": false,
                        "options": [
                          "Eventos",
                          "Turismo",
                          "Salud"
                        ],
                        "groupId": "66f4000000000000000000g1",
                        "reserved": false,
                        "required": false,
                        "optionsLocked": false,
                        "typeLocked": false,
                        "systemRequired": false
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1company-properties/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "No existe una propiedad con esa key en la organización del token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PropertyError"
                },
                "example": {
                  "success": false,
                  "error": "Property not found",
                  "code": "PROPERTY_NOT_FOUND"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/email-templates": {
      "get": {
        "operationId": "listEmailTemplates",
        "tags": [
          "Email templates"
        ],
        "summary": "Listar plantillas de correo",
        "description": "Requiere templates:read o *. Más recientes primero. El listado no incluye emailJson ni emailHtml (pídelos en el detalle).",
        "x-required-permissions": [
          "templates:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          },
          {
            "name": "saved",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            },
            "description": "true = solo guardadas; false = solo borradores; sin el parámetro, todas."
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 100
            },
            "description": "Texto literal a buscar en el nombre (sin distinguir mayúsculas)."
          }
        ],
        "responses": {
          "200": {
            "description": "Plantillas y paginación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmailTemplateListResult"
                },
                "examples": {
                  "list": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "66f2000000000000000000e1",
                          "name": "Bienvenida clientes nuevos",
                          "saved": true,
                          "blocksCount": 4,
                          "userId": "66f2000000000000000000a1",
                          "createdAt": "2026-09-20T15:00:00.000Z",
                          "updatedAt": "2026-09-21T10:30:00.000Z"
                        }
                      ],
                      "pagination": {
                        "total": 1,
                        "page": 1,
                        "limit": 100,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "post": {
        "operationId": "createEmailTemplate",
        "tags": [
          "Email templates"
        ],
        "summary": "Crear una plantilla de correo",
        "description": "Requiere templates:write o *. El autor es el usuario del token. emailJson gana sobre emailHtml (ver EmailTemplateInput). No envía correos.",
        "x-required-permissions": [
          "templates:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailTemplateCreateRequest"
              },
              "examples": {
                "blocks": {
                  "value": {
                    "name": "Bienvenida clientes nuevos",
                    "saved": true,
                    "previewText": "Gracias por tu compra",
                    "emailJson": [
                      {
                        "id": "b1",
                        "type": "header",
                        "text": "¡Bienvenido!"
                      },
                      {
                        "id": "b2",
                        "type": "text",
                        "content": "Gracias por comprarnos."
                      }
                    ]
                  }
                },
                "html": {
                  "value": {
                    "name": "Promo septiembre",
                    "emailHtml": "<p>Hola, <strong>20% de descuento</strong> esta semana.</p>"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Plantilla creada (con emailJson y emailHtml ya resueltos).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmailTemplateResult"
                },
                "examples": {
                  "created": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "66f2000000000000000000e1",
                        "name": "Bienvenida clientes nuevos",
                        "saved": true,
                        "blocksCount": 2,
                        "userId": "66f2000000000000000000a1",
                        "emailJson": [
                          {
                            "id": "b1",
                            "type": "header",
                            "text": "¡Bienvenido!"
                          },
                          {
                            "id": "b2",
                            "type": "text",
                            "content": "Gracias por comprarnos."
                          }
                        ],
                        "emailHtml": "<!doctype html><html lang=\"es\"><body>…</body></html>",
                        "createdAt": "2026-09-20T15:00:00.000Z",
                        "updatedAt": "2026-09-21T10:30:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Cuerpo inválido (falta name, emailJson no es un arreglo de bloques, tamaño excedido, HTML vacío tras sanitizar, nada que actualizar).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmailTemplateError"
                },
                "example": {
                  "success": false,
                  "error": "emailJson or emailHtml is required"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/email-templates/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Id de la plantilla de correo (ObjectId).",
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "operationId": "getEmailTemplateById",
        "tags": [
          "Email templates"
        ],
        "summary": "Detalle de una plantilla de correo",
        "description": "Requiere templates:read o *. Incluye emailJson y emailHtml. Un ID que no es ObjectId responde 404.",
        "x-required-permissions": [
          "templates:read"
        ],
        "responses": {
          "200": {
            "description": "Plantilla encontrada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmailTemplateResult"
                },
                "examples": {
                  "found": {
                    "$ref": "#/paths/~1email-templates/post/responses/201/content/application~1json/examples/created"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Plantilla inexistente, ID inválido o de otra organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmailTemplateError"
                },
                "example": {
                  "success": false,
                  "error": "Email template not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "put": {
        "operationId": "updateEmailTemplate",
        "tags": [
          "Email templates"
        ],
        "summary": "Editar una plantilla de correo",
        "description": "Requiere templates:write o *. Parcial: solo cambia lo que llega. emailJson REEMPLAZA todos los bloques y regenera el HTML (emailHtml se ignora); emailHtml solo, se sanitiza y deja emailJson en []. Un cuerpo sin ningún campo editable responde 400.",
        "x-required-permissions": [
          "templates:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailTemplateInput"
              },
              "example": {
                "name": "Bienvenida (v2)",
                "saved": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Plantilla actualizada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmailTemplateResult"
                },
                "examples": {
                  "updated": {
                    "$ref": "#/paths/~1email-templates/post/responses/201/content/application~1json/examples/created"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1email-templates/post/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1email-templates~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "delete": {
        "operationId": "deleteEmailTemplate",
        "tags": [
          "Email templates"
        ],
        "summary": "Borrar una plantilla de correo",
        "description": "Requiere templates:write o *. Borrado físico e irreversible (no hay papelera).",
        "x-required-permissions": [
          "templates:write"
        ],
        "responses": {
          "200": {
            "description": "Plantilla borrada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmailTemplateDeleteResult"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "66f2000000000000000000e1",
                    "deleted": true
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1email-templates~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/document-templates": {
      "get": {
        "operationId": "listDocumentTemplates",
        "tags": [
          "Document templates"
        ],
        "summary": "Listar plantillas de documentos PDF",
        "description": "Requiere templates:read o *. Más recientes primero; por defecto solo activas. Crear o editar plantillas (subida del PDF) no está en la API: se hace desde la app.",
        "x-required-permissions": [
          "templates:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          },
          {
            "name": "includeInactive",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            },
            "description": "true = incluye las desactivadas."
          }
        ],
        "responses": {
          "200": {
            "description": "Plantillas y paginación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentTemplateListResult"
                },
                "examples": {
                  "list": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "66f3000000000000000000f1",
                          "name": "Contrato de servicio",
                          "category": "Contratos",
                          "fileName": "contrato.pdf",
                          "isActive": true,
                          "version": 2,
                          "etapasCount": 2,
                          "fieldsCount": 5,
                          "createdAt": "2026-09-01T12:00:00.000Z",
                          "updatedAt": "2026-09-10T08:00:00.000Z"
                        }
                      ],
                      "pagination": {
                        "total": 1,
                        "page": 1,
                        "limit": 100,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/document-templates/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Id de la plantilla de documento (ObjectId).",
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "operationId": "getDocumentTemplateById",
        "tags": [
          "Document templates"
        ],
        "summary": "Detalle de una plantilla PDF (etapas y campos)",
        "description": "Requiere templates:read o *. Incluye las etapas con sus campos y el pdfFieldName que espera generate. Con includePdfFields=true descarga el PDF y lista sus campos AcroForm reales. Un ID que no es ObjectId responde 404.",
        "x-required-permissions": [
          "templates:read"
        ],
        "parameters": [
          {
            "name": "includePdfFields",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Plantilla encontrada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentTemplateResult"
                },
                "examples": {
                  "found": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "66f3000000000000000000f1",
                        "name": "Contrato de servicio",
                        "category": "Contratos",
                        "fileName": "contrato.pdf",
                        "isActive": true,
                        "version": 2,
                        "etapasCount": 1,
                        "fieldsCount": 2,
                        "etapas": [
                          {
                            "label": "cliente",
                            "fields": [
                              {
                                "label": "Nombre",
                                "key": "firstName",
                                "pdfFieldName": "cliente.firstName",
                                "type": "Text",
                                "required": true,
                                "options": "",
                                "valueTemplate": "{firstName} {lastName}"
                              },
                              {
                                "label": "Dirección",
                                "key": "address",
                                "pdfFieldName": "cliente.address",
                                "type": "Text",
                                "required": false,
                                "options": "",
                                "valueTemplate": ""
                              }
                            ]
                          }
                        ],
                        "createdAt": "2026-09-01T12:00:00.000Z",
                        "updatedAt": "2026-09-10T08:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Plantilla inexistente, ID inválido o de otra organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentTemplateError"
                },
                "example": {
                  "success": false,
                  "error": "Document template not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/document-templates/{id}/generate": {
      "parameters": [
        {
          "$ref": "#/paths/~1document-templates~1{id}/parameters/0"
        }
      ],
      "post": {
        "operationId": "generateDocumentTemplatePdf",
        "tags": [
          "Document templates"
        ],
        "summary": "Generar el PDF rellenado de una plantilla",
        "description": "Requiere templates:read o * (no escribe nada: el PDF se genera y se devuelve, no se guarda ni se envía). Rellena los campos AcroForm con values y responde el PDF binario. Los errores usan success/error.",
        "x-required-permissions": [
          "templates:read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DocumentTemplateGenerateRequest"
              },
              "example": {
                "values": {
                  "cliente.firstName": "Ana Gómez",
                  "cliente.address?": "Cra 1 #2-3"
                },
                "flatten": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "PDF rellenado.",
            "headers": {
              "Content-Disposition": {
                "description": "attachment; filename=\"<fileName de la plantilla>\"",
                "schema": {
                  "type": "string"
                },
                "example": "attachment; filename=\"contrato.pdf\""
              },
              "Content-Length": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "values inválido, o campos requeridos que no existen en el PDF (missingFields).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentTemplateError"
                },
                "example": {
                  "success": false,
                  "error": "Faltan campos requeridos en la plantilla PDF",
                  "missingFields": [
                    "cliente.firstName"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1document-templates~1{id}/get/responses/404"
          },
          "422": {
            "description": "El PDF base no es válido o no se pudo procesar.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentTemplateError"
                },
                "example": {
                  "success": false,
                  "error": "El archivo no es un PDF válido"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          },
          "502": {
            "description": "No se pudo descargar el PDF base del almacenamiento.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentTemplateError"
                },
                "example": {
                  "success": false,
                  "error": "No se pudo obtener la plantilla del almacenamiento"
                }
              }
            }
          }
        }
      }
    },
    "/organization": {
      "get": {
        "operationId": "getOrganizationSettings",
        "tags": [
          "Organization"
        ],
        "summary": "Datos de la empresa y ajustes generales",
        "description": "Requiere settings:read o *. Devuelve la organización del token por lista blanca: datos de empresa (nombre, NIT, contacto, dirección, logo) y settings (zona horaria, locale, moneda, ventana de envío, 2FA exigido, textos por defecto de cotizaciones y compras, campo del KPI de productos). Nunca empleados, plan, consecutivos ni credenciales.",
        "x-required-permissions": [
          "settings:read"
        ],
        "responses": {
          "200": {
            "description": "Organización del token.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "id",
                        "companyName",
                        "idType",
                        "idNumber",
                        "email",
                        "phone",
                        "whatsapp",
                        "address",
                        "logoUrl",
                        "iconUrl",
                        "settings",
                        "createdAt",
                        "updatedAt"
                      ],
                      "description": "Datos de la empresa y ajustes generales. Lista blanca: nunca incluye empleados, dueño, plan, estado de cuenta, consecutivos ni credenciales (WhatsApp, correo masivo, DIAN).",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "companyName": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "idType": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "Tipo de identificación (Nit, CC…)."
                        },
                        "idNumber": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "email": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "phone": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "whatsapp": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "WhatsApp de contacto que se muestra; no es el número conectado a la API."
                        },
                        "address": {
                          "type": "object",
                          "required": [
                            "address",
                            "city",
                            "cityCode",
                            "state",
                            "zipCode",
                            "country"
                          ],
                          "properties": {
                            "address": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "city": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "cityCode": {
                              "type": [
                                "string",
                                "null"
                              ],
                              "description": "Código DANE del municipio (5 dígitos). Fuente de verdad de city/state en Colombia; vacío = ciudad escrita a mano sin confirmar en el catálogo."
                            },
                            "state": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "zipCode": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "country": {
                              "type": [
                                "string",
                                "null"
                              ]
                            }
                          }
                        },
                        "logoUrl": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "Solo lectura por la API (se sube desde la app)."
                        },
                        "iconUrl": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "Solo lectura por la API."
                        },
                        "settings": {
                          "type": "object",
                          "required": [
                            "timezone",
                            "locale",
                            "currency",
                            "automationSendWindow",
                            "security",
                            "quotations",
                            "purchases",
                            "reportsSettings"
                          ],
                          "properties": {
                            "timezone": {
                              "type": "string",
                              "description": "Zona IANA (default America/Bogota)."
                            },
                            "locale": {
                              "type": "string",
                              "description": "BCP-47 (default es-CO). Formato de dinero y fechas."
                            },
                            "currency": {
                              "type": "string",
                              "description": "ISO-4217 (default COP)."
                            },
                            "automationSendWindow": {
                              "oneOf": [
                                {
                                  "$ref": "#/paths/~1organization/put/requestBody/content/application~1json/schema/properties/settings/properties/automationSendWindow/oneOf/0"
                                },
                                {
                                  "type": "null"
                                }
                              ]
                            },
                            "security": {
                              "type": "object",
                              "properties": {
                                "requireTwoFactor": {
                                  "type": "boolean",
                                  "description": "Solo lectura por la API."
                                }
                              }
                            },
                            "quotations": {
                              "type": "object",
                              "properties": {
                                "paymentTerms": {
                                  "type": "array",
                                  "items": {
                                    "type": "string"
                                  }
                                },
                                "shippingTerms": {
                                  "type": "array",
                                  "items": {
                                    "type": "string"
                                  }
                                },
                                "notes": {
                                  "type": [
                                    "string",
                                    "null"
                                  ]
                                },
                                "footerText": {
                                  "type": [
                                    "string",
                                    "null"
                                  ]
                                },
                                "validityDays": {
                                  "type": [
                                    "integer",
                                    "null"
                                  ],
                                  "minimum": 1,
                                  "maximum": 365
                                },
                                "numberPrefix": {
                                  "type": [
                                    "string",
                                    "null"
                                  ],
                                  "description": "Prefijo visual del número (COT-). El consecutivo no se expone ni se edita."
                                },
                                "taxLabel": {
                                  "type": [
                                    "string",
                                    "null"
                                  ],
                                  "description": "Rótulo del impuesto en el PDF (IVA por defecto)."
                                },
                                "bgImage": {
                                  "type": [
                                    "string",
                                    "null"
                                  ],
                                  "description": "Solo lectura por la API."
                                }
                              }
                            },
                            "purchases": {
                              "type": "object",
                              "properties": {
                                "paymentTerms": {
                                  "type": "array",
                                  "items": {
                                    "type": "string"
                                  }
                                },
                                "shippingTerms": {
                                  "type": "array",
                                  "items": {
                                    "type": "string"
                                  }
                                },
                                "notes": {
                                  "type": [
                                    "string",
                                    "null"
                                  ]
                                }
                              }
                            },
                            "reportsSettings": {
                              "type": "object",
                              "properties": {
                                "productQuantityFieldKey": {
                                  "type": [
                                    "string",
                                    "null"
                                  ],
                                  "description": "Key del campo de negocios que suma el KPI Productos vendidos."
                                }
                              }
                            }
                          }
                        },
                        "createdAt": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        },
                        "updatedAt": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "found": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "66f10000000000000000c001",
                        "companyName": "Manillas de Control",
                        "idType": "Nit",
                        "idNumber": "900123456-7",
                        "email": "ventas@empresa.com",
                        "phone": "+57 604 000 0000",
                        "whatsapp": "+57 300 000 0000",
                        "address": {
                          "address": "Cra 43A # 1-50",
                          "city": "Medellín",
                          "cityCode": "05001",
                          "state": "Antioquia",
                          "zipCode": "050021",
                          "country": "Colombia"
                        },
                        "logoUrl": "https://res.cloudinary.com/demo/image/upload/logo.png",
                        "iconUrl": null,
                        "settings": {
                          "timezone": "America/Bogota",
                          "locale": "es-CO",
                          "currency": "COP",
                          "automationSendWindow": {
                            "enabled": true,
                            "days": [
                              1,
                              2,
                              3,
                              4,
                              5
                            ],
                            "startHour": "08:00",
                            "endHour": "19:00",
                            "timezone": null,
                            "channels": [
                              "whatsapp"
                            ]
                          },
                          "security": {
                            "requireTwoFactor": false
                          },
                          "quotations": {
                            "paymentTerms": [
                              "50% anticipo"
                            ],
                            "shippingTerms": [
                              "Envío nacional"
                            ],
                            "notes": null,
                            "footerText": null,
                            "validityDays": 15,
                            "numberPrefix": "COT-",
                            "taxLabel": "IVA",
                            "bgImage": null
                          },
                          "purchases": {
                            "paymentTerms": [],
                            "shippingTerms": [],
                            "notes": null
                          },
                          "reportsSettings": {
                            "productQuantityFieldKey": null
                          }
                        },
                        "createdAt": "2025-01-10T14:00:00.000Z",
                        "updatedAt": "2026-09-25T14:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "La organización del token ya no existe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1organization/put/responses/400/content/application~1json/schema"
                },
                "example": {
                  "success": false,
                  "error": "Organization not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "put": {
        "operationId": "updateOrganizationSettings",
        "tags": [
          "Organization"
        ],
        "summary": "Editar datos de la empresa y ajustes generales",
        "description": "Requiere settings:write o * Y que el usuario del token sea el PROPIETARIO (si no, 403 OWNER_REQUIRED). Parcial: se aplica con $set por campo, así que lo no enviado queda igual; null borra un campo opcional. automationSendWindow se reemplaza completa. Mismas validaciones que la app (locale BCP-47, moneda ISO-4217, ventana HH:mm con al menos un día). Un campo fuera de la lista blanca responde 400 en vez de ignorarse.",
        "x-required-permissions": [
          "settings:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "description": "Parcial: solo cambia lo enviado; null borra un campo opcional. Cualquier otra clave (employees, owner, plan, settings.quotations.quotationNumber, logoUrl, settings.security, credenciales…) responde 400.",
                "properties": {
                  "companyName": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "idType": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200
                  },
                  "idNumber": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200
                  },
                  "email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200
                  },
                  "phone": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200
                  },
                  "whatsapp": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200
                  },
                  "address": {
                    "type": "object",
                    "additionalProperties": false,
                    "properties": {
                      "address": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "maxLength": 200
                      },
                      "city": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "maxLength": 200,
                        "description": "Ciudad en texto. En Colombia, si no es el nombre del municipio guardado se resuelve contra el catálogo DANE (coincidencia exacta o alias asigna cityCode; si no, cityCode queda vacío)."
                      },
                      "cityCode": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "maxLength": 5,
                        "description": "Código DANE del municipio (5 dígitos, GET /catalogs/municipalities). Si cambia, manda: city y state se derivan del catálogo. Inexistente → 400. Fuera de Colombia se ignora."
                      },
                      "state": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "maxLength": 200
                      },
                      "zipCode": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "maxLength": 200
                      },
                      "country": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "maxLength": 200
                      }
                    }
                  },
                  "settings": {
                    "type": "object",
                    "additionalProperties": false,
                    "properties": {
                      "timezone": {
                        "type": "string"
                      },
                      "locale": {
                        "type": "string"
                      },
                      "currency": {
                        "type": "string",
                        "minLength": 3,
                        "maxLength": 3
                      },
                      "automationSendWindow": {
                        "description": "Se reemplaza completa; null la quita.",
                        "oneOf": [
                          {
                            "type": "object",
                            "description": "Ventana de envío de las automatizaciones: días y franja en que SÍ se escribe al cliente, en la zona de la organización (o la propia timezone). Con enabled false no retiene nada.",
                            "required": [
                              "enabled"
                            ],
                            "properties": {
                              "enabled": {
                                "type": "boolean"
                              },
                              "days": {
                                "type": "array",
                                "description": "0 = domingo … 6 = sábado. Obligatorio (al menos uno) con enabled true.",
                                "items": {
                                  "type": "integer",
                                  "minimum": 0,
                                  "maximum": 6
                                }
                              },
                              "startHour": {
                                "type": [
                                  "string",
                                  "null"
                                ],
                                "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
                                "description": "HH:mm desde cuándo se envía."
                              },
                              "endHour": {
                                "type": [
                                  "string",
                                  "null"
                                ],
                                "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
                                "description": "HH:mm hasta cuándo (igual a startHour = día completo)."
                              },
                              "timezone": {
                                "type": [
                                  "string",
                                  "null"
                                ],
                                "description": "Zona IANA; null = la de la organización."
                              },
                              "channels": {
                                "type": "array",
                                "items": {
                                  "type": "string",
                                  "enum": [
                                    "whatsapp",
                                    "email"
                                  ]
                                }
                              }
                            }
                          },
                          {
                            "type": "null"
                          }
                        ]
                      },
                      "quotations": {
                        "type": "object",
                        "additionalProperties": false,
                        "properties": {
                          "paymentTerms": {
                            "type": "array",
                            "maxItems": 50,
                            "items": {
                              "type": "string"
                            }
                          },
                          "shippingTerms": {
                            "type": "array",
                            "maxItems": 50,
                            "items": {
                              "type": "string"
                            }
                          },
                          "notes": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "maxLength": 5000
                          },
                          "footerText": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "maxLength": 5000
                          },
                          "validityDays": {
                            "type": [
                              "integer",
                              "null"
                            ],
                            "minimum": 1,
                            "maximum": 365
                          },
                          "numberPrefix": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "maxLength": 200
                          },
                          "taxLabel": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "maxLength": 200
                          }
                        }
                      },
                      "purchases": {
                        "type": "object",
                        "additionalProperties": false,
                        "properties": {
                          "paymentTerms": {
                            "type": "array",
                            "maxItems": 50,
                            "items": {
                              "type": "string"
                            }
                          },
                          "shippingTerms": {
                            "type": "array",
                            "maxItems": 50,
                            "items": {
                              "type": "string"
                            }
                          },
                          "notes": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "maxLength": 5000
                          }
                        }
                      },
                      "reportsSettings": {
                        "type": "object",
                        "additionalProperties": false,
                        "properties": {
                          "productQuantityFieldKey": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "maxLength": 100
                          }
                        }
                      }
                    }
                  }
                }
              },
              "examples": {
                "empresa": {
                  "value": {
                    "companyName": "Manillas de Control",
                    "address": {
                      "city": "Medellín"
                    }
                  }
                },
                "moneda": {
                  "value": {
                    "settings": {
                      "locale": "en-US",
                      "currency": "USD"
                    }
                  }
                },
                "ventana": {
                  "value": {
                    "settings": {
                      "automationSendWindow": {
                        "enabled": true,
                        "days": [
                          1,
                          2,
                          3,
                          4,
                          5
                        ],
                        "startHour": "08:00",
                        "endHour": "19:00"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Organización actualizada (misma forma que el GET).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1organization/get/responses/200/content/application~1json/schema"
                },
                "examples": {
                  "updated": {
                    "$ref": "#/paths/~1organization/get/responses/200/content/application~1json/examples/found"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Payload inválido, campo no permitido o de solo lectura, o nada que actualizar.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Contrato de error de /organization (success:false + error; code en 403 OWNER_REQUIRED).",
                  "required": [
                    "success",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": false
                    },
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                },
                "examples": {
                  "locale": {
                    "value": {
                      "success": false,
                      "error": "settings.locale debe ser un código de idioma/región válido (BCP-47, ej. es-CO)."
                    }
                  },
                  "protected": {
                    "value": {
                      "success": false,
                      "error": "Campos no permitidos: employees. Solo se editan datos de empresa (...) y settings (...)."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "description": "Falta el permiso del token, o el usuario del token no es el propietario.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1organization/put/responses/400/content/application~1json/schema"
                },
                "example": {
                  "success": false,
                  "error": "Only the organization owner can change its settings",
                  "code": "OWNER_REQUIRED"
                }
              }
            }
          },
          "404": {
            "$ref": "#/paths/~1organization/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/users": {
      "get": {
        "operationId": "listUsers",
        "tags": [
          "Users"
        ],
        "summary": "Listar usuarios de la organización",
        "description": "Requiere users:read o *. Solo lectura: crear, editar o borrar usuarios cambia asientos cobrados y se hace desde la app. Orden de creación.",
        "x-required-permissions": [
          "users:read"
        ],
        "parameters": [
          {
            "name": "role",
            "in": "query",
            "description": "Filtra por rol.",
            "schema": {
              "type": "string",
              "enum": [
                "propietario",
                "empleado",
                "lector"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Usuarios y paginación.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data",
                    "pagination"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/paths/~1users~1{id}/get/responses/200/content/application~1json/schema/properties/data"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                },
                "examples": {
                  "list": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "66f10000000000000000d000",
                          "firstName": "Ana",
                          "lastName": "Pérez",
                          "email": "ana@empresa.com",
                          "mobile": null,
                          "role": "propietario",
                          "readOnly": false,
                          "avatar": null,
                          "lastSeenAt": "2026-09-25T14:00:00.000Z",
                          "createdAt": "2025-01-10T14:00:00.000Z",
                          "updatedAt": "2026-09-01T10:00:00.000Z"
                        },
                        {
                          "id": "66f10000000000000000d001",
                          "firstName": "Laura",
                          "lastName": "Gómez",
                          "email": "laura@empresa.com",
                          "mobile": "+573001112233",
                          "role": "empleado",
                          "readOnly": false,
                          "avatar": null,
                          "lastSeenAt": null,
                          "createdAt": "2026-03-01T10:00:00.000Z",
                          "updatedAt": "2026-09-20T10:00:00.000Z"
                        }
                      ],
                      "pagination": {
                        "page": 1,
                        "limit": 100,
                        "total": 2,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "role inválido.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": false
                    },
                    "error": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "success": false,
                  "error": "role debe ser uno de: propietario, empleado, lector"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/users/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB del usuario (campo id de las respuestas).",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "66f10000000000000000d001"
        }
      ],
      "get": {
        "operationId": "getUserById",
        "tags": [
          "Users"
        ],
        "summary": "Detalle de un usuario",
        "description": "Requiere users:read o *. Un ID que no es ObjectId o de otra organización responde 404.",
        "x-required-permissions": [
          "users:read"
        ],
        "responses": {
          "200": {
            "description": "Usuario encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "description": "Miembro del equipo. Lista blanca: nunca incluye contraseña, pushToken, credenciales de correo (SMTP/IMAP), datos de 2FA ni preferencias.",
                      "required": [
                        "id",
                        "firstName",
                        "lastName",
                        "email",
                        "mobile",
                        "role",
                        "readOnly",
                        "avatar",
                        "lastSeenAt",
                        "createdAt",
                        "updatedAt"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "firstName": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "lastName": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "email": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "mobile": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "role": {
                          "type": "string",
                          "enum": [
                            "propietario",
                            "empleado",
                            "lector"
                          ],
                          "description": "lector = asiento de solo lectura (no puede escribir)."
                        },
                        "readOnly": {
                          "type": "boolean",
                          "description": "true si role es lector."
                        },
                        "avatar": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "lastSeenAt": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time",
                          "description": "Última actividad en la app (sesión más reciente). null = sin sesión reciente."
                        },
                        "createdAt": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        },
                        "updatedAt": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "found": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "66f10000000000000000d001",
                        "firstName": "Laura",
                        "lastName": "Gómez",
                        "email": "laura@empresa.com",
                        "mobile": "+573001112233",
                        "role": "empleado",
                        "readOnly": false,
                        "avatar": null,
                        "lastSeenAt": "2026-09-25T13:40:00.000Z",
                        "createdAt": "2026-03-01T10:00:00.000Z",
                        "updatedAt": "2026-09-20T10:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Usuario inexistente, ID inválido o de otra organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1users/get/responses/400/content/application~1json/schema"
                },
                "example": {
                  "success": false,
                  "error": "User not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/scoring-rules": {
      "get": {
        "operationId": "listScoringRules",
        "tags": [
          "Scoring rules"
        ],
        "summary": "Listar reglas de puntuación de leads",
        "description": "Requiere settings:read o *. El puntaje de un contacto (leadScore) es la suma de los points de todas las condiciones que cumple en las reglas activas. Orden de creación.",
        "x-required-permissions": [
          "settings:read"
        ],
        "parameters": [
          {
            "name": "isActive",
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Reglas y paginación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScoringRuleListResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "post": {
        "operationId": "createScoringRule",
        "tags": [
          "Scoring rules"
        ],
        "summary": "Crear una regla de puntuación",
        "description": "Requiere settings:write o *. Valida cada condición: condition soportada, propertyName (en las de contacto) contra las propiedades de contacto de la organización, value con la forma de la condición y los ids (etapa, pipeline, producto, campo de negocio) de la organización. No recalcula puntajes: llame POST /scoring-rules/recalculate.",
        "x-required-permissions": [
          "settings:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScoringRuleCreate"
              },
              "examples": {
                "medellin": {
                  "value": {
                    "name": "Clientes de Medellín con compras",
                    "rules": [
                      {
                        "propertyName": "city",
                        "condition": "equals",
                        "value": "Medellín",
                        "points": 10
                      },
                      {
                        "condition": "total_deals_count_greater_than",
                        "value": 2,
                        "points": 20
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Regla creada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScoringRuleResult"
                }
              }
            }
          },
          "400": {
            "description": "Payload inválido (campo obligatorio, condición o propiedad desconocida, valor con forma incorrecta, id de otra organización).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScoringRuleError"
                },
                "examples": {
                  "property": {
                    "value": {
                      "success": false,
                      "error": "rules[0]: propertyName \"ciudad\" no es una propiedad de contacto de la organización (consulta GET /api/contact-properties)"
                    }
                  },
                  "value": {
                    "value": {
                      "success": false,
                      "error": "rules[1]: value debe ser un número para greater_than"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/scoring-rules/recalculate": {
      "post": {
        "operationId": "recalculateScoringRules",
        "tags": [
          "Scoring rules"
        ],
        "summary": "Recalcular el puntaje de todos los contactos",
        "description": "Requiere settings:write o *. Lanza en segundo plano el recálculo del leadScore de TODOS los contactos de la organización con las reglas activas (sobrescribe el puntaje actual) y responde 202 con el jobId. Si ya hay uno en curso devuelve ese (alreadyRunning true).",
        "x-required-permissions": [
          "settings:write"
        ],
        "responses": {
          "202": {
            "description": "Recálculo lanzado (o en curso).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScoringRecalculationResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/scoring-rules/recalculate/{jobId}": {
      "parameters": [
        {
          "name": "jobId",
          "in": "path",
          "required": true,
          "description": "jobId devuelto por POST /scoring-rules/recalculate.",
          "schema": {
            "type": "string"
          },
          "example": "recalc_3f2a1c9e-5b7d-4e8a-9c1f-2d3e4f5a6b7c"
        }
      ],
      "get": {
        "operationId": "getScoringRulesRecalculation",
        "tags": [
          "Scoring rules"
        ],
        "summary": "Estado de un recálculo de puntajes",
        "description": "Requiere settings:read o *. Los jobs viven en memoria del servidor: tras un reinicio, o una hora después de terminar, responden 404. Un job de otra organización también responde 404.",
        "x-required-permissions": [
          "settings:read"
        ],
        "responses": {
          "200": {
            "description": "Estado del job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScoringRecalculationResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Job inexistente, vencido o de otra organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScoringRuleError"
                },
                "example": {
                  "success": false,
                  "error": "Recalculation job not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/scoring-rules/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB de la regla de puntuación.",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "66f10000000000000000c001"
        }
      ],
      "get": {
        "operationId": "getScoringRuleById",
        "tags": [
          "Scoring rules"
        ],
        "summary": "Detalle de una regla de puntuación",
        "description": "Requiere settings:read o *. Un ID inválido o de otra organización responde 404.",
        "x-required-permissions": [
          "settings:read"
        ],
        "responses": {
          "200": {
            "description": "Regla encontrada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScoringRuleResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Regla inexistente, ID inválido o de otra organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScoringRuleError"
                },
                "example": {
                  "success": false,
                  "error": "Scoring rule not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "put": {
        "operationId": "updateScoringRuleById",
        "tags": [
          "Scoring rules"
        ],
        "summary": "Editar una regla de puntuación",
        "description": "Requiere settings:write o *. Parcial: name, description (null la borra), isActive y rules (reemplaza todas las condiciones). Sin ninguno responde 400. No recalcula puntajes.",
        "x-required-permissions": [
          "settings:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScoringRuleUpdate"
              },
              "examples": {
                "deactivate": {
                  "value": {
                    "isActive": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Regla actualizada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScoringRuleResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1scoring-rules/post/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1scoring-rules~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "delete": {
        "operationId": "deleteScoringRuleById",
        "tags": [
          "Scoring rules"
        ],
        "summary": "Eliminar una regla de puntuación",
        "description": "Requiere settings:write o *. Irreversible. Los puntajes ya calculados no cambian hasta el próximo recálculo.",
        "x-required-permissions": [
          "settings:write"
        ],
        "responses": {
          "200": {
            "description": "Regla eliminada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScoringRuleDeleteResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1scoring-rules~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/deal-checklists": {
      "get": {
        "operationId": "listDealChecklists",
        "tags": [
          "Deal checklists"
        ],
        "summary": "Listar los checklists de cierre de los pipelines",
        "description": "Requiere pipelines:read o *. Un checklist por pipeline de negocios; los pipelines sin checklist no aparecen. Orden de creación.",
        "x-required-permissions": [
          "pipelines:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Checklists y paginación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealChecklistListResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/deal-checklists/{pipelineId}": {
      "parameters": [
        {
          "name": "pipelineId",
          "in": "path",
          "required": true,
          "description": "Identificador del pipeline de negocios (GET /deal-pipelines).",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "66f10000000000000000a001"
        }
      ],
      "get": {
        "operationId": "getDealChecklist",
        "tags": [
          "Deal checklists"
        ],
        "summary": "Checklist de cierre de un pipeline",
        "description": "Requiere pipelines:read o *. Si el pipeline aún no tiene checklist responde 200 con id null e items vacío. Pipeline inválido o de otra organización → 404.",
        "x-required-permissions": [
          "pipelines:read"
        ],
        "responses": {
          "200": {
            "description": "Checklist del pipeline.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealChecklistResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Pipeline inexistente, ID inválido o de otra organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealChecklistError"
                },
                "example": {
                  "success": false,
                  "error": "Pipeline not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "put": {
        "operationId": "updateDealChecklist",
        "tags": [
          "Deal checklists"
        ],
        "summary": "Reemplazar el checklist de un pipeline (upsert)",
        "description": "Requiere pipelines:write o *. Reemplaza TODOS los ítems (lo crea si no existe). Incluya el id de los ítems que conserva para no perder las marcas de los negocios. requiredForStage debe ser una etapa de ese pipeline (si no, 400). Máximo 50 ítems.",
        "x-required-permissions": [
          "pipelines:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DealChecklistUpdate"
              },
              "examples": {
                "cierre": {
                  "value": {
                    "items": [
                      {
                        "name": "Llamada de descubrimiento",
                        "weight": 1,
                        "activityType": "llamada"
                      },
                      {
                        "name": "Enviar cotización",
                        "weight": 3,
                        "requiredForStage": "66f10000000000000000b002",
                        "enforcement": "block"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Checklist guardado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealChecklistResult"
                }
              }
            }
          },
          "400": {
            "description": "Ítems inválidos o requiredForStage que no es etapa del pipeline.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealChecklistError"
                },
                "examples": {
                  "name": {
                    "value": {
                      "success": false,
                      "error": "items[0]: name es obligatorio"
                    }
                  },
                  "stage": {
                    "value": {
                      "success": false,
                      "error": "requiredForStage debe ser una etapa de este pipeline: 66f10000000000000000b009 no lo es"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1deal-checklists~1{pipelineId}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/product-variants": {
      "get": {
        "operationId": "listProductVariants",
        "tags": [
          "Product variants"
        ],
        "summary": "Listar variantes de producto",
        "description": "Requiere products:read o *. Con productId, solo las de ese producto (404 si el producto no es de la organización; 400 si no es un id). Orden de creación.",
        "x-required-permissions": [
          "products:read"
        ],
        "parameters": [
          {
            "name": "productId",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^[a-fA-F0-9]{24}$"
            }
          },
          {
            "name": "isActive",
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Variantes y paginación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductVariantListResult"
                }
              }
            }
          },
          "400": {
            "description": "Payload o filtro inválido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductVariantError"
                },
                "examples": {
                  "sku": {
                    "value": {
                      "success": false,
                      "error": "sku es obligatorio"
                    }
                  },
                  "price": {
                    "value": {
                      "success": false,
                      "error": "price es obligatorio (número mayor o igual a 0)"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Producto padre inexistente o de otra organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductVariantError"
                },
                "example": {
                  "success": false,
                  "error": "Product not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "post": {
        "operationId": "createProductVariant",
        "tags": [
          "Product variants"
        ],
        "summary": "Crear una variante de un producto",
        "description": "Requiere products:write o *. El producto padre tiene que ser de la organización (404) y el sku único en la organización (409). No cambia hasVariants del producto.",
        "x-required-permissions": [
          "products:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProductVariantCreate"
              },
              "examples": {
                "rojo": {
                  "value": {
                    "productId": "66f10000000000000000e001",
                    "sku": "MAN-TYV-ROJ",
                    "price": 350,
                    "stock": 1000,
                    "attributeValues": [
                      {
                        "attributeName": "Color",
                        "value": "Rojo"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Variante creada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductVariantResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1product-variants/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1product-variants/get/responses/404"
          },
          "409": {
            "description": "Otra variante de la organización ya usa ese sku.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductVariantError"
                },
                "example": {
                  "success": false,
                  "error": "Ya existe una variante con ese sku en la organización",
                  "code": "DUPLICATE_SKU"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/product-variants/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB de la variante.",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "66f10000000000000000d001"
        }
      ],
      "get": {
        "operationId": "getProductVariantById",
        "tags": [
          "Product variants"
        ],
        "summary": "Detalle de una variante",
        "description": "Requiere products:read o *. Un ID inválido o de otra organización responde 404.",
        "x-required-permissions": [
          "products:read"
        ],
        "responses": {
          "200": {
            "description": "Variante encontrada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductVariantResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Variante inexistente, ID inválido o de otra organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductVariantError"
                },
                "example": {
                  "success": false,
                  "error": "Product variant not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "put": {
        "operationId": "updateProductVariantById",
        "tags": [
          "Product variants"
        ],
        "summary": "Editar una variante",
        "description": "Requiere products:write o *. Parcial: sku, price, stock, imageUrl (null limpia los dos últimos), isActive y attributeValues (reemplaza la lista). productId no se puede cambiar.",
        "x-required-permissions": [
          "products:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProductVariantUpdate"
              },
              "examples": {
                "stock": {
                  "value": {
                    "stock": 250
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Variante actualizada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductVariantResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1product-variants/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1product-variants~1{id}/get/responses/404"
          },
          "409": {
            "$ref": "#/paths/~1product-variants/post/responses/409"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "delete": {
        "operationId": "deleteProductVariantById",
        "tags": [
          "Product variants"
        ],
        "summary": "Eliminar una variante",
        "description": "Requiere products:delete o * (igual que borrar un producto). Irreversible.",
        "x-required-permissions": [
          "products:delete"
        ],
        "responses": {
          "200": {
            "description": "Variante eliminada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductVariantDeleteResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1product-variants~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/invoice-config": {
      "get": {
        "operationId": "getInvoiceConfig",
        "tags": [
          "Invoice config"
        ],
        "summary": "Configuración de facturación electrónica (solo lectura)",
        "description": "Requiere settings:read o * y el módulo de facturación electrónica del plan (en sombra hasta que se active). Devuelve empresa emisora, resoluciones y consecutivos, ambiente, certificado (cargado y vencimiento), correo SMTP/IMAP sin contraseñas y los textos por defecto. Ningún secreto sale, ni enmascarado. 404 si la organización no ha configurado facturación. No hay escritura por API: se configura desde la app.",
        "x-required-permissions": [
          "settings:read"
        ],
        "responses": {
          "200": {
            "description": "Configuración de la organización del token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceConfigResult"
                },
                "examples": {
                  "found": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "inv_1726000000000",
                        "environment": "production",
                        "company": {
                          "businessName": "Manillas SAS",
                          "nit": "900123456",
                          "dv": "7",
                          "email": "facturacion@empresa.co",
                          "phone": "3001234567",
                          "address": "Calle 10 # 20-30",
                          "municipalityId": "149",
                          "typeDocumentIdentificationId": "6",
                          "typeOrganizationId": "1",
                          "typeRegimeId": "1",
                          "typeLiabilityId": "7"
                        },
                        "invoiceResolution": {
                          "typeDocumentId": "01",
                          "prefix": "FE",
                          "resolution": "18764000000001",
                          "resolutionDate": "2026-01-15",
                          "from": "1",
                          "to": "5000",
                          "dateFrom": "2026-01-15",
                          "dateTo": "2028-01-15",
                          "nextNumber": "1043",
                          "hasTechnicalKey": true
                        },
                        "creditNote": {
                          "prefix": "NC",
                          "resolution": "0000000000",
                          "nextNumber": "24",
                          "headNote": null,
                          "footNote": null
                        },
                        "debitNote": null,
                        "supportDocument": null,
                        "certificate": {
                          "uploaded": true,
                          "expirationDate": "2027-03-01"
                        },
                        "software": {
                          "configured": true
                        },
                        "provider": {
                          "hasToken": true
                        },
                        "smtp": {
                          "configured": true,
                          "host": "smtp.gmail.com",
                          "port": 587,
                          "username": "facturacion@empresa.co",
                          "encryption": "tls",
                          "hasPassword": true
                        },
                        "imap": {
                          "enabled": true,
                          "configured": true,
                          "host": "imap.gmail.com",
                          "port": 993,
                          "username": "facturacion@empresa.co",
                          "tls": true,
                          "mailbox": "INBOX",
                          "autoProcess": true,
                          "markAsRead": true,
                          "deleteAfterProcess": false,
                          "hasPassword": true
                        },
                        "placeholders": {
                          "paymentTerms": "30 días",
                          "currency": "COP",
                          "notes": "Gracias por su compra",
                          "logoImg": "https://cdn.example.com/logo.png",
                          "headNote": null,
                          "footNote": null,
                          "shippingTerms": null,
                          "subjectMail": null,
                          "messageMail": null
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "La organización no ha configurado facturación electrónica.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceConfigError"
                },
                "example": {
                  "success": false,
                  "error": "Invoice configuration not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/integrations": {
      "get": {
        "operationId": "listIntegrations",
        "tags": [
          "Integrations"
        ],
        "summary": "Listar las integraciones conectadas (solo lectura)",
        "description": "Requiere settings:read o *. En orden de creación. Sin credenciales ni settings: solo servicio, nombre, si está activa y los nombres de las credenciales guardadas.",
        "x-required-permissions": [
          "settings:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Integraciones y paginación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IntegrationListResult"
                },
                "examples": {
                  "list": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "66f10000000000000000c001",
                          "service": "whatsapp",
                          "name": "WhatsApp ventas",
                          "isActive": true,
                          "configuredKeys": [
                            "accessToken",
                            "numberId",
                            "phoneNumber",
                            "verifyToken",
                            "whatsappAccountId"
                          ],
                          "createdAt": "2026-08-27T14:00:00.000Z",
                          "updatedAt": "2026-09-20T10:00:00.000Z"
                        }
                      ],
                      "pagination": {
                        "page": 1,
                        "limit": 100,
                        "total": 1,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/integrations/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB de la integración (campo id de las respuestas).",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "66f10000000000000000c001"
        }
      ],
      "get": {
        "operationId": "getIntegrationById",
        "tags": [
          "Integrations"
        ],
        "summary": "Detalle de una integración (solo lectura)",
        "description": "Requiere settings:read o *. Un ID que no es ObjectId o de otra organización responde 404.",
        "x-required-permissions": [
          "settings:read"
        ],
        "responses": {
          "200": {
            "description": "Integración encontrada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IntegrationResult"
                },
                "examples": {
                  "found": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "66f10000000000000000c001",
                        "service": "whatsapp",
                        "name": "WhatsApp ventas",
                        "isActive": true,
                        "configuredKeys": [
                          "accessToken",
                          "numberId",
                          "phoneNumber",
                          "verifyToken",
                          "whatsappAccountId"
                        ],
                        "createdAt": "2026-08-27T14:00:00.000Z",
                        "updatedAt": "2026-09-20T10:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Integración inexistente, ID inválido o de otra organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IntegrationError"
                },
                "example": {
                  "success": false,
                  "error": "Integration not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/mail-accounts": {
      "get": {
        "operationId": "listMailAccounts",
        "tags": [
          "Mail accounts"
        ],
        "summary": "Listar las cuentas de correo IMAP (solo lectura)",
        "description": "Requiere settings:read o *. En orden de creación. Estado, último error y última sincronización de cada cuenta, con el servidor IMAP; nunca la contraseña.",
        "x-required-permissions": [
          "settings:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cuentas y paginación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MailAccountListResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/mail-accounts/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB de la cuenta de correo (campo id de las respuestas).",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "66f10000000000000000d001"
        }
      ],
      "get": {
        "operationId": "getMailAccountById",
        "tags": [
          "Mail accounts"
        ],
        "summary": "Detalle de una cuenta de correo IMAP (solo lectura)",
        "description": "Requiere settings:read o *. Un ID que no es ObjectId o de otra organización responde 404.",
        "x-required-permissions": [
          "settings:read"
        ],
        "responses": {
          "200": {
            "description": "Cuenta encontrada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MailAccountResult"
                },
                "examples": {
                  "found": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "66f10000000000000000d001",
                        "email": "ventas@empresa.co",
                        "provider": "zoho",
                        "status": "active",
                        "isActive": true,
                        "lastError": null,
                        "lastErrorCode": null,
                        "lastCheckedAt": "2026-09-25T13:58:00.000Z",
                        "lastSyncAt": "2026-09-25T13:58:00.000Z",
                        "syncing": false,
                        "imapSettings": {
                          "host": "imap.zoho.com",
                          "port": 993,
                          "user": "ventas@empresa.co",
                          "tls": true
                        },
                        "createdAt": "2026-09-04T14:00:00.000Z",
                        "updatedAt": "2026-09-25T13:58:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Cuenta inexistente, ID inválido o de otra organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MailAccountError"
                },
                "example": {
                  "success": false,
                  "error": "Mail account not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/activities": {
      "get": {
        "operationId": "listActivities",
        "tags": [
          "Activities"
        ],
        "summary": "Listar seguimientos de contactos y empresas",
        "description": "Requiere activities:read o *. Más recientes primero (por date). from/to filtran sobre date; un to sin hora (YYYY-MM-DD) incluye todo ese día. No incluye las actividades del timeline de un negocio ni los eventos del calendario.",
        "x-required-permissions": [
          "activities:read"
        ],
        "parameters": [
          {
            "name": "contactId",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^[a-fA-F0-9]{24}$"
            }
          },
          {
            "name": "companyId",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^[a-fA-F0-9]{24}$"
            }
          },
          {
            "name": "advisorId",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^[a-fA-F0-9]{24}$"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "incomplete",
                "complete"
              ]
            }
          },
          {
            "name": "activityType",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "Reunion",
                "Llamada",
                "Correo",
                "Nota",
                "Whatsapp"
              ]
            }
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "YYYY-MM-DD o ISO 8601, inclusive."
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "YYYY-MM-DD (todo el día) o ISO 8601, inclusive."
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Seguimientos y paginación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActivityListResult"
                },
                "examples": {
                  "list": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "66f20000000000000000c001",
                          "activityType": "Llamada",
                          "title": "Confirmar pedido de manillas",
                          "date": "2026-09-25",
                          "notes": "",
                          "status": "incomplete",
                          "contactId": "66f20000000000000000a001",
                          "companyId": null,
                          "ownerId": "66f20000000000000000b001",
                          "advisorId": null,
                          "reminder": null,
                          "createdAt": "2026-09-24T19:12:00.000Z",
                          "updatedAt": "2026-09-24T19:12:00.000Z"
                        }
                      ],
                      "pagination": {
                        "page": 1,
                        "limit": 100,
                        "total": 1,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Payload o filtro inválido, o contacto/empresa/asesor que no es de la organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActivityError"
                },
                "examples": {
                  "tipo": {
                    "value": {
                      "success": false,
                      "error": "activityType debe ser uno de: Reunion, Llamada, Correo, Nota, Whatsapp"
                    }
                  },
                  "sinFicha": {
                    "value": {
                      "success": false,
                      "error": "La actividad debe estar vinculada al menos a un contacto (contactId) o a una empresa (companyId)"
                    }
                  },
                  "contacto": {
                    "value": {
                      "success": false,
                      "error": "El contacto no existe en esta organización"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "post": {
        "operationId": "createActivity",
        "tags": [
          "Activities"
        ],
        "summary": "Registrar un seguimiento",
        "description": "Requiere activities:write o *. Queda a nombre del usuario dueño del token (ownerId). contactId, companyId y advisorId deben ser de la organización del token (si no, 400). Con reminder, el asesor recibe un aviso por push y correo a esa hora. No envía nada al contacto.",
        "x-required-permissions": [
          "activities:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ActivityCreate"
              },
              "examples": {
                "llamada": {
                  "value": {
                    "activityType": "Llamada",
                    "title": "Confirmar pedido de manillas",
                    "date": "2026-09-25",
                    "contactId": "66f20000000000000000a001",
                    "reminder": "2026-09-25T15:00:00.000Z"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Seguimiento creado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActivityResult"
                },
                "examples": {
                  "created": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "66f20000000000000000c001",
                        "activityType": "Llamada",
                        "title": "Confirmar pedido de manillas",
                        "date": "2026-09-25",
                        "notes": "Pidió cotización para 500 unidades.",
                        "status": "incomplete",
                        "contactId": "66f20000000000000000a001",
                        "companyId": null,
                        "ownerId": "66f20000000000000000b001",
                        "advisorId": "66f20000000000000000b002",
                        "reminder": "2026-09-25T15:00:00.000Z",
                        "createdAt": "2026-09-24T19:12:00.000Z",
                        "updatedAt": "2026-09-24T19:12:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1activities/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/activities/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB del seguimiento (campo id de las respuestas).",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "66f20000000000000000c001"
        }
      ],
      "get": {
        "operationId": "getActivityById",
        "tags": [
          "Activities"
        ],
        "summary": "Detalle de un seguimiento",
        "description": "Requiere activities:read o *. Un ID que no es ObjectId o de otra organización responde 404.",
        "x-required-permissions": [
          "activities:read"
        ],
        "responses": {
          "200": {
            "description": "Seguimiento encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActivityResult"
                },
                "examples": {
                  "found": {
                    "$ref": "#/paths/~1activities/post/responses/201/content/application~1json/examples/created"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Seguimiento inexistente, ID inválido o de otra organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActivityError"
                },
                "example": {
                  "success": false,
                  "error": "Activity not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "put": {
        "operationId": "updateActivityById",
        "tags": [
          "Activities"
        ],
        "summary": "Editar un seguimiento",
        "description": "Requiere activities:write o *. Parcial. status complete/incomplete cierra o reabre también el próximo paso del negocio ligado. Cambiar reminder re-arma el aviso.",
        "x-required-permissions": [
          "activities:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ActivityUpdate"
              },
              "examples": {
                "hecho": {
                  "value": {
                    "status": "complete"
                  }
                },
                "quitarRecordatorio": {
                  "value": {
                    "reminder": null
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Seguimiento actualizado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActivityResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1activities/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1activities~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "delete": {
        "operationId": "deleteActivityById",
        "tags": [
          "Activities"
        ],
        "summary": "Eliminar un seguimiento",
        "description": "Requiere activities:write o *. Irreversible. Si era el recordatorio ligado a un paso de un negocio, ese paso queda sin recordatorio ligado.",
        "x-required-permissions": [
          "activities:write"
        ],
        "responses": {
          "200": {
            "description": "Seguimiento eliminado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActivityDeleteResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1activities~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/calendar-events": {
      "get": {
        "operationId": "listCalendarEvents",
        "tags": [
          "Calendar events"
        ],
        "summary": "Listar eventos del calendario en un rango",
        "description": "Requiere calendar:read o *. from y to son obligatorios; un rango mayor a 1098 días se recorta (range.clamped). Las series recurrentes salen expandidas en ocurrencias (occurrenceStart). Con includeActivities=true suma los seguimientos de contactos como eventos de solo lectura (id activity:<id>). Orden cronológico, paginado.",
        "x-required-permissions": [
          "calendar:read"
        ],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Inicio, ISO 8601."
          },
          {
            "name": "to",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Fin, ISO 8601."
          },
          {
            "name": "assignedTo",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^[a-fA-F0-9]{24}$"
            }
          },
          {
            "name": "includeCreatedBy",
            "in": "query",
            "schema": {
              "type": "boolean"
            },
            "description": "Con assignedTo, suma los eventos que esa persona creó."
          },
          {
            "name": "contactId",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^[a-fA-F0-9]{24}$"
            }
          },
          {
            "name": "includeActivities",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Eventos del rango, paginación y el rango efectivo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CalendarEventListResult"
                },
                "examples": {
                  "list": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "66f30000000000000000e002",
                          "source": "event",
                          "title": "Reunión semanal de ventas",
                          "description": "",
                          "start": "2026-09-29T14:00:00.000Z",
                          "end": "2026-09-29T14:30:00.000Z",
                          "allDay": false,
                          "timezone": "America/Bogota",
                          "type": "reunion",
                          "tags": [],
                          "location": "",
                          "attendees": [],
                          "assignedTo": null,
                          "createdBy": "66f20000000000000000b001",
                          "contactId": null,
                          "contactName": null,
                          "reminderEnabled": false,
                          "reminderMinutesBefore": null,
                          "recurrenceRule": "FREQ=WEEKLY;BYDAY=TU",
                          "occurrenceStart": "2026-09-29T14:00:00.000Z",
                          "seriesStart": "2026-09-01T14:00:00.000Z",
                          "seriesEnd": "2026-09-01T14:30:00.000Z",
                          "recurringEventId": "66f30000000000000000e002",
                          "originalStart": null,
                          "excludedDates": [],
                          "externalId": "",
                          "invitationSentAt": null,
                          "createdAt": "2026-08-30T12:00:00.000Z",
                          "updatedAt": "2026-08-30T12:00:00.000Z"
                        }
                      ],
                      "pagination": {
                        "page": 1,
                        "limit": 100,
                        "total": 1,
                        "pages": 1
                      },
                      "range": {
                        "from": "2026-09-28T05:00:00.000Z",
                        "to": "2026-10-05T05:00:00.000Z",
                        "clamped": false
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Payload o rango inválido, referencias de otra organización u operación sobre un seguimiento.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CalendarEventError"
                },
                "examples": {
                  "rango": {
                    "value": {
                      "success": false,
                      "error": "Los parámetros 'from' y 'to' (ISO 8601) son obligatorios; el rango máximo es de 1098 días",
                      "code": "INVALID_DATE_RANGE"
                    }
                  },
                  "contacto": {
                    "value": {
                      "success": false,
                      "error": "El contacto no existe en esta organización."
                    }
                  },
                  "ocurrencia": {
                    "value": {
                      "success": false,
                      "error": "Esa fecha no es una ocurrencia vigente de la serie."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "post": {
        "operationId": "createCalendarEvent",
        "tags": [
          "Calendar events"
        ],
        "summary": "Crear un evento",
        "description": "Requiere calendar:write o *. assignedTo, contactId y los contactId/userId de los invitados deben ser de la organización (si no, 400). Los invitados SOLO reciben correo con sendInvitations true (a diferencia de la app, donde el default es enviar); el resultado viene en data.invitations.",
        "x-required-permissions": [
          "calendar:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CalendarEventCreate"
              },
              "examples": {
                "visita": {
                  "value": {
                    "title": "Visita al cliente",
                    "start": "2026-09-29T15:00:00.000Z",
                    "end": "2026-09-29T16:00:00.000Z",
                    "type": "reunion",
                    "contactId": "66f20000000000000000a001",
                    "reminderEnabled": true,
                    "reminderMinutesBefore": [
                      60
                    ]
                  }
                },
                "semanal": {
                  "value": {
                    "title": "Reunión semanal de ventas",
                    "start": "2026-09-01T14:00:00.000Z",
                    "end": "2026-09-01T14:30:00.000Z",
                    "recurrenceRule": "FREQ=WEEKLY;BYDAY=TU"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Evento creado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CalendarEventResult"
                },
                "examples": {
                  "created": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "66f30000000000000000e001",
                        "source": "event",
                        "title": "Visita al cliente",
                        "description": "",
                        "start": "2026-09-29T15:00:00.000Z",
                        "end": "2026-09-29T16:00:00.000Z",
                        "allDay": false,
                        "timezone": "America/Bogota",
                        "type": "reunion",
                        "tags": [],
                        "location": "Oficina del cliente",
                        "attendees": [
                          {
                            "email": "compras@cliente.com",
                            "name": "Ana",
                            "status": "needs_action",
                            "contactId": "66f20000000000000000a001",
                            "userId": null
                          }
                        ],
                        "assignedTo": "66f20000000000000000b001",
                        "createdBy": "66f20000000000000000b001",
                        "contactId": "66f20000000000000000a001",
                        "contactName": "Ana Gómez",
                        "reminderEnabled": true,
                        "reminderMinutesBefore": [
                          60
                        ],
                        "recurrenceRule": "",
                        "occurrenceStart": null,
                        "seriesStart": null,
                        "seriesEnd": null,
                        "recurringEventId": null,
                        "originalStart": null,
                        "excludedDates": [],
                        "externalId": "",
                        "invitationSentAt": null,
                        "createdAt": "2026-09-24T19:12:00.000Z",
                        "updatedAt": "2026-09-24T19:12:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1calendar-events/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/calendar-events/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB del evento (en una ocurrencia de una serie, el id de la serie). En GET se acepta además activity:<id> para leer un seguimiento como evento.",
          "schema": {
            "type": "string"
          },
          "example": "66f30000000000000000e001"
        }
      ],
      "get": {
        "operationId": "getCalendarEventById",
        "tags": [
          "Calendar events"
        ],
        "summary": "Detalle de un evento",
        "description": "Requiere calendar:read o *. Una serie recurrente se devuelve como serie (recurrenceRule, excludedDates). activity:<id> devuelve un seguimiento como evento. Un ID inválido o de otra organización responde 404.",
        "x-required-permissions": [
          "calendar:read"
        ],
        "responses": {
          "200": {
            "description": "Evento encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CalendarEventResult"
                },
                "examples": {
                  "found": {
                    "$ref": "#/paths/~1calendar-events/post/responses/201/content/application~1json/examples/created"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Evento inexistente, ID inválido o de otra organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CalendarEventError"
                },
                "example": {
                  "success": false,
                  "error": "Calendar event not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "put": {
        "operationId": "updateCalendarEventById",
        "tags": [
          "Calendar events"
        ],
        "summary": "Editar un evento, la serie o una ocurrencia",
        "description": "Requiere calendar:write o *. Parcial. Sin occurrenceStart edita la serie completa; con occurrenceStart desprende esa ocurrencia como evento propio (la respuesta es el evento nuevo). Con sendInvitations true los invitados reciben la invitación actualizada y los quitados una cancelación (correos); por defecto no se envía nada. Los ids activity:<id> responden 400 (se editan con /activities).",
        "x-required-permissions": [
          "calendar:write"
        ],
        "parameters": [
          {
            "name": "occurrenceStart",
            "in": "query",
            "required": false,
            "description": "Fecha-hora ISO de UNA ocurrencia de una serie recurrente (el occurrenceStart que trae el listado). Sin él, la operación aplica a la serie completa.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CalendarEventUpdate"
              },
              "examples": {
                "mover": {
                  "value": {
                    "start": "2026-09-30T15:00:00.000Z",
                    "end": "2026-09-30T16:00:00.000Z"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Evento actualizado (o la ocurrencia desprendida).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CalendarEventResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1calendar-events/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1calendar-events~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "delete": {
        "operationId": "deleteCalendarEventById",
        "tags": [
          "Calendar events"
        ],
        "summary": "Eliminar un evento, la serie o una ocurrencia",
        "description": "Requiere calendar:write o *. Irreversible. Sin occurrenceStart borra el evento y, si es recurrente, la SERIE ENTERA con sus ocurrencias desprendidas (scope series); con occurrenceStart quita solo esa fecha (scope occurrence). Con sendCancellations=true los invitados ya invitados reciben un correo de cancelación; por defecto no. Los ids activity:<id> responden 400 (se borran con /activities).",
        "x-required-permissions": [
          "calendar:write"
        ],
        "parameters": [
          {
            "$ref": "#/paths/~1calendar-events~1{id}/put/parameters/0"
          },
          {
            "name": "sendCancellations",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Evento, serie u ocurrencia eliminada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CalendarEventDeleteResult"
                },
                "examples": {
                  "serie": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "66f30000000000000000e002",
                        "scope": "series",
                        "detachedDeleted": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1calendar-events/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1calendar-events~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/notifications": {
      "get": {
        "operationId": "listNotifications",
        "tags": [
          "Notifications"
        ],
        "summary": "Listar las notificaciones del usuario del token",
        "description": "Requiere notifications:read o *. Devuelve la bandeja de la campana del USUARIO a cuyo nombre se emitió el token, dentro de su organización (nunca la de otro empleado). Más recientes primero. No marca nada como leído.",
        "x-required-permissions": [
          "notifications:read"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 100
            },
            "description": "Máximo 100; un valor mayor se recorta."
          },
          {
            "name": "unread",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "1"
              ]
            },
            "description": "true o 1 = solo las no leídas."
          }
        ],
        "responses": {
          "200": {
            "description": "Notificaciones, total de no leídas y paginación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotificationListResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/notifications/unread-count": {
      "get": {
        "operationId": "getNotificationsUnreadCount",
        "tags": [
          "Notifications"
        ],
        "summary": "Contador de no leídas",
        "description": "Requiere notifications:read o *. El mismo número que el globo de la campana.",
        "x-required-permissions": [
          "notifications:read"
        ],
        "responses": {
          "200": {
            "description": "No leídas del usuario del token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotificationUnreadCountResult"
                },
                "example": {
                  "success": true,
                  "data": {
                    "unread": 3
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/notifications/{id}/read": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador de la notificación.",
          "schema": {
            "type": "string"
          }
        }
      ],
      "patch": {
        "operationId": "markNotificationRead",
        "tags": [
          "Notifications"
        ],
        "summary": "Marcar una notificación como leída",
        "description": "Requiere notifications:write o *. Idempotente: si ya estaba leída responde 200 con alreadyRead true. Un id inválido, inexistente o de otro usuario/organización responde 404.",
        "x-required-permissions": [
          "notifications:write"
        ],
        "responses": {
          "200": {
            "description": "Marcada (o ya lo estaba).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotificationMarkReadResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Notificación no encontrada para el usuario del token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotificationError"
                },
                "example": {
                  "success": false,
                  "error": "Notification not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/notifications/read-all": {
      "post": {
        "operationId": "markAllNotificationsRead",
        "tags": [
          "Notifications"
        ],
        "summary": "Marcar todas como leídas",
        "description": "Requiere notifications:write o *. Solo las del usuario del token; no borra nada.",
        "x-required-permissions": [
          "notifications:write"
        ],
        "responses": {
          "200": {
            "description": "Cuántas se marcaron.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotificationMarkAllReadResult"
                },
                "example": {
                  "success": true,
                  "data": {
                    "updated": 4,
                    "unread": 0
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/exports": {
      "post": {
        "operationId": "requestOrganizationExport",
        "tags": [
          "Exports"
        ],
        "summary": "Pedir el export completo de la organización",
        "description": "Requiere exports:write o * Y que el usuario del token sea el PROPIETARIO de la organización (si no, 403 OWNER_REQUIRED). Arma en segundo plano un ZIP con CSV (contactos, empresas, negocios, conversaciones, mensajes, cotizaciones, facturas, tareas, eventos, usuarios) y organization.json sin secretos, y envía el enlace por correo al propietario. Si ya hay uno en curso no inicia otro (alreadyRunning true). No modifica datos.",
        "x-required-permissions": [
          "exports:write"
        ],
        "responses": {
          "202": {
            "description": "Export encolado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportRequestedResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "description": "Falta el permiso del token, o el usuario del token no es el propietario.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportError"
                },
                "example": {
                  "success": false,
                  "error": "Only the organization owner can export its data",
                  "code": "OWNER_REQUIRED"
                }
              }
            }
          },
          "409": {
            "description": "El propietario no tiene correo para recibir el enlace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportError"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/exports/latest": {
      "get": {
        "operationId": "getLatestOrganizationExport",
        "tags": [
          "Exports"
        ],
        "summary": "Estado del último export y enlace de descarga",
        "description": "Requiere exports:read o * Y ser el PROPIETARIO (si no, 403 OWNER_REQUIRED). Si el ZIP sigue en S3 firma una URL NUEVA que caduca en 24 h; nunca hay URL pública permanente. El enlace da acceso a todos los datos de la organización.",
        "x-required-permissions": [
          "exports:read"
        ],
        "responses": {
          "200": {
            "description": "Estado del último export.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportLatestResult"
                },
                "examples": {
                  "ready": {
                    "value": {
                      "success": true,
                      "data": {
                        "status": "ready",
                        "downloadUrl": "https://bucket.s3.amazonaws.com/exports/66f1/2026-09-25.zip?X-Amz-Signature=abc",
                        "expiresAt": "2026-09-26T15:00:00.000Z",
                        "createdAt": "2026-09-25T14:58:00.000Z"
                      }
                    }
                  },
                  "none": {
                    "value": {
                      "success": true,
                      "data": {
                        "status": "none",
                        "downloadUrl": null,
                        "expiresAt": null,
                        "createdAt": null
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "description": "Falta el permiso del token, o el usuario del token no es el propietario.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportError"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/automations": {
      "get": {
        "operationId": "listAutomations",
        "tags": [
          "Automations"
        ],
        "summary": "Listar automatizaciones",
        "description": "Requiere automations:read o *. Más recientes primero. Cada una trae su estado (draft/published/paused), si tiene un borrador sin publicar, sus disparadores reales, los del borrador, la última ejecución y el conteo de ejecuciones por estado en la retención del historial (~30 días). No incluye los pasos (GET /automations/{id}).",
        "x-required-permissions": [
          "automations:read"
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "published",
                "paused"
              ]
            },
            "description": "published incluye las automatizaciones viejas sin estado guardado."
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Automatizaciones y paginación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutomationListResult"
                },
                "examples": {
                  "list": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "66f1000000000000000000e1",
                          "name": "Seguimiento de cotización",
                          "status": "published",
                          "publishedAt": "2026-09-20T15:00:00.000Z",
                          "publishedVersion": 3,
                          "hasUnpublishedChanges": false,
                          "reEntry": null,
                          "triggers": [
                            {
                              "id": "66f1000000000000000000f1",
                              "name": "Cotización creada",
                              "type": "quotation_created",
                              "isActive": true,
                              "stats": {
                                "totalRuns": 42,
                                "successCount": 40,
                                "failureCount": 2,
                                "lastRunAt": "2026-09-24T21:10:00.000Z",
                                "lastStatus": "success"
                              }
                            }
                          ],
                          "draftTriggers": [],
                          "lastRun": {
                            "id": "66f100000000000000000a01",
                            "status": "success",
                            "startedAt": "2026-09-24T21:10:00.000Z",
                            "finishedAt": "2026-09-24T21:10:02.000Z"
                          },
                          "runs": {
                            "total": 18,
                            "success": 17,
                            "failed": 1,
                            "partial": 0,
                            "waiting": 0,
                            "running": 0,
                            "cancelled": 0
                          },
                          "createdAt": "2026-09-01T12:00:00.000Z",
                          "updatedAt": "2026-09-20T15:00:00.000Z"
                        }
                      ],
                      "pagination": {
                        "page": 1,
                        "limit": 100,
                        "total": 1,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Parámetro o payload inválido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutomationError"
                },
                "examples": {
                  "status": {
                    "value": {
                      "success": false,
                      "error": "status debe ser draft, published o paused"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "post": {
        "operationId": "createAutomation",
        "tags": [
          "Automations"
        ],
        "summary": "Crear una automatización en BORRADOR",
        "description": "Requiere automations:write o *. La automatización nace SIEMPRE con status draft y los disparadores del borrador con isActive false, aunque el body diga otra cosa: no se dispara ni ejecuta nada hasta que un humano la publique desde la web (Automatizaciones → Publicar). Por API no se puede publicar, pausar, activar, probar ni borrar. El grafo se valida con las mismas reglas del botón Publicar (tipos de acción, variables disponibles para los disparadores, cron, filtros que el disparador guarda) y responde 400 con la lista de errores. Con AUTOMATION_STEPS_ENABLED apagado en el servidor responde 409.",
        "x-required-permissions": [
          "automations:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AutomationCreate"
              },
              "examples": {
                "bienvenida": {
                  "value": {
                    "name": "Bienvenida a contactos nuevos",
                    "triggers": [
                      {
                        "name": "Contacto nuevo",
                        "type": "contact_created"
                      }
                    ],
                    "steps": [
                      {
                        "stepId": "s_tarea",
                        "type": "create_task",
                        "config": {
                          "title": "Llamar a {{contact.phone}}"
                        }
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Automatización creada en borrador.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutomationResult"
                },
                "examples": {
                  "created": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "66f1000000000000000000e2",
                        "name": "Bienvenida a contactos nuevos",
                        "status": "draft",
                        "publishedAt": null,
                        "publishedVersion": 0,
                        "hasUnpublishedChanges": true,
                        "reEntry": null,
                        "published": null,
                        "draft": {
                          "name": "Bienvenida a contactos nuevos",
                          "steps": [
                            {
                              "stepId": "s_tarea",
                              "type": "create_task",
                              "enabled": true,
                              "onError": "stop",
                              "config": {
                                "title": "Llamar a {{contact.phone}}"
                              }
                            }
                          ],
                          "triggers": [
                            {
                              "triggerId": "66f1000000000000000000f2",
                              "name": "Contacto nuevo",
                              "type": "contact_created",
                              "isActive": false,
                              "description": null,
                              "filters": {},
                              "schedule": null
                            }
                          ],
                          "updatedAt": "2026-09-25T14:00:00.000Z"
                        },
                        "lastRun": null,
                        "runs": {
                          "total": 0,
                          "success": 0,
                          "failed": 0,
                          "partial": 0,
                          "waiting": 0,
                          "running": 0,
                          "cancelled": 0
                        },
                        "createdAt": "2026-09-25T14:00:00.000Z",
                        "updatedAt": "2026-09-25T14:00:00.000Z"
                      },
                      "warnings": []
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "El body no es un grafo (error) o el grafo no pasa la validación (error + errors + warnings), o un ***REDACTED*** sin valor guardado (error + details).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/AutomationValidationError"
                    },
                    {
                      "$ref": "#/components/schemas/AutomationError"
                    }
                  ]
                },
                "examples": {
                  "graph": {
                    "value": {
                      "success": false,
                      "error": "El grafo de la automatización tiene errores",
                      "errors": [
                        {
                          "stepId": "s_hola",
                          "field": "config",
                          "message": "La variable {{trigger.contact_name}} no está disponible con los disparadores seleccionados",
                          "severity": "error"
                        }
                      ],
                      "warnings": []
                    }
                  },
                  "shape": {
                    "value": {
                      "success": false,
                      "error": "steps debe ser un arreglo"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "409": {
            "description": "El servidor tiene apagadas las automatizaciones por pasos (AUTOMATION_STEPS_ENABLED).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutomationError"
                },
                "example": {
                  "success": false,
                  "error": "Las automatizaciones por pasos no están habilitadas en este servidor (AUTOMATION_STEPS_ENABLED)"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/automations/variables": {
      "get": {
        "operationId": "getAutomationVariables",
        "tags": [
          "Automations"
        ],
        "summary": "Catálogo de variables, disparadores y acciones",
        "description": "Requiere automations:read o *. Variables {{...}} disponibles para los tipos de disparador dados (sin triggers: solo las globales), los tipos de disparador válidos y los tipos de acción. No depende de la organización.",
        "x-required-permissions": [
          "automations:read"
        ],
        "parameters": [
          {
            "name": "triggers",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Tipos de disparador separados por coma. Uno desconocido → 400.",
            "example": "quotation_created,deal_status_changed"
          }
        ],
        "responses": {
          "200": {
            "description": "Catálogo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutomationVariablesResult"
                },
                "examples": {
                  "variables": {
                    "value": {
                      "success": true,
                      "data": {
                        "variables": [
                          {
                            "path": "contact.phone",
                            "label": "Teléfono del contacto",
                            "group": "Contacto",
                            "type": "string",
                            "example": "573001112233",
                            "triggers": "*"
                          },
                          {
                            "path": "trigger.quotation_number",
                            "label": "Número de la cotización",
                            "group": "Disparador",
                            "type": "string",
                            "example": "1024",
                            "triggers": [
                              "quotation_created",
                              "quotation_viewed"
                            ]
                          }
                        ],
                        "partial": [],
                        "triggerTypes": [
                          "inbound_message",
                          "contact_created",
                          "quotation_created"
                        ],
                        "actionTypes": [
                          {
                            "type": "send_whatsapp_template",
                            "description": "Envía una plantilla aprobada de WhatsApp."
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1automations/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/automations/validate": {
      "post": {
        "operationId": "validateAutomation",
        "tags": [
          "Automations"
        ],
        "summary": "Validar un grafo sin guardarlo",
        "description": "Requiere automations:read o *. Mismas reglas que POST /automations y que Publicar en la web. Responde 200 con valid, errors (bloquean) y warnings; 400 solo si el body ni siquiera es un grafo (name no es texto, steps o triggers no son arreglos). No guarda nada.",
        "x-required-permissions": [
          "automations:read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AutomationGraphInput"
              },
              "examples": {
                "vacio": {
                  "value": {
                    "name": "X",
                    "triggers": [],
                    "steps": []
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resultado de la validación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutomationValidationResult"
                },
                "examples": {
                  "invalid": {
                    "value": {
                      "success": true,
                      "data": {
                        "valid": false,
                        "errors": [
                          {
                            "stepId": null,
                            "field": "triggers",
                            "message": "La automatización necesita al menos un disparador",
                            "severity": "error"
                          }
                        ],
                        "warnings": []
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1automations/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/automations/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB de la automatización (campo id de las respuestas).",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "66f1000000000000000000e1"
        }
      ],
      "get": {
        "operationId": "getAutomation",
        "tags": [
          "Automations"
        ],
        "summary": "Obtener una automatización (publicado y borrador)",
        "description": "Requiere automations:read o *. published = lo que corre hoy (pasos y disparadores reales; null si nunca se publicó); draft = los cambios pendientes de publicar (null si no hay). Los secretos de las acciones (headers de autorización y cualquier header que no sea de contenido en http_request, apiKey, tokens, contraseñas) salen como ***REDACTED***. Id de otra organización o inexistente → 404.",
        "x-required-permissions": [
          "automations:read"
        ],
        "responses": {
          "200": {
            "description": "Automatización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutomationResult"
                },
                "examples": {
                  "found": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "66f1000000000000000000e1",
                        "name": "Seguimiento de cotización",
                        "status": "published",
                        "publishedAt": "2026-09-20T15:00:00.000Z",
                        "publishedVersion": 3,
                        "hasUnpublishedChanges": true,
                        "reEntry": null,
                        "published": {
                          "name": "Seguimiento de cotización",
                          "steps": [
                            {
                              "stepId": "s_webhook",
                              "type": "http_request",
                              "name": "Avisar al ERP",
                              "enabled": true,
                              "onError": "stop",
                              "config": {
                                "url": "https://erp.example.com/hooks/cotizacion",
                                "method": "POST",
                                "headers": {
                                  "Authorization": "***REDACTED***",
                                  "Content-Type": "application/json"
                                },
                                "body": {
                                  "numero": "{{trigger.quotation_number}}"
                                }
                              }
                            }
                          ],
                          "triggers": [
                            {
                              "id": "66f1000000000000000000f1",
                              "name": "Cotización creada",
                              "type": "quotation_created",
                              "isActive": true,
                              "stats": null,
                              "description": null,
                              "filters": {
                                "minTotal": 1000000
                              },
                              "schedule": null
                            }
                          ]
                        },
                        "draft": {
                          "name": "Seguimiento de cotización v2",
                          "steps": [
                            {
                              "stepId": "s_wa",
                              "type": "send_whatsapp_template",
                              "enabled": true,
                              "onError": "stop",
                              "config": {
                                "templateName": "seguimiento_cotizacion",
                                "to": "{{contact.phone}}"
                              }
                            }
                          ],
                          "triggers": [
                            {
                              "triggerId": "66f1000000000000000000f1",
                              "name": "Cotización creada",
                              "type": "quotation_created",
                              "isActive": false,
                              "description": null,
                              "filters": {
                                "minTotal": 1000000
                              },
                              "schedule": null
                            }
                          ],
                          "updatedAt": "2026-09-25T14:00:00.000Z"
                        },
                        "lastRun": null,
                        "runs": {
                          "total": 0,
                          "success": 0,
                          "failed": 0,
                          "partial": 0,
                          "waiting": 0,
                          "running": 0,
                          "cancelled": 0
                        },
                        "createdAt": "2026-09-01T12:00:00.000Z",
                        "updatedAt": "2026-09-25T14:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "La automatización no existe o es de otra organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutomationError"
                },
                "example": {
                  "success": false,
                  "error": "Automation not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      },
      "put": {
        "operationId": "updateAutomationDraft",
        "tags": [
          "Automations"
        ],
        "summary": "Editar el BORRADOR de una automatización",
        "description": "Requiere automations:write o *. Reemplaza solo el borrador: lo publicado, el estado, la versión y los disparadores reales NO cambian; los cambios quedan pendientes hasta que un humano los publique en la web. Parcial: lo que no se envía se toma del borrador actual o, si no hay, de lo publicado. Los disparadores del borrador quedan inactivos. Un valor ***REDACTED*** (de GET) se reemplaza por el valor guardado en el mismo stepId y la misma ruta; sin original → 400. reEntry solo se acepta si la automatización nunca se publicó (en una publicada se aplica de inmediato: se cambia en la web). El nombre del listado solo cambia en automatizaciones que nunca se publicaron. Id ajeno → 404.",
        "x-required-permissions": [
          "automations:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AutomationDraftUpdate"
              },
              "examples": {
                "renombrar": {
                  "value": {
                    "name": "Seguimiento de cotización v2"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Borrador guardado; devuelve la automatización completa.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutomationResult"
                },
                "examples": {
                  "found": {
                    "$ref": "#/paths/~1automations~1{id}/get/responses/200/content/application~1json/examples/found"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1automations/post/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1automations~1{id}/get/responses/404"
          },
          "409": {
            "$ref": "#/paths/~1automations/post/responses/409"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/automations/{id}/runs": {
      "parameters": [
        {
          "$ref": "#/paths/~1automations~1{id}/parameters/0"
        }
      ],
      "get": {
        "operationId": "listAutomationRuns",
        "tags": [
          "Automations"
        ],
        "summary": "Historial de ejecuciones de una automatización",
        "description": "Requiere automations:read o *. Paginado, más recientes primero. De cada paso solo tipo, estado, duración, intentos, rama y error: nunca input/output ni el evento que la disparó (traen datos del cliente y secretos). Las ejecuciones caducan a los ~30 días.",
        "x-required-permissions": [
          "automations:read"
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "running",
                "success",
                "partial",
                "failed",
                "waiting",
                "cancelled"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ejecuciones y paginación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutomationRunListResult"
                },
                "examples": {
                  "runs": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "66f100000000000000000a01",
                          "automationId": "66f1000000000000000000e1",
                          "triggerId": "66f1000000000000000000f1",
                          "conversationId": null,
                          "contactId": "66f100000000000000000c01",
                          "status": "failed",
                          "startedAt": "2026-09-24T21:10:00.000Z",
                          "finishedAt": "2026-09-24T21:10:02.000Z",
                          "error": "Paso s_webhook: HTTP 500",
                          "replayOf": null,
                          "replayCount": 0,
                          "steps": [
                            {
                              "stepId": "s_webhook",
                              "type": "http_request",
                              "name": "Avisar al ERP",
                              "status": "failed",
                              "ms": 1830,
                              "attempts": 1,
                              "path": null,
                              "error": "HTTP 500"
                            }
                          ],
                          "createdAt": "2026-09-24T21:10:00.000Z"
                        }
                      ],
                      "pagination": {
                        "page": 1,
                        "limit": 100,
                        "total": 1,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/paths/~1automations/get/responses/400"
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "$ref": "#/paths/~1automations~1{id}/get/responses/404"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/automation-triggers": {
      "get": {
        "operationId": "listAutomationTriggers",
        "tags": [
          "Automation triggers"
        ],
        "summary": "Listar disparadores de automatizaciones",
        "description": "Requiere automations:read o *. Solo lectura: los disparadores reales (publicados) de la organización, más recientes primero, con su automatización. Crear o activar un disparador haría correr automatizaciones reales, por eso no hay escritura: los de un borrador se editan con PUT /automations/{id} y se vuelven reales al publicar en la web.",
        "x-required-permissions": [
          "automations:read"
        ],
        "parameters": [
          {
            "name": "automationId",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^[a-fA-F0-9]{24}$"
            }
          },
          {
            "name": "type",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "inbound_message",
                "contact_created",
                "company_created",
                "deal_status_changed",
                "conversation_stage_changed",
                "stale_conversation",
                "scheduled",
                "calendar_event_upcoming",
                "quotation_created",
                "quotation_viewed",
                "quotation_status_changed",
                "invoice_created",
                "invoice_overdue",
                "form_submitted",
                "deal_stale",
                "repurchase_due"
              ]
            }
          },
          {
            "name": "isActive",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Disparadores y paginación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutomationTriggerListResult"
                },
                "examples": {
                  "list": {
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": "66f1000000000000000000f1",
                          "automationId": "66f1000000000000000000e1",
                          "automation": {
                            "id": "66f1000000000000000000e1",
                            "name": "Seguimiento de cotización",
                            "status": "published"
                          },
                          "name": "Cotización creada",
                          "description": null,
                          "type": "quotation_created",
                          "isActive": true,
                          "filters": {
                            "minTotal": 1000000
                          },
                          "schedule": null,
                          "stats": {
                            "totalRuns": 42,
                            "successCount": 40,
                            "failureCount": 2,
                            "lastRunAt": "2026-09-24T21:10:00.000Z",
                            "lastStatus": "success",
                            "lastError": null
                          },
                          "createdAt": "2026-09-01T12:00:00.000Z",
                          "updatedAt": "2026-09-20T15:00:00.000Z"
                        }
                      ],
                      "pagination": {
                        "page": 1,
                        "limit": 100,
                        "total": 1,
                        "pages": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Filtro inválido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutomationTriggerError"
                },
                "example": {
                  "success": false,
                  "error": "type inválido"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/automation-triggers/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB del disparador.",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "66f1000000000000000000f1"
        }
      ],
      "get": {
        "operationId": "getAutomationTrigger",
        "tags": [
          "Automation triggers"
        ],
        "summary": "Obtener un disparador",
        "description": "Requiere automations:read o *. Id de otra organización o inexistente → 404.",
        "x-required-permissions": [
          "automations:read"
        ],
        "responses": {
          "200": {
            "description": "Disparador.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutomationTriggerResult"
                },
                "examples": {
                  "found": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "66f1000000000000000000f1",
                        "automationId": "66f1000000000000000000e1",
                        "automation": {
                          "id": "66f1000000000000000000e1",
                          "name": "Seguimiento de cotización",
                          "status": "published"
                        },
                        "name": "Cotización creada",
                        "description": null,
                        "type": "quotation_created",
                        "isActive": true,
                        "filters": {
                          "minTotal": 1000000
                        },
                        "schedule": null,
                        "stats": {
                          "totalRuns": 42,
                          "successCount": 40,
                          "failureCount": 2,
                          "lastRunAt": "2026-09-24T21:10:00.000Z",
                          "lastStatus": "success",
                          "lastError": null
                        },
                        "createdAt": "2026-09-01T12:00:00.000Z",
                        "updatedAt": "2026-09-20T15:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "No existe o es de otra organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutomationTriggerError"
                },
                "example": {
                  "success": false,
                  "error": "Automation trigger not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/import/contacts": {
      "post": {
        "operationId": "importContactsRows",
        "tags": [
          "Import"
        ],
        "summary": "Importar contactos por filas JSON (con dryRun)",
        "description": "Requiere contacts:write o *. Mismo motor que el importador de Excel de la app: dedupe por email (sin mayúsculas) → mobile → phone (últimos 10 dígitos, con o sin indicativo), dedupe dentro del lote (filas repetidas se fusionan), una celda vacía nunca borra un dato existente, `owner` = correo del asesor dueño (si no cruza, queda a nombre del usuario del token), `company` enlaza la empresa por nombre y `createMissingCompanies` crea las que falten. Aplica el límite de contactos del plan (los que no caben van a skippedByPlan). Una clave que no es propiedad de contacto de la organización rechaza SOLO esa fila (rejectedRows). No emite eventos de dominio ni dispara automatizaciones. Máximo 500 filas por llamada (413). Con `dryRun: true` devuelve el resumen sin escribir nada; sin dryRun deja un registro en el historial con source \"api\". Comparte con la app el límite de 30 importaciones (y 30 simulaciones) por organización y hora (429). Ciudades: cityCode (código DANE) o city; con locationMode=strict la fila con ciudad no resuelta se rechaza con candidatos (ver ImportContactsRequest). Identificación (Épica Z): idType/idNumber/dv se normalizan siempre en modo lenient (código DIAN, número sin puntos, DV del NIT en dv); lo que no se reconoce queda como llegó y sale agrupado en warnings, nunca rechaza la fila.",
        "x-required-permissions": [
          "contacts:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ImportContactsRequest"
              },
              "examples": {
                "dryRun": {
                  "value": {
                    "dryRun": true,
                    "rows": [
                      {
                        "firstName": "Ana",
                        "email": "ana@acme.co",
                        "mobile": "3001112233",
                        "company": "Acme SAS"
                      },
                      {
                        "firstName": "Pedro",
                        "email": "pedro@beta.co",
                        "owner": "asesor@miempresa.co"
                      },
                      {
                        "firstName": "Luis",
                        "cargo": "Gerente"
                      }
                    ]
                  }
                },
                "import": {
                  "value": {
                    "createMissingCompanies": true,
                    "rows": [
                      {
                        "firstName": "Ana",
                        "email": "ana@acme.co",
                        "mobile": "3001112233",
                        "company": "Acme SAS"
                      }
                    ]
                  }
                },
                "strictCity": {
                  "summary": "Ciudades en modo estricto (una fila con Rionegro sin departamento se rechaza).",
                  "value": {
                    "locationMode": "strict",
                    "rows": [
                      {
                        "firstName": "Ana",
                        "email": "ana@acme.co",
                        "cityCode": "05001"
                      },
                      {
                        "firstName": "Luis",
                        "email": "luis@acme.co",
                        "city": "Rionegro"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Simulación (dryRun true). No escribió nada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportContactsDryRunResult"
                },
                "examples": {
                  "dryRun": {
                    "value": {
                      "success": true,
                      "data": {
                        "dryRun": true,
                        "totalRows": 3,
                        "totalRowsWithData": 3,
                        "wouldCreate": 1,
                        "wouldUpdate": 1,
                        "wouldSkipByPlan": 0,
                        "skippedEmptyRows": 0,
                        "mergedDuplicateRows": 0,
                        "rejectedCount": 1,
                        "rejectedRows": [
                          {
                            "rowNumber": 3,
                            "reason": "Clave(s) desconocida(s): cargo (no son propiedades de contacto de la organización; consulta GET /api/contact-properties)",
                            "values": {
                              "firstName": "Luis",
                              "cargo": "Gerente"
                            }
                          }
                        ],
                        "preview": [
                          {
                            "rowNumber": 1,
                            "properties": {
                              "firstName": "Ana",
                              "email": "ana@acme.co",
                              "mobile": "573001112233",
                              "lastName": "",
                              "company": "Acme SAS"
                            },
                            "updatesExisting": true,
                            "existingContactId": "66f10000000000000000c001",
                            "companyLinked": "Acme SAS"
                          },
                          {
                            "rowNumber": 2,
                            "properties": {
                              "firstName": "Pedro",
                              "email": "pedro@beta.co",
                              "lastName": "",
                              "mobile": ""
                            },
                            "updatesExisting": false,
                            "existingContactId": null,
                            "companyLinked": null
                          }
                        ],
                        "companies": {
                          "wouldLink": 1,
                          "wouldCreate": 0,
                          "skippedByPlan": 0,
                          "missing": 0
                        },
                        "planInfo": {
                          "limit": 1000,
                          "used": 240,
                          "remaining": 760,
                          "planName": "pro"
                        },
                        "warnings": []
                      }
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Importación aplicada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportContactsResult"
                },
                "examples": {
                  "imported": {
                    "value": {
                      "success": true,
                      "data": {
                        "dryRun": false,
                        "importId": "66f10000000000000000d001",
                        "totalRows": 3,
                        "totalRowsWithData": 3,
                        "importedCount": 1,
                        "updatedCount": 1,
                        "skippedByPlan": 0,
                        "skippedEmptyRows": 0,
                        "mergedDuplicateRows": 0,
                        "rejectedCount": 1,
                        "rejectedRows": [
                          {
                            "rowNumber": 3,
                            "reason": "Clave(s) desconocida(s): cargo (no son propiedades de contacto de la organización; consulta GET /api/contact-properties)",
                            "values": {
                              "firstName": "Luis",
                              "cargo": "Gerente"
                            }
                          }
                        ],
                        "companiesLinked": 1,
                        "companiesCreated": 0,
                        "companiesSkippedByPlan": 0,
                        "warnings": []
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Payload inválido (rows vacío o no arreglo, flags no booleanos) o la organización no tiene propiedades de contacto configuradas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportError"
                },
                "example": {
                  "success": false,
                  "error": "rows debe ser un arreglo con al menos una fila"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "413": {
            "description": "Más de 500 filas en una llamada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportError"
                },
                "example": {
                  "success": false,
                  "error": "Máximo 500 filas por llamada (llegaron 800); divide el lote",
                  "maxRows": 500
                }
              }
            }
          },
          "429": {
            "description": "Límite de importaciones por hora de la organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "example": {
                  "message": "Tu organización alcanzó el máximo de 30 importaciones por hora. Podrás intentar de nuevo en 12 minuto(s).",
                  "code": "RATE_LIMIT_EXCEEDED",
                  "retryAfterMinutes": 12
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/import/history": {
      "get": {
        "operationId": "listImportHistory",
        "tags": [
          "Import"
        ],
        "summary": "Historial de importaciones",
        "description": "Requiere contacts:read o *. Importaciones de la organización (web y API, de contactos, empresas, negocios y compras), más reciente primero, sin las filas rechazadas (van en el detalle).",
        "x-required-permissions": [
          "contacts:read"
        ],
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "contacts",
                "companies",
                "deals",
                "purchases"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Importaciones y paginación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportListResult"
                }
              }
            }
          },
          "400": {
            "description": "type inválido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportError"
                },
                "example": {
                  "success": false,
                  "error": "type debe ser uno de: contacts, companies, deals, purchases"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/import/history/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identificador MongoDB de la importación (importId).",
          "schema": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "example": "66f10000000000000000d001"
        }
      ],
      "get": {
        "operationId": "getImportHistoryById",
        "tags": [
          "Import"
        ],
        "summary": "Detalle de una importación con sus filas rechazadas",
        "description": "Requiere contacts:read o *. Un id que no es ObjectId o de otra organización responde 404.",
        "x-required-permissions": [
          "contacts:read"
        ],
        "responses": {
          "200": {
            "description": "Importación encontrada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportDetailResult"
                },
                "examples": {
                  "detail": {
                    "value": {
                      "success": true,
                      "data": {
                        "id": "66f10000000000000000d001",
                        "type": "contacts",
                        "source": "api",
                        "fileName": "api:zapier",
                        "status": "completed",
                        "userId": "66f10000000000000000e001",
                        "totalRows": 3,
                        "importedCount": 1,
                        "updatedCount": 1,
                        "skippedByPlan": 0,
                        "skippedEmptyRows": 0,
                        "rejectedCount": 1,
                        "companiesLinked": 1,
                        "companiesCreated": 0,
                        "warnings": [],
                        "errorMessage": null,
                        "rolledBackAt": null,
                        "rolledBackBy": null,
                        "createdAt": "2026-09-25T14:00:00.000Z",
                        "updatedAt": "2026-09-25T14:00:01.000Z",
                        "rejectedRows": [
                          {
                            "rowNumber": 3,
                            "reason": "Clave(s) desconocida(s): cargo",
                            "values": {
                              "firstName": "Luis",
                              "cargo": "Gerente"
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "$ref": "#/paths/~1me/get/responses/403"
          },
          "404": {
            "description": "Importación inexistente, id inválido o de otra organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportError"
                },
                "example": {
                  "success": false,
                  "error": "Import not found"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    },
    "/import/history/{id}/rollback": {
      "parameters": [
        {
          "$ref": "#/paths/~1import~1history~1{id}/parameters/0"
        }
      ],
      "post": {
        "operationId": "rollbackImport",
        "tags": [
          "Import"
        ],
        "summary": "Revertir una importación",
        "description": "Requiere contacts:write o *. Borra lo que la importación creó y restaura lo que actualizó (contactos, empresas o negocios; en contactos también borra las empresas creadas por createMissingCompanies). Solo el propietario de la organización o el autor (el usuario del token) puede revertir (403 ROLLBACK_FORBIDDEN). Una importación ya revertida responde 409; una de compras, 400.",
        "x-required-permissions": [
          "contacts:write"
        ],
        "responses": {
          "200": {
            "description": "Importación revertida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportRollbackResult"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "66f10000000000000000d001",
                    "deletedCount": 1,
                    "restoredCount": 1,
                    "deletedCompanies": 0
                  }
                }
              }
            }
          },
          "400": {
            "description": "Tipo de importación sin reversión (compras).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportError"
                },
                "example": {
                  "success": false,
                  "error": "La reversión solo está soportada para importaciones de contactos, empresas y negocios"
                }
              }
            }
          },
          "401": {
            "$ref": "#/paths/~1me/get/responses/401"
          },
          "403": {
            "description": "Permiso del token insuficiente, o el usuario no es propietario ni autor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportError"
                },
                "example": {
                  "success": false,
                  "error": "Solo el propietario de la organización o quien hizo la importación puede revertirla",
                  "code": "ROLLBACK_FORBIDDEN"
                }
              }
            }
          },
          "404": {
            "$ref": "#/paths/~1import~1history~1{id}/get/responses/404"
          },
          "409": {
            "description": "Ya revertida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportError"
                },
                "example": {
                  "success": false,
                  "error": "Esta importación ya fue revertida",
                  "code": "ALREADY_ROLLED_BACK"
                }
              }
            }
          },
          "500": {
            "$ref": "#/paths/~1me/get/responses/500"
          }
        }
      }
    }
  },
  "webhooks": {
    "conversationStageEntered": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "conversation.stage.entered — una conversación entró a una etapa con webhook",
        "description": "Se configura por etapa en Ajustes → Conversaciones → columna del pipeline\n(\"Webhook\" + URL http/https). FusionCRM hace un POST fire-and-forget con\ntimeout de 10 s, **sin reintentos y sin firma**: valide el origen por la URL\nsecreta o por `X-Fusioncrm-Organization`. Se dispara siempre que la\nconversación ENTRA a la etapa, venga de la app, de `PUT /conversations/{id}/stage`,\ndel MCP o de una automatización. Mover a la misma etapa no dispara.\n",
        "parameters": [
          {
            "name": "X-Fusioncrm-Event",
            "in": "header",
            "schema": {
              "type": "string",
              "const": "conversation.stage.entered"
            }
          },
          {
            "name": "X-Fusioncrm-Organization",
            "in": "header",
            "description": "Id de la organización que envía.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConversationStageEnteredPayload"
              },
              "examples": {
                "stageEntered": {
                  "$ref": "#/components/examples/ConversationStageEntered"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Cualquier 2xx se considera entregado. Otros códigos solo se registran en el log del servidor."
          }
        }
      }
    },
    "automationHttpRequest": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Petición HTTP de una automatización (deal_status_changed, quotation_created, invoice_created, form_submitted…)",
        "description": "Los eventos de negocio no tienen webhook fijo: salen al exterior por la acción\n**\"Petición HTTP\"** (`http_request`) de una automatización (Automatizaciones →\nnueva → disparador → acción Petición HTTP). Usted define URL, método, headers y\ncuerpo con variables `{{trigger.*}}`, `{{contact.*}}` y `{{steps.*}}`; el\ncuerpo documentado aquí es la convención recomendada (reenviar `trigger`\ncompleto) y los schemas describen exactamente qué claves trae `trigger` en\ncada disparador. Timeout 8 s, sin reintentos por defecto; las URLs pasan por\nel guard SSRF (no se permiten redes privadas). `eventId` es estable por evento\ny sirve para deduplicar en el receptor.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AutomationHttpRequestBody"
              },
              "examples": {
                "dealStatusChanged": {
                  "$ref": "#/components/examples/DealStatusChanged"
                },
                "quotationCreated": {
                  "$ref": "#/components/examples/QuotationCreated"
                },
                "invoiceCreated": {
                  "$ref": "#/components/examples/InvoiceCreated"
                },
                "formSubmitted": {
                  "$ref": "#/components/examples/FormSubmitted"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "La acción se marca `ok`; un 5xx la marca reintentable (según la política de la acción) y un 4xx la detiene."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiTokenBearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "Token de API generado en Ajustes → API Tokens (no JWT de sesión)."
      },
      "ApiTokenHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "El mismo token de API, sin el prefijo Bearer."
      }
    },
    "schemas": {
      "Pagination": {
        "type": "object",
        "required": [
          "total",
          "page",
          "limit",
          "pages"
        ],
        "properties": {
          "total": {
            "type": "integer",
            "minimum": 0
          },
          "page": {
            "type": "integer",
            "minimum": 1
          },
          "limit": {
            "type": "integer",
            "minimum": 1
          },
          "pages": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "ApiError": {
        "description": "Los middlewares devuelven message/code; algunos controllers usan\nsuccess:false/error. Se conservan ambos contratos existentes.\n",
        "type": "object",
        "anyOf": [
          {
            "required": [
              "message"
            ]
          },
          {
            "required": [
              "error"
            ]
          }
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "message": {
            "type": "string"
          },
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "details": {},
          "requiredPermission": {
            "type": "string"
          },
          "availablePermissions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Contact": {
        "type": "object",
        "description": "Representación aplanada del contacto. Las properties personalizadas se exponen como claves adicionales. Para claves repetidas gana la primera marcada isPrimary; si no existe marca, la primera. No todos los contactos tienen email o mobile.",
        "required": [
          "id",
          "firstName",
          "lastName",
          "fullName",
          "company",
          "companyId",
          "leadScore"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Identificador del contacto."
          },
          "firstName": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "fullName": {
            "type": "string"
          },
          "company": {
            "type": "string",
            "description": "Nombre de empresa como texto."
          },
          "companyId": {
            "type": [
              "string",
              "null"
            ],
            "description": "ID de la empresa vinculada, o null."
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "mobile": {
            "type": [
              "string",
              "null"
            ]
          },
          "phone": {
            "type": [
              "string",
              "null"
            ]
          },
          "city": {
            "type": "string",
            "description": "Ciudad como texto. Puede estar \"sin confirmar\" (cityCode vacío) si entró por un camino no estricto."
          },
          "cityCode": {
            "type": "string",
            "description": "Código DANE del municipio (5 dígitos, ej. 05001 = Medellín). Fuente de verdad de la ubicación (Épica Y); city, state y stateCode se derivan de él al guardar. Vacío = ciudad sin confirmar; ausente = contacto sin ubicación."
          },
          "state": {
            "type": "string",
            "description": "Departamento (texto)",
            "derivado de cityCode.": null
          },
          "stateCode": {
            "type": "string",
            "description": "Código DANE del departamento (2 dígitos",
            "ej. 05 = Antioquia)": null,
            "derivado de cityCode.": null
          },
          "leadScore": {
            "type": "number"
          },
          "attribution": {
            "description": "Atribución first-touch del contacto (se fija una sola vez, cuando entra por un formulario, widget o landing del CRM). null si no tiene (creado a mano, por WhatsApp o por importación). Campo aditivo desde el 14-sep-2026.",
            "oneOf": [
              {
                "type": "object",
                "description": "Datos de primer contacto (first-touch). Todos los campos salvo firstTouchAt pueden ser null cuando la fuente no los trajo. Las URLs (referrer, landingPage) vienen tal cual las capturó el formulario; los UTM sin normalizar (source del contacto sí se normaliza al catálogo, attribution.utmSource no).",
                "required": [
                  "firstTouchAt"
                ],
                "properties": {
                  "firstTouchAt": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Fecha del primer contacto en ISO 8601 (UTC)."
                  },
                  "channel": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Canal de entrada; hoy siempre 'web'."
                  },
                  "utmSource": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "utmMedium": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "utmCampaign": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "utmTerm": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "utmContent": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "referrer": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "document.referrer de la página donde se envió el form."
                  },
                  "landingPage": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "URL de la página donde se envió el form (page_url)."
                  },
                  "gclid": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Click ID de Google Ads."
                  },
                  "fbclid": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Click ID de Meta Ads."
                  },
                  "formId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "ID del formulario del CRM que lo captó."
                  },
                  "landingPageId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "ID de la landing page del CRM (/p/:slug)",
                    "si aplica.": null
                  },
                  "landingPageName": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "additionalProperties": false
              },
              {
                "type": "null"
              }
            ]
          },
          "tags": {
            "type": "array",
            "description": "Etiquetas del contacto (Contact.tags). Siempre presente; [] si no tiene.",
            "items": {
              "type": "string"
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "additionalProperties": true
      },
      "RawContact": {
        "type": "object",
        "description": "Documento persistido que devuelve únicamente la rama 200 del POST de creación. Conserva properties como array y puede incluir otros campos del documento.",
        "required": [
          "_id",
          "organizationId",
          "properties"
        ],
        "properties": {
          "_id": {
            "type": "string"
          },
          "organizationId": {
            "type": "string"
          },
          "companyId": {
            "type": [
              "string",
              "null"
            ]
          },
          "properties": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "key"
              ],
              "properties": {
                "_id": {
                  "type": "string"
                },
                "key": {
                  "type": "string"
                },
                "value": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "isVisible": {
                  "type": "boolean"
                },
                "label": {
                  "type": "string"
                },
                "isPrimary": {
                  "type": "boolean"
                }
              },
              "additionalProperties": true
            }
          },
          "leadScore": {
            "type": "number"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "additionalProperties": true
      },
      "ContactCreate": {
        "type": "object",
        "description": "Requiere email, mobile, whatsapp_phone o linkedConversationId con valor no vacío. whatsapp_phone precede a mobile; first_name/last_name preceden a firstName/lastName. Sin nombre, al CREAR se completa firstName con el nombre del perfil de la conversación (la enlazada o la del número), luego el número y luego el handle. Mínimo del sistema (nombre + medio) y propiedades required de la org: por ahora solo se registran en el log del servidor, nunca rechazan (Épica Z, Z-D19). companyName busca o crea una empresa de la organización; company solo escribe texto. companyId no vacío tiene precedencia sobre companyName. Los campos top-level desconocidos se ignoran. No acepta properties[]. Todos los valores de texto se guardan sin espacios exteriores (\"Juan \" → \"Juan\").",
        "anyOf": [
          {
            "required": [
              "email"
            ]
          },
          {
            "required": [
              "mobile"
            ]
          },
          {
            "required": [
              "whatsapp_phone"
            ]
          },
          {
            "required": [
              "linkedConversationId"
            ]
          }
        ],
        "properties": {
          "linkedConversationId": {
            "type": "string",
            "pattern": "^[0-9a-fA-F]{24}$",
            "description": "ID de una conversación de la organización (lead de WhatsApp, Instagram o Messenger). Cuenta como medio de contacto: basta con firstName + linkedConversationId. Se valida antes de escribir nada: de otra organización o inexistente → 404 CONVERSATION_NOT_FOUND; con otro contacto ya enlazado → 409 CONVERSATION_ALREADY_LINKED (trae contactId). Al crear (o al actualizar el contacto existente) enlaza la conversación al contacto. No se guarda como property."
          },
          "firstName": {
            "type": "string"
          },
          "first_name": {
            "type": "string",
            "description": "Alias con precedencia sobre firstName."
          },
          "lastName": {
            "type": "string"
          },
          "last_name": {
            "type": "string",
            "description": "Alias con precedencia sobre lastName."
          },
          "email": {
            "type": "string",
            "minLength": 1,
            "description": "Se eliminan espacios exteriores; dedupe por igualdad exacta."
          },
          "mobile": {
            "type": "string",
            "minLength": 1,
            "description": "Se normaliza a dígitos."
          },
          "whatsapp_phone": {
            "type": "string",
            "minLength": 1,
            "description": "Alias de mobile con precedencia; se normaliza a dígitos."
          },
          "phone": {
            "type": "string",
            "description": "Se normaliza a dígitos; no basta por sí solo para crear."
          },
          "companyId": {
            "type": [
              "string",
              "null"
            ],
            "description": "ID de una empresa existente de la organización; null o vacío no desvinculan en POST."
          },
          "companyName": {
            "type": "string",
            "description": "Busca o crea una empresa por nombre y la vincula."
          },
          "company": {
            "type": "string",
            "description": "Texto de empresa; no crea un vínculo por sí solo."
          },
          "city": {
            "type": "string",
            "description": "Ciudad (Colombia). Modo ESTRICTO: debe coincidir sin ambigüedad con un municipio del catálogo DANE (nombre o alias; se ignoran tildes y mayúsculas). Ambigua (Rionegro), mal escrita (Medelin) o desconocida → 422 CITY_UNRESOLVED con candidates. Con country distinto de Colombia se guarda como texto libre. Si cityCode viene, city se deriva de él."
          },
          "cityCode": {
            "type": "string",
            "pattern": "^\\d{5}$",
            "description": "Código DANE del municipio (5 dígitos, ej. 05001 = Medellín, 76001 = Cali). Tiene prioridad sobre city y deriva city, state y stateCode. Inexistente → 400 INVALID_CITY_CODE. Buscar códigos en GET /catalogs/municipalities?q=."
          },
          "state": {
            "type": "string",
            "description": "Departamento (texto). Solo sirve como pista para desambiguar city (Rionegro + Santander → 68615); se sobreescribe con el derivado de cityCode."
          },
          "stateCode": {
            "type": "string",
            "description": "Código DANE del departamento (2 dígitos). Solo lo escribe el servidor (derivado de cityCode); el valor enviado se ignora."
          },
          "country": {
            "type": "string"
          },
          "position": {
            "type": "string"
          },
          "companyType": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "idNumber": {
            "type": "string",
            "description": "Número de documento; precede al alias nit. Se guarda normalizado: sin puntos ni espacios; para NIT (31) el DV escrito con guion o pegado (10 dígitos) se mueve a dv. Tipo colombiano → solo dígitos (1-15); extranjero (41/42/50) → alfanumérico en mayúsculas. Inválido para el tipo → 400 INVALID_ID_NUMBER."
          },
          "nit": {
            "type": "string"
          },
          "idType": {
            "description": "Tipo de documento (Épica Z, modo ESTRICTO). Se guarda como código DIAN: 11 RC, 12 TI, 13 cédula, 21 TE, 22 CE, 31 NIT, 41 pasaporte, 42 doc. extranjero, 47 PEP, 48 PPT, 50 NIT de otro país, 91 NUIP. También acepta un texto inequívoco (CC, NIT, cédula, pasaporte…) o el valor de una opción del select idType de la organización. Ambiguo (\"1\", \"2\", \"11\", \"12\") o desconocido → 422 ID_TYPE_UNRESOLVED con candidates (códigos).",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "11",
                  "12",
                  "13",
                  "21",
                  "22",
                  "31",
                  "41",
                  "42",
                  "47",
                  "48",
                  "50",
                  "91"
                ]
              },
              {
                "type": "string"
              }
            ]
          },
          "dv": {
            "type": "string",
            "pattern": "^\\d$",
            "description": "Dígito de verificación",
            "solo con idType 31 (con otro tipo queda vacío). Si falta se calcula (mod-11 DIAN); uno distinto se guarda y queda en el log.": null
          },
          "source": {
            "type": "string"
          },
          "referral": {
            "description": "Texto u objeto de atribución; los objetos se serializan como JSON en una property.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "object",
                "additionalProperties": true
              }
            ]
          },
          "notes": {
            "type": "string",
            "description": "Precede a notas y comments."
          },
          "notas": {
            "type": "string"
          },
          "comments": {
            "type": "string"
          },
          "custom_fields": {
            "type": "object",
            "description": "Compatibilidad legacy; Ciudad y Nit preceden a city e idNumber/nit. cityCode también se acepta aquí.",
            "properties": {
              "Empresa": {
                "type": "string",
                "description": "Fallback de texto tras company y companyName."
              },
              "Ciudad": {
                "type": "string",
                "description": "Alias de city; misma validación estricta."
              },
              "cityCode": {
                "type": "string",
                "pattern": "^\\d{5}$",
                "description": "Código DANE del municipio; precede a Ciudad."
              },
              "Nit": {
                "type": "string"
              }
            },
            "additionalProperties": true
          }
        },
        "additionalProperties": true
      },
      "ContactUpdate": {
        "type": "object",
        "description": "Actualización parcial por campos planos; no exige email ni teléfono. Los campos desconocidos también se guardan como properties (usar valores de texto). phone/mobile se normalizan a dígitos. companyId es un campo raíz: null o \"\" desvincula, un ID válido vincula y sincroniza company con el nombre de la empresa. companyName solo cambia texto en PUT; no busca ni crea empresa. El merge conserva la property primaria (o primera) y colapsa claves duplicadas. No usar properties[] ni custom_fields como en otros endpoints. Todos los valores de texto se guardan sin espacios exteriores (\"Juan \" → \"Juan\").",
        "properties": {
          "firstName": {
            "type": "string"
          },
          "first_name": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "last_name": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "mobile": {
            "type": "string"
          },
          "whatsapp_phone": {
            "type": "string"
          },
          "phone": {
            "type": "string"
          },
          "companyId": {
            "type": [
              "string",
              "null"
            ]
          },
          "company": {
            "type": "string"
          },
          "companyName": {
            "type": "string"
          },
          "position": {
            "type": "string"
          },
          "idType": {
            "description": "Tipo de documento (Épica Z, modo ESTRICTO como en POST; sin idType/idNumber/dv en el body no se toca la identificación). Se guarda como código DIAN: 11 RC, 12 TI, 13 cédula, 21 TE, 22 CE, 31 NIT, 41 pasaporte, 42 doc. extranjero, 47 PEP, 48 PPT, 50 NIT de otro país, 91 NUIP. También acepta un texto inequívoco (CC, NIT, cédula, pasaporte…) o el valor de una opción del select idType de la organización. Ambiguo (\"1\", \"2\", \"11\", \"12\") o desconocido → 422 ID_TYPE_UNRESOLVED con candidates (códigos).",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "11",
                  "12",
                  "13",
                  "21",
                  "22",
                  "31",
                  "41",
                  "42",
                  "47",
                  "48",
                  "50",
                  "91"
                ]
              },
              {
                "type": "string"
              }
            ]
          },
          "idNumber": {
            "type": "string",
            "description": "Número de documento; precede al alias nit. Se guarda normalizado: sin puntos ni espacios; para NIT (31) el DV escrito con guion o pegado (10 dígitos) se mueve a dv. Tipo colombiano → solo dígitos (1-15); extranjero (41/42/50) → alfanumérico en mayúsculas. Inválido para el tipo → 400 INVALID_ID_NUMBER."
          },
          "nit": {
            "type": "string",
            "description": "Alias de idNumber."
          },
          "dv": {
            "type": "string",
            "pattern": "^\\d$",
            "description": "Dígito de verificación",
            "solo con idType 31 (con otro tipo queda vacío). Si falta se calcula (mod-11 DIAN); uno distinto se guarda y queda en el log.": null
          },
          "city": {
            "type": "string",
            "description": "Ciudad (Colombia), modo ESTRICTO como en POST: ambigua, mal escrita o desconocida → 422 CITY_UNRESOLVED con candidates. El state ya guardado en el contacto sirve de pista para desambiguar. Sin city/cityCode/state en el body no se toca la ubicación. Un city vacío (\"\") limpia también cityCode, state y stateCode sin error."
          },
          "cityCode": {
            "type": "string",
            "pattern": "^\\d{5}$",
            "description": "Código DANE del municipio (5 dígitos); deriva city, state y stateCode. Inexistente → 400 INVALID_CITY_CODE."
          },
          "state": {
            "type": "string",
            "description": "Departamento (texto); pista para desambiguar city."
          },
          "stateCode": {
            "type": "string",
            "description": "Código DANE del departamento; solo lo escribe el servidor (derivado de cityCode). El valor enviado se ignora y no dispara la validación de ciudad."
          },
          "country": {
            "type": "string"
          },
          "notes": {
            "type": "string"
          },
          "notas": {
            "type": "string"
          },
          "comments": {
            "type": "string"
          }
        },
        "additionalProperties": true
      },
      "ContactResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/Contact"
          }
        }
      },
      "ContactCreatedResult": {
        "type": "object",
        "required": [
          "success",
          "message",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "message": {
            "type": "string",
            "const": "Contact created successfully"
          },
          "data": {
            "$ref": "#/components/schemas/Contact"
          }
        }
      },
      "ContactUpsertResult": {
        "type": "object",
        "required": [
          "message",
          "contact"
        ],
        "description": "El contacto existente fue modificado. No incluye success ni duplicate.",
        "properties": {
          "message": {
            "type": "string",
            "const": "Contact updated successfully"
          },
          "contact": {
            "$ref": "#/components/schemas/RawContact"
          }
        }
      },
      "ContactListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Contact"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "ContactSearchResult": {
        "type": "object",
        "required": [
          "success",
          "contact"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "contact": {
            "type": "object",
            "description": "Formato específico de búsqueda por teléfono, distinto de data en GET por ID. properties es un objeto aplanado; en este endpoint legacy gana la última property repetida y no se aplica isPrimary.",
            "required": [
              "id",
              "organizationId",
              "properties",
              "firstName",
              "lastName",
              "fullName",
              "email",
              "mobile",
              "phone",
              "company",
              "position",
              "leadScore"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "organizationId": {
                "type": "string"
              },
              "properties": {
                "type": "object",
                "additionalProperties": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              },
              "firstName": {
                "type": "string"
              },
              "lastName": {
                "type": "string"
              },
              "fullName": {
                "type": "string"
              },
              "email": {
                "type": "string"
              },
              "mobile": {
                "type": "string"
              },
              "phone": {
                "type": "string"
              },
              "company": {
                "type": "string"
              },
              "position": {
                "type": "string"
              },
              "leadScore": {
                "type": "number"
              },
              "attribution": {
                "description": "Igual que en Contact; null si el contacto no tiene atribución.",
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/Contact/properties/attribution/oneOf/0"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "tags": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "createdAt": {
                "type": "string",
                "format": "date-time"
              },
              "updatedAt": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        }
      },
      "ContactDeleteResult": {
        "type": "object",
        "required": [
          "success",
          "message",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "message": {
            "type": "string",
            "const": "Contact deleted successfully"
          },
          "data": {
            "type": "object",
            "required": [
              "id"
            ],
            "properties": {
              "id": {
                "type": "string"
              }
            }
          }
        }
      },
      "AdvancedFilterRequest": {
        "type": "object",
        "required": [
          "Contacts"
        ],
        "properties": {
          "Contacts": {
            "type": "array",
            "description": "Filtros AND sobre properties; puede enviarse []. is_empty requiere que la property exista con valor vacío o null (no equivale a missing del listado). contains/not_contains interpretan el valor como expresión regular.",
            "items": {
              "type": "object",
              "required": [
                "field",
                "condition"
              ],
              "properties": {
                "field": {
                  "type": "string",
                  "examples": [
                    "city"
                  ]
                },
                "condition": {
                  "type": "string",
                  "enum": [
                    "equals",
                    "not_equals",
                    "contains",
                    "not_contains",
                    "is_empty",
                    "is_not_empty"
                  ]
                },
                "value": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          },
          "Deals": {
            "type": "array",
            "description": "Los filtros regulares determinan hasDeals; por sí solos no excluyen contactos. Añadir field=hasDeals y value=true para conservar solo quienes tengan negocios coincidentes. Se usa associatedContactId (no associatedContactIds). last_deal_before/after filtra por el máximo createdAt de TODOS los negocios asociados, independientemente de los filtros regulares.",
            "items": {
              "type": "object",
              "required": [
                "field"
              ],
              "properties": {
                "field": {
                  "type": "string",
                  "examples": [
                    "amount",
                    "hasDeals",
                    "createdAt"
                  ]
                },
                "condition": {
                  "type": "string",
                  "description": "Se ignora para field=hasDeals; en los demás filtros debe indicarse.",
                  "enum": [
                    "equals",
                    "not_equals",
                    "greater_than",
                    "less_than",
                    "between",
                    "contains",
                    "not_contains",
                    "last_deal_before",
                    "last_deal_after"
                  ]
                },
                "value": {
                  "type": [
                    "string",
                    "number",
                    "boolean",
                    "null"
                  ],
                  "description": "Booleano para hasDeals; fecha ISO para last_deal_before/after; número o cadena numérica para comparaciones de importe."
                },
                "valueTo": {
                  "type": [
                    "string",
                    "number"
                  ],
                  "description": "Límite superior inclusivo para between (value es el inferior inclusivo)."
                }
              }
            }
          },
          "page": {
            "type": "integer",
            "minimum": 1,
            "default": 1
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 500,
            "default": 50
          }
        }
      },
      "AdvancedFilterResult": {
        "type": "object",
        "required": [
          "data",
          "pagination"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "_id",
                "properties",
                "hasDeals"
              ],
              "properties": {
                "_id": {
                  "type": "string"
                },
                "properties": {
                  "type": "object",
                  "additionalProperties": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "hasDeals": {
                  "type": "boolean"
                }
              }
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "ContactError": {
        "type": "object",
        "description": "Error del controller; algunos handlers omiten success. Para INVALID_CITY_CODE (400) el cuerpo es el de CityUnresolvedError (code, input, status y candidates vacío). Identificación (Épica Z): 422 ID_TYPE_UNRESOLVED trae input y, si el valor es ambiguo, candidates con códigos DIAN (\"1\" → [\"11\",\"13\"], \"11\" → [\"11\",\"47\"], \"12\" → [\"12\",\"48\"]); 400 INVALID_ID_NUMBER trae field: idNumber.",
        "required": [
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "input": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "field": {
            "type": "string",
            "description": "Campo inválido (INVALID_ID_NUMBER → idNumber; CONVERSATION_* → linkedConversationId)."
          },
          "contactId": {
            "type": "string",
            "description": "Contacto ya enlazado a la conversación (409 CONVERSATION_ALREADY_LINKED)."
          },
          "candidates": {
            "type": "array",
            "description": "Municipios (CITY_UNRESOLVED) o códigos DIAN (ID_TYPE_UNRESOLVED).",
            "items": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/FormError/properties/candidates/items"
                },
                {
                  "type": "string",
                  "description": "Código DIAN posible del tipo de documento."
                }
              ]
            }
          }
        }
      },
      "Company": {
        "type": "object",
        "description": "Representación aplanada de la empresa: properties[] se exponen como claves planas (name, industry, website, email, phone, address, city, country, idType, idNumber, size, notes y cualquier otra personalizada). Para claves repetidas gana la primera.",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "industry": {
            "type": "string"
          },
          "website": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "phone": {
            "type": "string",
            "description": "Normalizado a dígitos."
          },
          "address": {
            "type": "string"
          },
          "city": {
            "type": "string",
            "description": "Ciudad como texto; puede estar sin confirmar (cityCode vacío)."
          },
          "cityCode": {
            "type": "string",
            "description": "Código DANE del municipio (5 dígitos); fuente de verdad de la ubicación. Vacío = sin confirmar."
          },
          "state": {
            "type": "string",
            "description": "Departamento",
            "derivado de cityCode.": null
          },
          "stateCode": {
            "type": "string",
            "description": "Código DANE del departamento (2 dígitos)",
            "derivado de cityCode.": null
          },
          "country": {
            "type": "string"
          },
          "idType": {
            "type": "string",
            "description": "Tipo de documento como código DIAN (31 NIT",
            "13 cédula…). Las empresas aún no migradas pueden traer el valor viejo de la organización.": null
          },
          "idNumber": {
            "type": "string",
            "description": "Número de documento sin puntos ni DV; clave de dedupe."
          },
          "dv": {
            "type": "string",
            "description": "Dígito de verificación del NIT (solo con idType 31)."
          },
          "size": {
            "type": "string"
          },
          "notes": {
            "type": "string"
          },
          "iconUrl": {
            "type": "string",
            "format": "uri",
            "description": "Icono del website (apple-touch-icon, favicon declarado o /favicon.ico), detectado por el backend al crear/editar la empresa. Solo lectura; se omite si la empresa no tiene website o no se encontró icono. Puede tardar unos segundos en aparecer después de un POST/PUT."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "additionalProperties": true
      },
      "CompanyWrite": {
        "type": "object",
        "description": "Body plano; cada clave se guarda como property de la empresa. Aliases aceptados (con precedencia de la clave canónica): companyName/company→name, industria→industry, web→website, telefono/mobile→phone, direccion→address, ciudad→city, pais→country, nit/NIT→idNumber, notas→notes. Los valores que parecen teléfono se normalizan a dígitos; los objetos se serializan a JSON. Se ignoran id, _id, organizationId, properties, EmployeeOwner, files, importId, iconUrl, iconSource, createdAt y updatedAt. Ubicación (Épica Y, modo estricto): cityCode válido o city que resuelva sin ambigüedad contra el catálogo DANE; si no, 422 CITY_UNRESOLVED con candidates (400 INVALID_CITY_CODE para un cityCode inexistente). Sin city/cityCode/state en el body la ubicación no se toca.",
        "properties": {
          "name": {
            "type": "string"
          },
          "industry": {
            "type": "string"
          },
          "website": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "phone": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "city": {
            "type": "string",
            "description": "Ciudad (Colombia); nombre inequívoco del catálogo DANE. Se deriva de cityCode si viene."
          },
          "cityCode": {
            "type": "string",
            "pattern": "^\\d{5}$",
            "description": "Código DANE del municipio (5 dígitos); deriva city",
            "state y stateCode.": null
          },
          "state": {
            "type": "string",
            "description": "Departamento (texto); solo pista para desambiguar city."
          },
          "country": {
            "type": "string"
          },
          "idType": {
            "description": "Tipo de documento (Épica Z, modo ESTRICTO). Se guarda como código DIAN: 11 RC, 12 TI, 13 cédula, 21 TE, 22 CE, 31 NIT, 41 pasaporte, 42 doc. extranjero, 47 PEP, 48 PPT, 50 NIT de otro país, 91 NUIP. También acepta un texto inequívoco (CC, NIT, cédula, pasaporte…) o el valor de una opción del select idType de la organización. Ambiguo (\"1\", \"2\", \"11\", \"12\") o desconocido → 422 ID_TYPE_UNRESOLVED con candidates (códigos).",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "11",
                  "12",
                  "13",
                  "21",
                  "22",
                  "31",
                  "41",
                  "42",
                  "47",
                  "48",
                  "50",
                  "91"
                ]
              },
              {
                "type": "string"
              }
            ]
          },
          "idNumber": {
            "type": "string",
            "description": "Número de documento; alias nit/NIT. Se guarda normalizado: sin puntos ni espacios; para NIT (31) el DV escrito con guion o pegado (10 dígitos) se mueve a dv. Tipo colombiano → solo dígitos (1-15); extranjero (41/42/50) → alfanumérico en mayúsculas. Inválido para el tipo → 400 INVALID_ID_NUMBER."
          },
          "dv": {
            "type": "string",
            "pattern": "^\\d$",
            "description": "Dígito de verificación",
            "solo con idType 31 (con otro tipo queda vacío). Si falta se calcula (mod-11 DIAN); uno distinto se guarda y queda en el log.": null
          },
          "size": {
            "type": "string"
          },
          "notes": {
            "type": "string"
          }
        },
        "additionalProperties": true
      },
      "CompanyCreate": {
        "description": "Requiere name o idNumber (o un alias de ellos) con valor no vacío.",
        "allOf": [
          {
            "$ref": "#/components/schemas/CompanyWrite"
          },
          {
            "anyOf": [
              {
                "required": [
                  "name"
                ]
              },
              {
                "required": [
                  "idNumber"
                ]
              },
              {
                "required": [
                  "companyName"
                ]
              },
              {
                "required": [
                  "nit"
                ]
              }
            ]
          }
        ]
      },
      "CompanyDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Company"
          },
          {
            "type": "object",
            "required": [
              "contacts",
              "deals",
              "resume"
            ],
            "properties": {
              "contacts": {
                "type": "array",
                "description": "Contactos vinculados, más recientes primero.",
                "items": {
                  "$ref": "#/components/schemas/CompanyContactsResult/properties/data/items"
                }
              },
              "deals": {
                "type": "array",
                "description": "Negocios asociados ordenados por fecha de venta descendente (wonAt, o closingDate/createdAt en los viejos).",
                "items": {
                  "type": "object",
                  "required": [
                    "id",
                    "title",
                    "amount"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "title": {
                      "type": "string"
                    },
                    "amount": {
                      "type": "number"
                    },
                    "closingDate": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "wonAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "lostAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "status": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        }
                      }
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              },
              "resume": {
                "type": "object",
                "required": [
                  "totalRevenue",
                  "totalDeals",
                  "totalContacts",
                  "lastDeal"
                ],
                "properties": {
                  "totalRevenue": {
                    "type": "number",
                    "description": "Suma de amount de todos los negocios",
                    "ganados o no.": null
                  },
                  "totalDeals": {
                    "type": "integer"
                  },
                  "totalContacts": {
                    "type": "integer"
                  },
                  "lastDeal": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "description": "Negocio más reciente por fecha de venta.",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "title": {
                        "type": "string"
                      },
                      "amount": {
                        "type": "number"
                      },
                      "closingDate": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "wonAt": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time"
                      },
                      "lostAt": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time"
                      }
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "CompanyResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "message": {
            "type": "string"
          },
          "data": {
            "$ref": "#/components/schemas/Company"
          }
        }
      },
      "CompanyDetailResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/CompanyDetail"
          }
        }
      },
      "CompanyListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Company"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "CompanyContactsResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "Contacto aplanado tal como lo devuelven los endpoints de empresa.",
              "required": [
                "id",
                "firstName",
                "lastName",
                "fullName",
                "leadScore"
              ],
              "properties": {
                "id": {
                  "type": "string"
                },
                "firstName": {
                  "type": "string"
                },
                "lastName": {
                  "type": "string"
                },
                "fullName": {
                  "type": "string"
                },
                "email": {
                  "type": "string"
                },
                "mobile": {
                  "type": "string"
                },
                "leadScore": {
                  "type": "number"
                },
                "createdAt": {
                  "type": "string",
                  "format": "date-time"
                },
                "updatedAt": {
                  "type": "string",
                  "format": "date-time"
                }
              },
              "additionalProperties": true
            }
          }
        }
      },
      "LinkContactsRequest": {
        "type": "object",
        "required": [
          "contactIds"
        ],
        "properties": {
          "contactIds": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string"
            },
            "description": "IDs de contactos. Los inválidos o de otra organización se cuentan en skipped."
          }
        }
      },
      "LinkContactsResult": {
        "type": "object",
        "required": [
          "success",
          "linked",
          "skipped"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "linked": {
            "type": "integer",
            "description": "Contactos actualizados con companyId."
          },
          "skipped": {
            "type": "integer",
            "description": "IDs inválidos o ajenos a la organización."
          }
        }
      },
      "CompanyDeleteResult": {
        "type": "object",
        "required": [
          "success",
          "message",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "message": {
            "type": "string",
            "const": "Company deleted successfully"
          },
          "data": {
            "type": "object",
            "required": [
              "id"
            ],
            "properties": {
              "id": {
                "type": "string"
              }
            }
          }
        }
      },
      "CompanyError": {
        "type": "object",
        "description": "Para INVALID_CITY_CODE (400) el cuerpo es el de CityUnresolvedError (code, input, status, candidates vacío). Identificación (Épica Z): 422 ID_TYPE_UNRESOLVED trae input y, si el valor es ambiguo, candidates con códigos DIAN; 400 INVALID_ID_NUMBER trae field: idNumber.",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "input": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "field": {
            "type": "string",
            "description": "Campo inválido (INVALID_ID_NUMBER → idNumber)."
          },
          "candidates": {
            "type": "array",
            "description": "Municipios (CITY_UNRESOLVED) o códigos DIAN (ID_TYPE_UNRESOLVED).",
            "items": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/FormError/properties/candidates/items"
                },
                {
                  "type": "string",
                  "description": "Código DIAN posible del tipo de documento."
                }
              ]
            }
          }
        }
      },
      "Deal": {
        "type": "object",
        "required": [
          "id",
          "title",
          "amount",
          "closingDate",
          "pipeline",
          "status",
          "contact",
          "company",
          "customFields"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "amount": {
            "type": "number"
          },
          "closingDate": {
            "type": "string",
            "format": "date-time",
            "description": "Fecha calendario de cierre (medianoche UTC del día). Mientras el negocio está abierto es la ESTIMADA que puso el vendedor; al entrar a una etapa ganada o perdida pasa a ser la fecha REAL de cierre (hoy en la zona horaria de la organización, o la fecha enviada en esa misma escritura). Es el campo por el que filtran closingFrom/closingTo y los informes de ventas."
          },
          "wonAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Instante en que entró a una etapa ganada. null si nunca se ganó o se reabrió."
          },
          "lostAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Instante en que entró a una etapa perdida. Excluyente con wonAt."
          },
          "expectedCloseDate": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "La closingDate estimada que tenía antes de cerrarse. null si nunca cerró o cerró el día previsto."
          },
          "pipeline": {
            "type": [
              "object",
              "null"
            ],
            "required": [
              "id"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "status": {
            "type": [
              "object",
              "null"
            ],
            "description": "Etapa del negocio; null si no tiene etapa asignada.",
            "required": [
              "id"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "color": {
                "type": "string"
              },
              "isWon": {
                "type": "boolean",
                "description": "Ausente = sin configurar (se infiere por nombre). No asumir false."
              },
              "isLost": {
                "type": "boolean",
                "description": "Igual que isWon",
                "tri-estado.": null
              },
              "probability": {
                "type": "number",
                "minimum": 0,
                "maximum": 100,
                "description": "Ausente = etapa sin probabilidad configurada."
              }
            }
          },
          "contact": {
            "type": [
              "object",
              "null"
            ],
            "description": "Contacto principal aplanado (properties como claves planas); null si no hay.",
            "required": [
              "id",
              "fullName",
              "company",
              "leadScore"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "fullName": {
                "type": "string"
              },
              "company": {
                "type": "string",
                "description": "Texto de la property companyName; puede ser ''."
              },
              "email": {
                "type": "string"
              },
              "mobile": {
                "type": "string"
              },
              "leadScore": {
                "type": "number"
              }
            },
            "additionalProperties": true
          },
          "company": {
            "type": [
              "object",
              "null"
            ],
            "required": [
              "id",
              "name"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string",
                "description": "Nombre de la empresa; '' si no se pudo resolver."
              }
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "customFields": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "field",
                "value"
              ],
              "properties": {
                "field": {
                  "type": "string",
                  "description": "Nombre del campo personalizado (o su ID si no se pudo poblar)."
                },
                "type": {
                  "type": "string"
                },
                "value": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "DealCreate": {
        "type": "object",
        "required": [
          "title",
          "amount",
          "pipeline"
        ],
        "properties": {
          "title": {
            "type": "string"
          },
          "amount": {
            "type": [
              "number",
              "string"
            ],
            "description": "Número o cadena numérica; se guarda como número."
          },
          "pipeline": {
            "type": "string",
            "description": "ID de un pipeline de la organización (ver GET /deals/config)."
          },
          "status": {
            "type": "string",
            "description": "ID de una etapa del MISMO pipeline. Opcional; sin etapa queda null."
          },
          "closingDate": {
            "type": "string",
            "format": "date-time",
            "description": "Por defecto hoy (zona horaria de la organización). Si el negocio nace en una etapa ganada/perdida, esta fecha es la de cierre real y se sella wonAt/lostAt con ella."
          },
          "contact": {
            "$ref": "#/components/schemas/DealContactInput"
          },
          "companyId": {
            "type": "string",
            "description": "Empresa de la organización. Si se omite, se hereda el companyId del contacto asociado (si lo tiene)."
          },
          "customFields": {
            "type": "array",
            "description": "Se validan contra los campos del pipeline; un campo required no puede ir vacío.",
            "items": {
              "type": "object",
              "required": [
                "field",
                "value"
              ],
              "properties": {
                "field": {
                  "type": "string",
                  "description": "ID del campo personalizado (DealsFields) del pipeline."
                },
                "value": {
                  "type": "string",
                  "description": "Se guarda recortado; vacío se omite al crear."
                }
              }
            }
          }
        }
      },
      "DealUpdate": {
        "type": "object",
        "description": "Solo se tocan los campos enviados.",
        "properties": {
          "title": {
            "type": "string"
          },
          "amount": {
            "type": [
              "number",
              "string"
            ],
            "description": "Número o cadena numérica."
          },
          "pipeline": {
            "type": "string",
            "description": "ID de pipeline de la organización. No revalida la etapa actual."
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "description": "ID de etapa de la organización; null quita la etapa. Cambiar la etapa pasa por los requisitos de Blueprints (422 si faltan) y emite deals.status_changed (automatizaciones) si realmente cambió."
          },
          "closingDate": {
            "type": "string",
            "format": "date-time",
            "description": "Si se envía junto con un cambio de etapa, es la fecha de cierre que queda (al cerrar y al reabrir). Sin ella, al cerrar se usa hoy (zona horaria de la organización) y al reabrir vuelve la estimada original."
          },
          "contact": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/DealContactInput"
              },
              {
                "type": "null"
              }
            ],
            "description": "null desasocia; un objeto se resuelve por id o teléfono (400 si no resuelve)."
          },
          "companyId": {
            "type": [
              "string",
              "null"
            ],
            "description": "null o '' desasocia la empresa; un id se valida contra la organización."
          },
          "customFields": {
            "type": "array",
            "description": "Upsert por campo; también se usan para evaluar los requisitos de la etapa destino.",
            "items": {
              "$ref": "#/components/schemas/DealCreate/properties/customFields/items"
            }
          }
        }
      },
      "DealContactInput": {
        "type": "object",
        "description": "Contacto a asociar. Con mobile o whatsapp_phone busca un contacto de la organización por móvil exacto y, si no existe, LO CREA con las properties recibidas (queda auditado como creado por la API). Con id usa un contacto existente de la organización. Si viene ambos, el teléfono tiene precedencia. Un objeto sin id ni teléfono no asocia nada.",
        "properties": {
          "id": {
            "type": "string",
            "description": "ID de un contacto existente de la organización."
          },
          "mobile": {
            "type": "string"
          },
          "whatsapp_phone": {
            "type": "string",
            "description": "Alias de mobile."
          },
          "name": {
            "type": "string",
            "description": "Se guarda como firstName al crear."
          },
          "firstName": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "company": {
            "type": "string",
            "description": "Texto de empresa (companyName) al crear."
          },
          "companyName": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "companyType": {
            "type": "string"
          }
        }
      },
      "DealResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "message": {
            "type": "string"
          },
          "data": {
            "$ref": "#/components/schemas/Deal"
          },
          "warnings": {
            "type": "array",
            "description": "Solo al crear: campos `required` con `enforcement: \"warn\"` que quedaron vacíos. No impiden crear el negocio, se reportan.",
            "items": {
              "type": "object",
              "required": [
                "id",
                "name"
              ],
              "properties": {
                "id": {
                  "type": "string",
                  "description": "_id de la definición del campo (dealsfields)."
                },
                "name": {
                  "type": "string"
                },
                "type": {
                  "type": "string",
                  "example": "select"
                }
              }
            }
          }
        }
      },
      "DealListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Deal"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "DealDeleteResult": {
        "type": "object",
        "required": [
          "success",
          "message",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "message": {
            "type": "string",
            "const": "Deal deleted successfully"
          },
          "data": {
            "type": "object",
            "required": [
              "id"
            ],
            "properties": {
              "id": {
                "type": "string"
              }
            }
          }
        }
      },
      "DealsConfigResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "name",
                "stages"
              ],
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "stages": {
                  "type": "array",
                  "description": "Ordenadas por order ascendente.",
                  "items": {
                    "type": "object",
                    "required": [
                      "id",
                      "name",
                      "order"
                    ],
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "name": {
                        "type": "string"
                      },
                      "color": {
                        "type": "string"
                      },
                      "order": {
                        "type": "integer"
                      },
                      "isWon": {
                        "type": "boolean"
                      },
                      "isLost": {
                        "type": "boolean"
                      },
                      "probability": {
                        "type": "number"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "DealError": {
        "type": "object",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          },
          "required": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "details": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "missingFields": {
            "type": "array",
            "description": "Campos personalizados `required` del pipeline que faltan al crear. Trae el id porque `customFields[].field` se manda por _id y GET /deals/config no expone las definiciones: sin esto no hay de dónde sacarlos.",
            "items": {
              "$ref": "#/components/schemas/DealResult/properties/warnings/items"
            }
          }
        }
      },
      "StageRequirementsError": {
        "type": "object",
        "description": "Requisitos de Blueprints no cumplidos para llegar a la etapa destino.",
        "required": [
          "success",
          "error",
          "code",
          "message",
          "missing"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "const": "STAGE_REQUIREMENTS_UNMET"
          },
          "message": {
            "type": "string"
          },
          "missing": {
            "type": "array",
            "description": "Requisitos bloqueantes.",
            "items": {
              "type": "object",
              "required": [
                "kind",
                "id",
                "label",
                "stageId",
                "stageName"
              ],
              "properties": {
                "kind": {
                  "type": "string",
                  "enum": [
                    "field",
                    "checklist"
                  ]
                },
                "id": {
                  "type": "string",
                  "description": "ID del campo personalizado o del ítem del checklist."
                },
                "label": {
                  "type": "string"
                },
                "stageId": {
                  "type": "string",
                  "description": "Etapa que exige el requisito."
                },
                "stageName": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "description": "Requisitos en modo warn; no bloquean.",
            "items": {
              "$ref": "#/components/schemas/StageRequirementsError/properties/missing/items"
            }
          }
        }
      },
      "Form": {
        "type": "object",
        "required": [
          "id",
          "name",
          "description",
          "slug",
          "active",
          "fields",
          "hiddenFields",
          "createContact",
          "defaultSource",
          "tags",
          "deal",
          "stats"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "slug": {
            "type": [
              "string",
              "null"
            ]
          },
          "active": {
            "type": "boolean"
          },
          "fields": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "key",
                "label",
                "type",
                "required"
              ],
              "properties": {
                "key": {
                  "type": "string",
                  "description": "Clave de la property de contacto de la organización (email",
                  "mobile": null,
                  "firstName o una personalizada).": null
                },
                "label": {
                  "type": "string"
                },
                "type": {
                  "type": "string",
                  "description": "text",
                  "email": null,
                  "tel": null,
                  "number": null,
                  "textarea": null,
                  "select...": null
                },
                "required": {
                  "type": "boolean"
                },
                "options": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          },
          "hiddenFields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Claves de atribución que captura la página pública (utm_*, gclid, fbclid, referrer, page_url)."
          },
          "createContact": {
            "type": "boolean"
          },
          "defaultSource": {
            "type": "string",
            "description": "Origen del catálogo cerrado cuando no llega utm_source."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "deal": {
            "type": [
              "object",
              "null"
            ],
            "description": "null si el form no crea negocio.",
            "properties": {
              "pipelineId": {
                "type": "string"
              },
              "stageId": {
                "type": "string"
              }
            }
          },
          "stats": {
            "type": "object",
            "required": [
              "submissions"
            ],
            "properties": {
              "submissions": {
                "type": "integer"
              },
              "lastSubmissionAt": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "share": {
            "type": "object",
            "description": "Solo en el detalle; requiere que el form tenga slug y publicKey.",
            "required": [
              "publicUrl",
              "embedScript",
              "embedIframe",
              "webhookUrl"
            ],
            "properties": {
              "publicUrl": {
                "type": "string",
                "format": "uri"
              },
              "embedScript": {
                "type": "string",
                "description": "Etiqueta script para embed.js."
              },
              "embedIframe": {
                "type": "string"
              },
              "webhookUrl": {
                "type": "string",
                "format": "uri",
                "description": "Submit público con ?key=publicKey (para Elementor",
                "Zapier...).": null
              }
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "FormUpdate": {
        "type": "object",
        "description": "Cualquier subconjunto de la lista blanca del editor. Nunca cambia slug, publicKey, stats ni la organización; las claves fuera de la lista se ignoran. Saneo aplicado: hiddenFields se filtra al catálogo permitido, defaultSource se normaliza al catálogo de orígenes (desconocido → website), tags se deduplican (máx. 30), allowedDomains se normalizan a host, notify.emails se validan (máx. 10), theme.primaryColor debe ser hexadecimal de 6 dígitos y afterSubmit.redirectUrl debe ser http(s).",
        "properties": {
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "active": {
            "type": "boolean",
            "description": "false pausa el form (submit responde 410)."
          },
          "fields": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "key"
              ],
              "properties": {
                "key": {
                  "type": "string",
                  "description": "Alias aceptado - fieldName. Se descartan los campos sin key."
                },
                "label": {
                  "type": "string",
                  "maxLength": 120
                },
                "placeholder": {
                  "type": "string",
                  "maxLength": 120
                },
                "type": {
                  "type": "string",
                  "default": "text"
                },
                "required": {
                  "type": "boolean",
                  "default": false
                },
                "options": {
                  "type": "array",
                  "maxItems": 50,
                  "items": {
                    "type": "string"
                  }
                },
                "order": {
                  "type": "integer",
                  "description": "Por defecto la posición en el array."
                }
              }
            }
          },
          "hiddenFields": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "utm_source",
                "utm_medium",
                "utm_campaign",
                "utm_term",
                "utm_content",
                "gclid",
                "fbclid",
                "referrer",
                "page_url"
              ]
            }
          },
          "theme": {
            "type": "object",
            "properties": {
              "primaryColor": {
                "type": "string",
                "pattern": "^#[0-9a-fA-F]{6}$"
              },
              "logoUrl": {
                "type": "string"
              },
              "buttonText": {
                "type": "string"
              },
              "showBranding": {
                "type": "boolean"
              },
              "title": {
                "type": "string"
              },
              "description": {
                "type": "string"
              },
              "appearance": {
                "type": "string",
                "enum": [
                  "light",
                  "dark"
                ],
                "description": "Paleta de la página pública y del widget."
              }
            }
          },
          "widget": {
            "type": "object",
            "description": "Widget flotante: el mismo snippet de embed con data-widget=\"1\" pinta un botón fijo en la web que abre el formulario en un panel. La config se sirve en GET /f/{slug}/widget.json.",
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "position": {
                "type": "string",
                "enum": [
                  "right",
                  "left"
                ]
              },
              "launcherText": {
                "type": "string",
                "maxLength": 40
              },
              "headerTitle": {
                "type": "string",
                "maxLength": 60,
                "description": "Texto de la barra superior del panel; vacío = barra sin texto."
              }
            }
          },
          "afterSubmit": {
            "type": "object",
            "properties": {
              "mode": {
                "type": "string",
                "enum": [
                  "message",
                  "redirect",
                  "whatsapp"
                ]
              },
              "message": {
                "type": "string"
              },
              "redirectUrl": {
                "type": "string",
                "format": "uri"
              },
              "whatsapp": {
                "type": "object",
                "description": "Modo whatsapp; tras enviar abre wa.me con el mensaje ({{nombre}} = firstName). phone vacío = número de la organización.",
                "properties": {
                  "phone": {
                    "type": "string",
                    "description": "Solo dígitos con indicativo",
                    "ej. 573001234567.": null
                  },
                  "message": {
                    "type": "string",
                    "maxLength": 500
                  }
                }
              }
            }
          },
          "createContact": {
            "type": "boolean"
          },
          "defaultSource": {
            "type": "string"
          },
          "tags": {
            "type": "array",
            "maxItems": 30,
            "items": {
              "type": "string"
            }
          },
          "listId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Lista estática a la que se agrega el contacto; null la quita."
          },
          "ownerId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Usuario dueño de los contactos y negocios creados; null lo quita."
          },
          "deal": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "pipelineId": {
                "type": "string"
              },
              "stageId": {
                "type": "string"
              },
              "titleTemplate": {
                "type": "string",
                "description": "Admite {{nombre}}, {{email}} y claves de campos."
              },
              "amount": {
                "type": "number"
              }
            }
          },
          "notify": {
            "type": "object",
            "properties": {
              "emails": {
                "type": "array",
                "maxItems": 10,
                "items": {
                  "type": "string",
                  "format": "email"
                }
              },
              "inApp": {
                "type": "boolean"
              }
            }
          },
          "consent": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "text": {
                "type": "string"
              },
              "required": {
                "type": "boolean"
              }
            }
          },
          "allowedDomains": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Hosts desde los que se acepta el embed; un envío ajeno se acepta pero se marca sospechoso."
          }
        }
      },
      "FormResponse": {
        "type": "object",
        "required": [
          "id",
          "formId",
          "contactId",
          "dealId",
          "outcome",
          "responses",
          "context",
          "consent",
          "suspicious",
          "suspiciousReason",
          "receivedAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "formId": {
            "type": "string"
          },
          "contactId": {
            "type": [
              "string",
              "null"
            ]
          },
          "dealId": {
            "type": [
              "string",
              "null"
            ]
          },
          "outcome": {
            "type": "string",
            "enum": [
              "created",
              "updated",
              "not_created"
            ],
            "description": "created = contacto nuevo; updated = existía y solo se rellenaron vacíos; not_created = el form no crea contactos o el envío fue sospechoso."
          },
          "responses": {
            "type": "object",
            "additionalProperties": true,
            "description": "Valores enviados, por clave de campo."
          },
          "context": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "referrer": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "pageUrl": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "utm": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                }
              },
              "gclid": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "fbclid": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "embed": {
                "type": "boolean"
              }
            }
          },
          "consent": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "accepted": {
                "type": "boolean"
              },
              "text": {
                "type": "string"
              },
              "at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "suspicious": {
            "type": "boolean"
          },
          "suspiciousReason": {
            "type": [
              "string",
              "null"
            ]
          },
          "receivedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "FormSubmitRequest": {
        "type": "object",
        "description": "Valores por clave de campo del form, más atribución opcional. No aplica honeypot ni dominios permitidos (llamada autenticada); sí dedupe por email/móvil, atribución first-touch, etiquetas, lista, negocio, aviso al dueño y el trigger form_submitted. Un string vacío en un campo requerido cuenta como ausente.",
        "minProperties": 1,
        "properties": {
          "utm_source": {
            "type": "string",
            "description": "Se normaliza al catálogo de orígenes."
          },
          "utm_medium": {
            "type": "string"
          },
          "utm_campaign": {
            "type": "string"
          },
          "utm_term": {
            "type": "string"
          },
          "utm_content": {
            "type": "string"
          },
          "referrer": {
            "type": "string"
          },
          "page_url": {
            "type": "string"
          }
        },
        "additionalProperties": true
      },
      "FormSubmitResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "responseId",
              "outcome",
              "suspicious"
            ],
            "properties": {
              "responseId": {
                "type": "string"
              },
              "outcome": {
                "type": "string",
                "enum": [
                  "created",
                  "updated",
                  "not_created"
                ]
              },
              "contactId": {
                "type": "string",
                "description": "Ausente si no se creó ni encontró contacto."
              },
              "dealId": {
                "type": "string",
                "description": "Ausente si el form no crea negocio o falló al crearlo."
              },
              "suspicious": {
                "type": "boolean"
              },
              "suspiciousReason": {
                "type": "string"
              }
            }
          }
        }
      },
      "FormResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/Form"
          }
        }
      },
      "FormListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Form"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "LandingPage": {
        "type": "object",
        "required": [
          "id",
          "name",
          "slug",
          "status",
          "template",
          "formId",
          "publicUrl",
          "sectionsCount",
          "stats",
          "publishedAt",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "description": "Nombre interno."
          },
          "slug": {
            "type": "string",
            "description": "Único global; la página vive en <PAGES_PUBLIC_BASE_URL>/p/<slug>."
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "published"
            ]
          },
          "template": {
            "type": "string",
            "enum": [
              "captacion",
              "cotizacion",
              "evento",
              "descarga",
              "blank"
            ]
          },
          "formId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Formulario de captación que se pinta inline en el bloque form."
          },
          "publicUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Solo cuando está publicada."
          },
          "sectionsCount": {
            "type": "integer",
            "minimum": 0,
            "description": "Secciones activas."
          },
          "sections": {
            "type": "array",
            "description": "Solo en el detalle.",
            "items": {
              "$ref": "#/components/schemas/LandingSection"
            }
          },
          "theme": {
            "type": "object",
            "description": "Solo en el detalle.",
            "additionalProperties": true
          },
          "seo": {
            "type": "object",
            "description": "Solo en el detalle.",
            "additionalProperties": true
          },
          "whatsapp": {
            "type": "object",
            "description": "Solo en el detalle. phone en dígitos E.164 sin +; null = usa el número de la organización.",
            "properties": {
              "phone": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "message": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Mensaje prellenado del chat."
              },
              "sticky": {
                "type": "boolean"
              }
            }
          },
          "tracking": {
            "type": "object",
            "description": "Solo en el detalle. IDs validados; nunca script libre.",
            "properties": {
              "metaPixelId": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "ga4MeasurementId": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "legal": {
            "type": "object",
            "description": "Solo en el detalle.",
            "properties": {
              "privacyUrl": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Política de datos propia; null = la de FusionCRM con la org como responsable."
              }
            }
          },
          "stats": {
            "type": "object",
            "required": [
              "views",
              "uniques",
              "submissions",
              "whatsappClicks"
            ],
            "properties": {
              "views": {
                "type": "integer",
                "minimum": 0
              },
              "uniques": {
                "type": "integer",
                "minimum": 0
              },
              "submissions": {
                "type": "integer",
                "minimum": 0
              },
              "whatsappClicks": {
                "type": "integer",
                "minimum": 0
              },
              "lastViewAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          },
          "publishedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "LandingSection": {
        "type": "object",
        "required": [
          "id",
          "type",
          "enabled",
          "order",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "hero",
              "benefits",
              "text",
              "gallery",
              "testimonials",
              "faq",
              "whatsapp_cta",
              "form"
            ],
            "description": "Bloque del catálogo cerrado. En el CRM se nombran Portada, Por qué comprarte a ti, Cuéntalo con tus palabras, Muestra tu producto, Lo que dicen tus clientes, Preguntas frecuentes, Háblame por WhatsApp y Déjame tus datos."
          },
          "enabled": {
            "type": "boolean"
          },
          "order": {
            "type": "integer",
            "minimum": 0
          },
          "data": {
            "type": "object",
            "description": "Contenido del bloque según su tipo (texto plano, se escapa al renderizar).",
            "additionalProperties": true
          }
        }
      },
      "LandingPageResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/LandingPage"
          }
        }
      },
      "LandingPageListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LandingPage"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "LandingPageStatsResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "id",
              "name",
              "slug",
              "status",
              "range",
              "totals",
              "series",
              "funnel",
              "bySource",
              "bySection",
              "byDevice",
              "lastResponses"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "slug": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "enum": [
                  "draft",
                  "published"
                ]
              },
              "range": {
                "type": "object",
                "required": [
                  "from",
                  "to"
                ],
                "properties": {
                  "from": {
                    "type": "string",
                    "format": "date",
                    "description": "YYYY-MM-DD en America/Bogota."
                  },
                  "to": {
                    "type": "string",
                    "format": "date"
                  }
                }
              },
              "totals": {
                "type": "object",
                "description": "Suma del agregado diario en el rango. conversionForm = envíos/únicos y conversionWhatsapp = clics WhatsApp/únicos, en porcentaje con un decimal.",
                "required": [
                  "views",
                  "uniques",
                  "sessions",
                  "submissions",
                  "whatsappClicks",
                  "conversionForm",
                  "conversionWhatsapp"
                ],
                "properties": {
                  "views": {
                    "type": "integer"
                  },
                  "uniques": {
                    "type": "integer"
                  },
                  "sessions": {
                    "type": "integer"
                  },
                  "scroll50": {
                    "type": "integer"
                  },
                  "ctaClicks": {
                    "type": "integer"
                  },
                  "whatsappClicks": {
                    "type": "integer"
                  },
                  "formStarts": {
                    "type": "integer"
                  },
                  "submissions": {
                    "type": "integer"
                  },
                  "outboundClicks": {
                    "type": "integer"
                  },
                  "conversationsStarted": {
                    "type": "integer"
                  },
                  "dealsWon": {
                    "type": "integer"
                  },
                  "revenueWon": {
                    "type": "number"
                  },
                  "timeOnPageSum": {
                    "type": "integer"
                  },
                  "timeOnPageCount": {
                    "type": "integer"
                  },
                  "avgTimeOnPage": {
                    "type": "integer",
                    "description": "Segundos."
                  },
                  "conversionForm": {
                    "type": "number"
                  },
                  "conversionWhatsapp": {
                    "type": "number"
                  }
                }
              },
              "series": {
                "type": "array",
                "description": "Un punto por día del rango (los días sin datos van en cero).",
                "items": {
                  "type": "object",
                  "required": [
                    "date",
                    "views",
                    "uniques",
                    "submissions",
                    "whatsappClicks",
                    "ctaClicks"
                  ],
                  "properties": {
                    "date": {
                      "type": "string",
                      "format": "date"
                    },
                    "views": {
                      "type": "integer"
                    },
                    "uniques": {
                      "type": "integer"
                    },
                    "submissions": {
                      "type": "integer"
                    },
                    "whatsappClicks": {
                      "type": "integer"
                    },
                    "ctaClicks": {
                      "type": "integer"
                    }
                  }
                }
              },
              "funnel": {
                "type": "object",
                "description": "Sesiones únicas que llegaron a cada paso (log de eventos, últimos 90 días).",
                "required": [
                  "sessions",
                  "scrolled",
                  "engaged",
                  "converted",
                  "won"
                ],
                "properties": {
                  "sessions": {
                    "type": "integer"
                  },
                  "scrolled": {
                    "type": "integer",
                    "description": "Llegaron al 50 % de scroll."
                  },
                  "engaged": {
                    "type": "integer",
                    "description": "Tocaron un CTA, WhatsApp o empezaron el form."
                  },
                  "converted": {
                    "type": "integer",
                    "description": "Enviaron el form o abrieron WhatsApp."
                  },
                  "won": {
                    "type": "integer",
                    "description": "Negocio atribuido a la página pasado a ganado."
                  }
                }
              },
              "bySource": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "source",
                    "views",
                    "submissions",
                    "whatsappClicks"
                  ],
                  "properties": {
                    "source": {
                      "type": "string",
                      "description": "utm_source normalizado; direct cuando no había; qr para el código QR."
                    },
                    "views": {
                      "type": "integer"
                    },
                    "submissions": {
                      "type": "integer"
                    },
                    "whatsappClicks": {
                      "type": "integer"
                    }
                  }
                }
              },
              "bySection": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "section",
                    "ctaClicks",
                    "whatsappClicks"
                  ],
                  "properties": {
                    "section": {
                      "type": "string",
                      "description": "hero, whatsapp_cta, sticky, thanks, header, form…"
                    },
                    "ctaClicks": {
                      "type": "integer"
                    },
                    "whatsappClicks": {
                      "type": "integer"
                    }
                  }
                }
              },
              "byDevice": {
                "type": "object",
                "required": [
                  "mobile",
                  "desktop",
                  "tablet"
                ],
                "properties": {
                  "mobile": {
                    "type": "integer"
                  },
                  "desktop": {
                    "type": "integer"
                  },
                  "tablet": {
                    "type": "integer"
                  }
                }
              },
              "lastResponses": {
                "type": "array",
                "description": "Últimos 10 envíos del formulario desde esta página.",
                "items": {
                  "type": "object",
                  "required": [
                    "id",
                    "contactId",
                    "dealId",
                    "outcome",
                    "utmSource",
                    "receivedAt"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "contactId": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "dealId": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "outcome": {
                      "type": "string",
                      "enum": [
                        "created",
                        "updated",
                        "not_created",
                        "rejected"
                      ]
                    },
                    "utmSource": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "receivedAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "LandingPageCreate": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 120
          },
          "templateId": {
            "type": "string",
            "enum": [
              "captacion",
              "cotizacion",
              "evento",
              "descarga",
              "blank"
            ],
            "default": "blank",
            "description": "Alias aceptado: template."
          },
          "formId": {
            "type": "string",
            "description": "Formulario de la organización para el bloque form."
          },
          "sections": {
            "type": "array",
            "maxItems": 12,
            "items": {
              "type": "object",
              "required": [
                "type"
              ],
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Consérvalo al editar una sección existente; si falta se genera."
                },
                "type": {
                  "type": "string",
                  "enum": [
                    "hero",
                    "benefits",
                    "text",
                    "gallery",
                    "testimonials",
                    "faq",
                    "whatsapp_cta",
                    "form"
                  ]
                },
                "enabled": {
                  "type": "boolean",
                  "default": true
                },
                "data": {
                  "type": "object",
                  "additionalProperties": true,
                  "description": "Contenido según el tipo (ver example en GET /landing-pages/templates). Claves desconocidas se descartan; URLs de imagen solo http(s). El orden es la posición en el arreglo. Máximo 12 secciones y un solo bloque form."
                }
              }
            },
            "description": "Si llega, reemplaza las secciones de la plantilla."
          },
          "theme": {
            "type": "object",
            "properties": {
              "primaryColor": {
                "type": "string",
                "pattern": "^#[0-9a-fA-F]{6}$"
              },
              "accentColor": {
                "type": "string",
                "description": "#rrggbb; vacío lo quita."
              },
              "logoUrl": {
                "type": "string",
                "description": "URL http(s); otra cosa se guarda vacía."
              },
              "fontPreset": {
                "type": "string",
                "enum": [
                  "system",
                  "serif",
                  "rounded"
                ]
              },
              "showBranding": {
                "type": "boolean",
                "description": "En plan Free queda siempre en true."
              }
            }
          },
          "seo": {
            "type": "object",
            "properties": {
              "title": {
                "type": "string",
                "maxLength": 120
              },
              "description": {
                "type": "string",
                "maxLength": 300
              },
              "ogImageUrl": {
                "type": "string"
              },
              "faviconUrl": {
                "type": "string"
              },
              "indexable": {
                "type": "boolean"
              }
            }
          },
          "tracking": {
            "type": "object",
            "properties": {
              "metaPixelId": {
                "type": "string",
                "description": "15 o 16 dígitos; vacío lo quita."
              },
              "ga4MeasurementId": {
                "type": "string",
                "description": "G-XXXXXXXXXX; vacío lo quita."
              }
            }
          },
          "whatsapp": {
            "type": "object",
            "properties": {
              "phone": {
                "type": "string",
                "description": "Con indicativo; se guardan solo dígitos (7 a 15). Vacío = el número de la organización."
              },
              "message": {
                "type": "string",
                "maxLength": 300
              },
              "sticky": {
                "type": "boolean"
              }
            }
          },
          "legal": {
            "type": "object",
            "properties": {
              "privacyUrl": {
                "type": "string"
              }
            }
          }
        }
      },
      "LandingPageUpdate": {
        "type": "object",
        "minProperties": 1,
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 120
          },
          "slug": {
            "type": "string",
            "minLength": 3,
            "description": "Se normaliza a kebab-case; único global (409 si choca)."
          },
          "formId": {
            "type": [
              "string",
              "null"
            ],
            "description": "null o vacío lo desasocia."
          },
          "sections": {
            "type": "array",
            "maxItems": 12,
            "items": {
              "$ref": "#/components/schemas/LandingPageCreate/properties/sections/items"
            },
            "description": "Lista COMPLETA en el orden final."
          },
          "theme": {
            "$ref": "#/components/schemas/LandingPageCreate/properties/theme"
          },
          "seo": {
            "$ref": "#/components/schemas/LandingPageCreate/properties/seo"
          },
          "tracking": {
            "$ref": "#/components/schemas/LandingPageCreate/properties/tracking"
          },
          "whatsapp": {
            "$ref": "#/components/schemas/LandingPageCreate/properties/whatsapp"
          },
          "legal": {
            "type": "object",
            "properties": {
              "privacyUrl": {
                "type": "string"
              }
            }
          }
        }
      },
      "LandingPageUpdateResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "unpublished",
          "unpublishedReason"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/LandingPage"
          },
          "unpublished": {
            "type": "boolean",
            "description": "true si la edición devolvió la página a borrador."
          },
          "unpublishedReason": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "LandingTemplatesResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "templates",
              "sectionTypes",
              "icons"
            ],
            "properties": {
              "templates": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "id",
                    "name",
                    "description",
                    "goal",
                    "needsForm",
                    "sectionTypes"
                  ],
                  "properties": {
                    "id": {
                      "type": "string",
                      "enum": [
                        "captacion",
                        "cotizacion",
                        "evento",
                        "descarga",
                        "blank"
                      ]
                    },
                    "name": {
                      "type": "string"
                    },
                    "description": {
                      "type": "string"
                    },
                    "goal": {
                      "type": "string",
                      "enum": [
                        "whatsapp",
                        "form",
                        "both"
                      ]
                    },
                    "needsForm": {
                      "type": "boolean"
                    },
                    "sectionTypes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "sectionTypes": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "type",
                    "label",
                    "description",
                    "example"
                  ],
                  "properties": {
                    "type": {
                      "type": "string"
                    },
                    "label": {
                      "type": "string"
                    },
                    "description": {
                      "type": "string"
                    },
                    "example": {
                      "type": "object",
                      "additionalProperties": true
                    }
                  }
                }
              },
              "icons": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "FormResponseListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FormResponse"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "FormError": {
        "type": "object",
        "description": "Con code CITY_UNRESOLVED / INVALID_CITY_CODE (Épica Y, formularios en modo estricto) trae además input y candidates (municipios posibles del catálogo DANE) para que el cliente reenvíe con cityCode.",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "enum": [
              "CITY_UNRESOLVED",
              "INVALID_CITY_CODE"
            ]
          },
          "input": {
            "type": "string"
          },
          "candidates": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "Municipio posible del catálogo DANE para una ciudad no resuelta.",
              "required": [
                "code",
                "shortName",
                "stateName",
                "stateCode"
              ],
              "properties": {
                "code": {
                  "type": "string",
                  "pattern": "^\\d{5}$",
                  "description": "Código DANE del municipio (5 dígitos)."
                },
                "shortName": {
                  "type": "string",
                  "description": "Nombre corto del municipio (ej. Rionegro)."
                },
                "stateName": {
                  "type": "string",
                  "description": "Departamento (ej. Antioquia)."
                },
                "stateCode": {
                  "type": "string",
                  "pattern": "^\\d{2}$",
                  "description": "Código DANE del departamento (2 dígitos)."
                }
              }
            }
          }
        }
      },
      "Conversation": {
        "type": "object",
        "description": "Conversación con pipeline, etapa y asignado poblados. Las claves _id de los objetos anidados son las de los documentos poblados; el id raíz es el de la conversación.",
        "required": [
          "id",
          "title",
          "pipeline",
          "currentStage"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string",
            "description": "Se fija al crear y NO se sincroniza con el nombre del contacto."
          },
          "pipeline": {
            "type": [
              "object",
              "null"
            ],
            "description": "Pipeline de conversaciones poblado. Ojo, usa _id (no id) porque es el documento poblado.",
            "required": [
              "_id",
              "name"
            ],
            "properties": {
              "_id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "currentStage": {
            "type": [
              "object",
              "null"
            ],
            "description": "Etapa actual poblada (PipelineStage). Usa _id, no id.",
            "required": [
              "_id",
              "name"
            ],
            "properties": {
              "_id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "order": {
                "type": "integer"
              },
              "color": {
                "type": "string"
              }
            }
          },
          "assignedTo": {
            "type": [
              "object",
              "null"
            ],
            "description": "Usuario humano asignado, poblado. Ausente si nunca se asignó; null cuando la conversación la atiende un agente IA (assignedTo y assignedAgentId son excluyentes).",
            "required": [
              "_id"
            ],
            "properties": {
              "_id": {
                "type": "string"
              },
              "firstName": {
                "type": "string"
              },
              "lastName": {
                "type": "string"
              },
              "email": {
                "type": "string"
              },
              "profilePicture": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "priority": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high"
            ]
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "source": {
            "type": "string",
            "enum": [
              "whatsapp",
              "facebook",
              "instagram"
            ]
          },
          "unreadCount": {
            "type": "integer",
            "minimum": 0
          },
          "lastMessage": {
            "type": [
              "object",
              "string",
              "null"
            ],
            "description": "Último mensaje. En GET (listar, buscar, por ID, por teléfono) viene poblado con los campos indicados; en las respuestas de POST y PUT NO se puebla y llega como el ID del mensaje (string) o ausente si la conversación no tiene mensajes.",
            "properties": {
              "_id": {
                "type": "string"
              },
              "message": {
                "type": "string"
              },
              "direction": {
                "type": "string",
                "enum": [
                  "incoming",
                  "outgoing"
                ]
              },
              "timestamp": {
                "type": "string",
                "format": "date-time"
              },
              "type": {
                "type": "string"
              },
              "from": {
                "type": "string"
              },
              "to": {
                "type": "string"
              },
              "mediaUrl": {
                "type": "string"
              },
              "isRead": {
                "type": "boolean"
              },
              "source": {
                "type": "string",
                "enum": [
                  "whatsapp",
                  "facebook",
                  "instagram"
                ]
              },
              "deliveryStatus": {
                "type": "string",
                "enum": [
                  "accepted",
                  "sent",
                  "delivered",
                  "read",
                  "failed",
                  "held_for_quality_assessment",
                  "paused"
                ]
              }
            }
          },
          "lastMessageTimestamp": {
            "type": "string",
            "format": "date-time"
          },
          "participants": {
            "type": "object",
            "description": "Participantes de la conversación. Lo escriben los canales entrantes (WhatsApp, Messenger, Instagram); una conversación creada por esta API NO tiene participants y por eso no la encuentra find-by-phone.",
            "properties": {
              "user": {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "User"
                  },
                  "reference": {
                    "type": "string",
                    "description": "ID del usuario dueño de la integración."
                  }
                }
              },
              "contact": {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "Contact"
                  },
                  "reference": {
                    "type": "string",
                    "description": "Teléfono (WhatsApp) o PSID (Messenger/Instagram) del contacto."
                  },
                  "displayInfo": {
                    "type": "object",
                    "properties": {
                      "mobile": {
                        "type": "string"
                      },
                      "name": {
                        "type": "string"
                      },
                      "lastName": {
                        "type": "string"
                      },
                      "email": {
                        "type": "string"
                      },
                      "position": {
                        "type": "string"
                      },
                      "contactId": {
                        "type": "string"
                      }
                    }
                  },
                  "contactData": {
                    "type": "object",
                    "description": "Datos del contacto del CRM resueltos por el móvil de displayInfo. Solo aparece cuando existe un contacto con ese mobile en la property mobile; si no, el campo se omite.",
                    "required": [
                      "id",
                      "firstName",
                      "lastName",
                      "fullName",
                      "email",
                      "mobile",
                      "company",
                      "position",
                      "leadScore"
                    ],
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "firstName": {
                        "type": "string"
                      },
                      "lastName": {
                        "type": "string"
                      },
                      "fullName": {
                        "type": "string"
                      },
                      "email": {
                        "type": "string"
                      },
                      "mobile": {
                        "type": "string"
                      },
                      "company": {
                        "type": "string",
                        "description": "Texto de la property companyName; puede ser ''."
                      },
                      "position": {
                        "type": "string"
                      },
                      "leadScore": {
                        "type": "number"
                      }
                    }
                  }
                }
              }
            }
          },
          "metadata": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "key",
                "value"
              ],
              "properties": {
                "_id": {
                  "type": "string",
                  "description": "Lo asigna Mongo al subdocumento; no se envía al crear."
                },
                "key": {
                  "type": "string"
                },
                "value": {
                  "description": "Cualquier valor JSON."
                }
              }
            }
          },
          "isArchived": {
            "type": "boolean"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ConversationWithMessages": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Conversation"
          },
          {
            "type": "object",
            "required": [
              "messages"
            ],
            "properties": {
              "messages": {
                "type": "array",
                "description": "Últimos messagesLimit mensajes en orden cronológico ascendente (el más antiguo primero).",
                "items": {
                  "$ref": "#/components/schemas/Message"
                }
              }
            }
          }
        ]
      },
      "Message": {
        "type": "object",
        "description": "Mensaje tal como se guarda en Mongo (documento completo, sin transformar). Los entrantes traen isRead según los recibos del canal; los salientes nacen con isRead true.",
        "required": [
          "_id",
          "message",
          "direction",
          "timestamp",
          "type",
          "conversation"
        ],
        "properties": {
          "_id": {
            "type": "string"
          },
          "user": {
            "type": "string",
            "description": "Usuario dueño del canal por el que entró/salió el mensaje."
          },
          "organization": {
            "type": "string"
          },
          "conversation": {
            "type": "string"
          },
          "from": {
            "type": "string"
          },
          "to": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "transcription": {
            "type": "string",
            "description": "Solo en audios transcritos (ElevenLabs)."
          },
          "mediaUrl": {
            "type": "string"
          },
          "mediaId": {
            "type": "string"
          },
          "filename": {
            "type": "string"
          },
          "mimeType": {
            "type": "string"
          },
          "latitude": {
            "type": "number"
          },
          "longitude": {
            "type": "number"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          },
          "type": {
            "type": "string",
            "description": "text",
            "image": null,
            "audio": null,
            "video": null,
            "document": null,
            "sticker": null,
            "location": null,
            "reaction": null,
            "unsupported…": null
          },
          "direction": {
            "type": "string",
            "enum": [
              "incoming",
              "outgoing"
            ]
          },
          "isRead": {
            "type": "boolean"
          },
          "possibleName": {
            "type": "string"
          },
          "replyToMessage": {
            "type": [
              "string",
              "null"
            ]
          },
          "replyToSnapshot": {
            "type": "object",
            "properties": {
              "message": {
                "type": "string"
              },
              "type": {
                "type": "string"
              },
              "direction": {
                "type": "string",
                "enum": [
                  "incoming",
                  "outgoing"
                ]
              },
              "mediaUrl": {
                "type": "string"
              },
              "timestamp": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "messageId": {
            "type": "string",
            "description": "ID del mensaje en el canal (wamid en WhatsApp)."
          },
          "reactions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "reaction": {
                  "type": "string"
                },
                "user": {
                  "type": "string"
                },
                "timestamp": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            }
          },
          "messageErrors": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "code": {
                  "type": "integer"
                },
                "title": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                },
                "details": {
                  "type": "string"
                }
              }
            }
          },
          "source": {
            "type": "string",
            "enum": [
              "whatsapp",
              "facebook",
              "instagram"
            ]
          },
          "deliveryStatus": {
            "type": "string",
            "enum": [
              "accepted",
              "sent",
              "delivered",
              "read",
              "failed",
              "held_for_quality_assessment",
              "paused"
            ]
          },
          "deliveryError": {
            "type": "string"
          },
          "__v": {
            "type": "integer"
          }
        },
        "additionalProperties": true
      },
      "ConversationCreate": {
        "type": "object",
        "required": [
          "title"
        ],
        "properties": {
          "title": {
            "type": "string",
            "description": "Obligatorio y no vacío."
          },
          "pipeline": {
            "type": "string",
            "description": "ID de un pipeline de conversaciones. Si se omite se usa el pipeline default de la organización (400 si no hay default). Ojo, el ID enviado NO se valida contra la organización del token."
          },
          "stage": {
            "type": "string",
            "description": "ID de la etapa inicial; debe pertenecer al pipeline. Si se omite, la de menor order."
          },
          "assignedTo": {
            "type": "string",
            "description": "ID de usuario. No se valida que sea empleado de la organización."
          },
          "priority": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high"
            ],
            "default": "medium"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "source": {
            "type": "string",
            "enum": [
              "whatsapp",
              "facebook",
              "instagram"
            ],
            "default": "whatsapp"
          },
          "metadata": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "key",
                "value"
              ],
              "properties": {
                "key": {
                  "type": "string"
                },
                "value": {
                  "description": "Cualquier valor JSON."
                }
              }
            }
          }
        }
      },
      "ConversationUpdate": {
        "type": "object",
        "description": "Solo se tocan los campos enviados; cualquier otra clave se ignora. Un body sin ninguno de estos campos responde 400. tags y metadata REEMPLAZAN el array completo.",
        "properties": {
          "title": {
            "type": "string"
          },
          "assignedTo": {
            "type": [
              "string",
              "null"
            ],
            "description": "ID de usuario humano. Asignarlo limpia assignedAgentId. No se valida contra la organización."
          },
          "assignedAgentId": {
            "type": [
              "string",
              "null"
            ],
            "description": "ID de un agente IA de la organización (404 si no existe). Asignarlo limpia assignedTo y reanuda las automatizaciones pausadas por un handoff previo. No se puede enviar junto con assignedTo (400)."
          },
          "priority": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high"
            ]
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "metadata": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "key",
                "value"
              ],
              "properties": {
                "key": {
                  "type": "string"
                },
                "value": {
                  "description": "Cualquier valor JSON."
                }
              }
            }
          }
        }
      },
      "MoveStageRequest": {
        "type": "object",
        "required": [
          "stageId"
        ],
        "properties": {
          "stageId": {
            "type": "string",
            "description": "ID de una etapa del pipeline ACTUAL de la conversación."
          }
        }
      },
      "ChangePipelineRequest": {
        "type": "object",
        "required": [
          "pipelineId"
        ],
        "properties": {
          "pipelineId": {
            "type": "string",
            "description": "ID de un pipeline de conversaciones de la organización."
          },
          "initialStageId": {
            "type": "string",
            "description": "Etapa destino dentro del nuevo pipeline. Si se omite, la de menor order."
          }
        }
      },
      "MessageCreate": {
        "type": "object",
        "required": [
          "message",
          "direction"
        ],
        "properties": {
          "message": {
            "type": "string",
            "description": "Texto; obligatorio y no vacío."
          },
          "direction": {
            "type": "string",
            "enum": [
              "incoming",
              "outgoing"
            ]
          },
          "from": {
            "type": "string",
            "description": "Número/ID de origen. Opcional; se deriva según direction si falta."
          },
          "to": {
            "type": "string",
            "description": "Número/ID de destino. Opcional; se deriva según direction si falta."
          },
          "type": {
            "type": "string",
            "default": "text",
            "description": "text",
            "image": null,
            "audio": null,
            "video": null,
            "document…": null
          },
          "mediaUrl": {
            "type": "string"
          },
          "mediaId": {
            "type": "string",
            "description": "ID del medio en la plataforma (WhatsApp)."
          }
        }
      },
      "ConversationResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/Conversation"
          }
        }
      },
      "ConversationDetailResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/ConversationWithMessages"
          },
          "messagesPagination": {
            "type": "object",
            "description": "Paginación de mensajes hacia atrás (cursor). Solo en GET /conversations/{id}.",
            "required": [
              "limit",
              "hasMore",
              "nextBefore"
            ],
            "properties": {
              "limit": {
                "type": "integer",
                "example": 50
              },
              "hasMore": {
                "type": "boolean",
                "description": "true si existen mensajes más antiguos que los devueltos."
              },
              "nextBefore": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "Valor a pasar como messagesBefore para pedir la página anterior. null si no hay más.",
                "example": "2026-09-18T20:05:00.000Z"
              }
            }
          }
        }
      },
      "ConversationListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ConversationWithMessages"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "ConversationDeleteResult": {
        "type": "object",
        "required": [
          "success",
          "message",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "message": {
            "type": "string",
            "const": "Conversation deleted successfully"
          },
          "data": {
            "type": "object",
            "required": [
              "id"
            ],
            "properties": {
              "id": {
                "type": "string"
              }
            }
          }
        }
      },
      "MessageResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/Message"
          }
        }
      },
      "ConversationsConfigResult": {
        "type": "object",
        "description": "Pipelines de conversaciones con sus etapas. Aquí los identificadores se llaman id.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "name",
                "stages"
              ],
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "isDefault": {
                  "type": "boolean"
                },
                "stages": {
                  "type": "array",
                  "description": "Ordenadas por order ascendente.",
                  "items": {
                    "type": "object",
                    "required": [
                      "id",
                      "name",
                      "order"
                    ],
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "name": {
                        "type": "string"
                      },
                      "order": {
                        "type": "integer"
                      },
                      "color": {
                        "type": "string"
                      },
                      "autoAssign": {
                        "type": "boolean"
                      },
                      "webhookEnabled": {
                        "type": "boolean"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "PipelineListResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "_id",
                "name"
              ],
              "properties": {
                "_id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "isDefault": {
                  "type": "boolean"
                }
              }
            }
          }
        }
      },
      "PipelineStagesResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "pipeline",
              "stages"
            ],
            "properties": {
              "pipeline": {
                "$ref": "#/components/schemas/PipelineListResult/properties/data/items"
              },
              "stages": {
                "type": "array",
                "description": "Ordenadas por order ascendente.",
                "items": {
                  "type": "object",
                  "description": "Etapa tal como la devuelve Mongo (con _id).",
                  "required": [
                    "_id",
                    "name",
                    "order"
                  ],
                  "properties": {
                    "_id": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "order": {
                      "type": "integer"
                    },
                    "color": {
                      "type": "string"
                    },
                    "autoAssign": {
                      "type": "boolean"
                    },
                    "webhookEnabled": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "StageHistoryResult": {
        "type": "object",
        "description": "Historial de cambios de etapa (logs CONVERSATION_STAGE_CHANGE), más reciente primero. La paginación va plana dentro de data (total, page, limit), NO en el objeto pagination del resto de la API.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "conversationId",
              "total",
              "page",
              "limit",
              "items"
            ],
            "properties": {
              "conversationId": {
                "type": "string"
              },
              "total": {
                "type": "integer",
                "minimum": 0
              },
              "page": {
                "type": "integer",
                "minimum": 1
              },
              "limit": {
                "type": "integer",
                "minimum": 1
              },
              "items": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "_id",
                    "timestamp",
                    "triggeredBy",
                    "user",
                    "fromStage",
                    "toStage",
                    "pipeline",
                    "conversationTitle"
                  ],
                  "properties": {
                    "_id": {
                      "type": "string",
                      "description": "ID del log."
                    },
                    "timestamp": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "triggeredBy": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "enum": [
                        "user",
                        "automation",
                        "system",
                        null
                      ],
                      "description": "Los movimientos hechos por esta API se registran como system (con el userId/email del dueño del token en user)."
                    },
                    "user": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "properties": {
                        "userId": {
                          "type": "string"
                        },
                        "firstName": {
                          "type": "string"
                        },
                        "lastName": {
                          "type": "string"
                        },
                        "email": {
                          "type": "string"
                        }
                      }
                    },
                    "fromStage": {
                      "$ref": "#/components/schemas/StageHistoryResult/properties/data/properties/items/items/properties/toStage"
                    },
                    "toStage": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "description": "Etapa origen/destino tal como quedó en el log. Si la etapa origen no se pudo resolver, _id es '' y name '(desconocida)' con order -1.",
                      "required": [
                        "_id",
                        "name",
                        "order"
                      ],
                      "properties": {
                        "_id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "order": {
                          "type": "integer"
                        }
                      }
                    },
                    "pipeline": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "required": [
                        "_id",
                        "name"
                      ],
                      "properties": {
                        "_id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        }
                      }
                    },
                    "conversationTitle": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      },
      "ConversationError": {
        "type": "object",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string",
            "description": "Solo en errores internos",
            "con el detalle de la excepción.": null
          }
        }
      },
      "Quotation": {
        "type": "object",
        "description": "Representación pública de la cotización (GET, PUT, PATCH status). No expone companyId, userId, optionalItems, el historial de estados ni la señal de lectura (viewCount, publicToken…).",
        "required": [
          "id",
          "quotationNumber",
          "name",
          "status",
          "contact",
          "items",
          "subtotal",
          "taxes",
          "total",
          "paymentTerms",
          "shippingTerms",
          "expirationDate",
          "creationDate",
          "lastModified"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "quotationNumber": {
            "type": "integer",
            "description": "Correlativo por organización. Es el number de GET /quotations/{number}/download."
          },
          "name": {
            "type": "string",
            "description": "Al crear por API queda como \"<número> - <nombre del cliente>\"."
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "ready",
              "sent",
              "accepted",
              "rejected",
              "expired",
              "invoiced",
              "canceled"
            ],
            "description": "Estado de la cotización: draft (Borrador, a medias), ready (Lista, falta enviarla), sent (Enviada), accepted (Aceptada), rejected (Rechazada), expired (Vencida), invoiced (Facturada), canceled (Anulada, terminal). Nunca se asigna libre: sigue la matriz de transiciones (ver PATCH /quotations/{id}/status). Al crear solo admite draft o ready."
          },
          "sentAt": {
            "type": "string",
            "format": "date-time",
            "description": "Último envío; ausente si nunca salió."
          },
          "sentChannel": {
            "type": "string",
            "enum": [
              "email",
              "whatsapp",
              "in_person",
              "other",
              "mcp"
            ],
            "description": "Por dónde salió la cotización."
          },
          "acceptedAt": {
            "type": "string",
            "format": "date-time"
          },
          "rejectedAt": {
            "type": "string",
            "format": "date-time"
          },
          "rejectionReason": {
            "type": "string"
          },
          "rejectionReasonCode": {
            "$ref": "#/components/schemas/QuotationUpdate/properties/reasonCode"
          },
          "contact": {
            "type": [
              "object",
              "null"
            ],
            "description": "Contacto aplanado: firstName y lastName se funden en fullName, companyName se expone como company y el resto de properties del contacto (email, mobile, phone, city, idNumber, address…) van como claves planas. Para claves repetidas gana la marcada isPrimary o, sin marca, la primera. null si el contacto ya no existe.",
            "required": [
              "id",
              "fullName",
              "company"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "fullName": {
                "type": "string"
              },
              "company": {
                "type": "string",
                "description": "Texto de la property companyName; '' si no hay."
              },
              "email": {
                "type": "string"
              },
              "mobile": {
                "type": "string"
              },
              "phone": {
                "type": "string"
              },
              "idNumber": {
                "type": "string"
              },
              "city": {
                "type": "string"
              }
            },
            "additionalProperties": true
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/QuotationItem"
            }
          },
          "subtotal": {
            "type": "number"
          },
          "taxes": {
            "type": "number",
            "description": "Monto total de impuestos (no porcentaje)."
          },
          "discounts": {
            "type": "number",
            "description": "Puede faltar en cotizaciones antiguas; la API crea con 0."
          },
          "total": {
            "type": "number"
          },
          "paymentTerms": {
            "type": "string"
          },
          "shippingTerms": {
            "type": "string"
          },
          "observaciones": {
            "type": "string"
          },
          "additionalNotes": {
            "type": "string"
          },
          "expirationDate": {
            "type": "string",
            "format": "date-time"
          },
          "creationDate": {
            "type": "string",
            "format": "date-time"
          },
          "lastModified": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "QuotationItem": {
        "type": "object",
        "description": "Ítem tal como está guardado. El backend NO recalcula nada: quantity, unitPrice, taxes y total son los que mandó el cliente (taxes se usa como porcentaje en el PDF; la fórmula vive en el cliente).",
        "required": [
          "name",
          "quantity",
          "unitPrice",
          "taxes",
          "total"
        ],
        "properties": {
          "_id": {
            "type": "string",
            "description": "ID del subdocumento; lo asigna Mongo."
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "quantity": {
            "type": "number"
          },
          "unitPrice": {
            "type": "number"
          },
          "taxes": {
            "type": "number"
          },
          "total": {
            "type": "number"
          },
          "imageUrl": {
            "type": "string",
            "description": "Puede ser '' si no se envió imagen."
          },
          "productId": {
            "type": "string",
            "description": "Referencia al producto del catálogo",
            "si el ítem se creó desde uno.": null
          }
        }
      },
      "QuotationCreate": {
        "type": "object",
        "description": "Contrato legacy en español (nació para n8n/Elementor), con alias en inglés. El número de cotización lo asigna el servidor incrementando settings.quotations.quotationNumber de la organización, y SOLO después de validar todo el body (un 400 no consume número). Términos: paymentTerms / shippingTerms del body, si no totals.forma_De_Pago / tiempo_de_entrega, si no la primera opción configurada en Ajustes > Cotizaciones; sin ninguno → 400 MISSING_QUOTATION_TERMS. expirationDate del body (yyyy-MM-dd, o un ISO que empiece por esa fecha; se guarda la medianoche UTC de ese día; otro formato → 400 INVALID_EXPIRATION_DATE) o hoy + settings.quotations.validityDays (30 si no hay) en la zona de la org. discounts 0. Si el contacto ya existe (por mobile) su nombre NO se sobrescribe con Cliente.name; solo se llena si está vacío o no existe. Nace en status ready (Lista) salvo que se pida draft; ningún otro estado se admite (400 INVALID_STATUS). Emite quotation.created solo si nace en ready.",
        "required": [
          "Cliente",
          "cotizacion",
          "totals"
        ],
        "properties": {
          "Cliente": {
            "type": "object",
            "description": "Cliente de la cotización. Se busca un contacto de la organización con esa property mobile EXACTA (sin normalizar dígitos ni prefijo): si existe, se reutiliza SIN pisar lo guardado (firstName e idNumber solo se llenan si están vacíos o no existen; el mobile no cambia porque es la clave de búsqueda); si no existe, SE CREA con estos datos (idType fijo NIT, auditado como creado por la API). Cada campo admite la clave en español con mayúscula inicial o en inglés en minúscula; la española tiene precedencia.",
            "allOf": [
              {
                "anyOf": [
                  {
                    "required": [
                      "Mobile"
                    ]
                  },
                  {
                    "required": [
                      "mobile"
                    ]
                  }
                ]
              },
              {
                "anyOf": [
                  {
                    "required": [
                      "Nombre"
                    ]
                  },
                  {
                    "required": [
                      "name"
                    ]
                  }
                ]
              }
            ],
            "properties": {
              "Nombre": {
                "type": "string",
                "description": "Se guarda como firstName."
              },
              "name": {
                "type": "string"
              },
              "Mobile": {
                "type": "string",
                "description": "Móvil tal cual; es la clave de búsqueda del contacto."
              },
              "mobile": {
                "type": "string"
              },
              "Email": {
                "type": "string"
              },
              "email": {
                "type": "string"
              },
              "Nit": {
                "type": "string",
                "description": "Se guarda como idNumber."
              },
              "id": {
                "type": "string",
                "description": "Alias de Nit (NO es el id de un contacto existente)."
              },
              "Empresa": {
                "type": "string",
                "description": "Se guarda como companyName (texto; no crea Company)."
              },
              "empresa": {
                "type": "string"
              },
              "Direccion": {
                "type": "string"
              },
              "direccion": {
                "type": "string"
              },
              "Ciudad": {
                "type": "string"
              },
              "ciudad": {
                "type": "string"
              },
              "Estado": {
                "type": "string",
                "description": "Departamento/estado."
              },
              "estado": {
                "type": "string"
              },
              "CompanyType": {
                "type": "string"
              },
              "companyType": {
                "type": "string"
              }
            },
            "additionalProperties": true
          },
          "cotizacion": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "object",
              "description": "Ítem en el formato de entrada. Acepta las claves en español (producto, cantidad, precio, iva) o en inglés (name, quantity, unitPrice, taxes); se guardan como name/description/quantity/unitPrice/taxes/total/imageUrl. Nombre, cantidad, precio y total son obligatorios y numéricos: si falta alguno responde 400 INVALID_QUOTATION_ITEMS con `fields`, ANTES de consumir número. iva/taxes vacío = 0; presente pero no numérico (\"19%\") → 400.",
              "required": [
                "total"
              ],
              "properties": {
                "producto": {
                  "type": "string",
                  "description": "Nombre del ítem."
                },
                "name": {
                  "type": "string",
                  "description": "Alias en inglés de producto."
                },
                "descripcion": {
                  "type": "string"
                },
                "description": {
                  "type": "string",
                  "description": "Alias de descripcion."
                },
                "cantidad": {
                  "type": [
                    "number",
                    "string"
                  ]
                },
                "quantity": {
                  "type": [
                    "number",
                    "string"
                  ],
                  "description": "Alias de cantidad."
                },
                "precio": {
                  "type": [
                    "number",
                    "string"
                  ],
                  "description": "Precio unitario."
                },
                "unitPrice": {
                  "type": [
                    "number",
                    "string"
                  ],
                  "description": "Alias de precio."
                },
                "iva": {
                  "type": [
                    "number",
                    "string"
                  ],
                  "description": "Impuesto del ítem (el PDF lo trata como porcentaje)."
                },
                "taxes": {
                  "type": [
                    "number",
                    "string"
                  ],
                  "description": "Alias de iva."
                },
                "total": {
                  "type": [
                    "number",
                    "string"
                  ]
                },
                "imageUrl": {
                  "type": "string"
                },
                "imagen": {
                  "type": "string",
                  "description": "Alias de imageUrl."
                }
              },
              "additionalProperties": true
            }
          },
          "totals": {
            "type": "object",
            "description": "Totales de la cotización; el backend los guarda tal cual, no los recalcula. subtotal y total son obligatorios y numéricos (400 INVALID_QUOTATION_TOTALS). iva vacío = total − subtotal. forma_De_Pago y tiempo_de_entrega (o sus variantes de mayúsculas) son la forma legacy de mandar paymentTerms y shippingTerms (ver QuotationCreate).",
            "required": [
              "subtotal",
              "total"
            ],
            "properties": {
              "subtotal": {
                "type": [
                  "number",
                  "string"
                ]
              },
              "iva": {
                "type": [
                  "number",
                  "string"
                ],
                "description": "Monto de impuestos; se guarda como taxes."
              },
              "total": {
                "type": [
                  "number",
                  "string"
                ]
              },
              "forma_De_Pago": {
                "type": "string",
                "description": "Se guarda como paymentTerms."
              },
              "forma_de_pago": {
                "type": "string",
                "description": "Alias de forma_De_Pago."
              },
              "tiempo_de_entrega": {
                "type": "string",
                "description": "Se guarda como shippingTerms."
              },
              "tiempo_De_Entrega": {
                "type": "string",
                "description": "Alias de tiempo_de_entrega."
              }
            },
            "additionalProperties": true
          },
          "paymentTerms": {
            "type": "string",
            "description": "Forma de pago de la cotización."
          },
          "shippingTerms": {
            "type": "string",
            "description": "Tiempo de entrega de la cotización."
          },
          "expirationDate": {
            "type": "string",
            "description": "Fecha de vencimiento (yyyy-MM-dd, o ISO que empiece por ella)."
          },
          "observaciones": {
            "type": "string",
            "description": "Notas; si falta se arma \"Forma de pago: … Tiempo de entrega: …\"."
          },
          "name": {
            "type": "string",
            "description": "Título; si falta, \"<número> - <cliente>\"."
          },
          "status": {
            "$ref": "#/components/schemas/RawQuotation/properties/status"
          }
        }
      },
      "QuotationUpdate": {
        "type": "object",
        "description": "Actualización parcial: solo se tocan los campos enviados; cualquier otro campo del body se ignora (no se puede cambiar contact ni quotationNumber). items REEMPLAZA el array completo. Los montos se castean a número (cadenas numéricas válidas). Un monto no numérico o un ítem incompleto fallan la validación del modelo y responden 500, no 400. status, si viene distinto del actual, pasa por la matriz de transiciones igual que PATCH /quotations/{id}/status (409 INVALID_TRANSITION si no vale) antes de tocar los demás campos. Una cotización invoiced o canceled no se edita (409 QUOTATION_LOCKED). Siempre actualiza lastModified.",
        "properties": {
          "status": {
            "$ref": "#/components/schemas/Quotation/properties/status"
          },
          "reason": {
            "type": "string",
            "description": "Motivo",
            "solo junto con status.": null
          },
          "reasonCode": {
            "type": "string",
            "enum": [
              "price",
              "timing",
              "competitor",
              "no_response",
              "other"
            ],
            "description": "Motivo de rechazo de lista corta (precio, tiempo de entrega, eligió a otro, no respondió, otro)."
          },
          "channel": {
            "$ref": "#/components/schemas/Quotation/properties/sentChannel"
          },
          "name": {
            "type": "string"
          },
          "paymentTerms": {
            "type": "string"
          },
          "shippingTerms": {
            "type": "string"
          },
          "observaciones": {
            "type": "string"
          },
          "additionalNotes": {
            "type": "string"
          },
          "expirationDate": {
            "type": "string",
            "format": "date-time"
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "Ítem para reemplazar items en PUT. name, quantity, unitPrice, taxes y total son obligatorios por validación del modelo: si falta alguno el guardado falla y la respuesta es 500, no 400. Los números pueden ir como cadena numérica (se castean).",
              "required": [
                "name",
                "quantity",
                "unitPrice",
                "taxes",
                "total"
              ],
              "properties": {
                "name": {
                  "type": "string"
                },
                "description": {
                  "type": "string"
                },
                "quantity": {
                  "type": [
                    "number",
                    "string"
                  ]
                },
                "unitPrice": {
                  "type": [
                    "number",
                    "string"
                  ]
                },
                "taxes": {
                  "type": [
                    "number",
                    "string"
                  ]
                },
                "total": {
                  "type": [
                    "number",
                    "string"
                  ]
                },
                "imageUrl": {
                  "type": "string"
                },
                "productId": {
                  "type": "string"
                }
              }
            }
          },
          "subtotal": {
            "type": [
              "number",
              "string"
            ]
          },
          "taxes": {
            "type": [
              "number",
              "string"
            ]
          },
          "discounts": {
            "type": [
              "number",
              "string"
            ]
          },
          "total": {
            "type": [
              "number",
              "string"
            ]
          }
        }
      },
      "RawQuotation": {
        "type": "object",
        "description": "Documento persistido tal cual lo devuelve el POST de creación (formato distinto del Quotation de GET/PUT): claves de Mongo (_id, __v, timestamps) y contactId poblado con el contacto crudo (properties como array). companyId nunca se setea por este camino.",
        "required": [
          "_id",
          "quotationNumber",
          "name",
          "contactId",
          "organizationId",
          "items",
          "subtotal",
          "taxes",
          "total",
          "status",
          "expirationDate",
          "creationDate"
        ],
        "properties": {
          "_id": {
            "type": "string"
          },
          "quotationNumber": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "observaciones": {
            "type": "string",
            "description": "\"Forma de pago: X. Tiempo de entrega: Y\"."
          },
          "contactId": {
            "$ref": "#/components/schemas/RawContact"
          },
          "organizationId": {
            "type": "string"
          },
          "userId": {
            "type": "string",
            "description": "Usuario dueño del API Token."
          },
          "creationDate": {
            "type": "string",
            "format": "date-time"
          },
          "expirationDate": {
            "type": "string",
            "format": "date-time"
          },
          "lastModified": {
            "type": "string",
            "format": "date-time"
          },
          "paymentTerms": {
            "type": "string"
          },
          "shippingTerms": {
            "type": "string"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/QuotationItem"
            }
          },
          "optionalItems": {
            "type": "array",
            "items": {},
            "description": "Siempre vacío al crear."
          },
          "subtotal": {
            "type": "number"
          },
          "taxes": {
            "type": "number"
          },
          "discounts": {
            "type": "number",
            "const": 0
          },
          "total": {
            "type": "number"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "ready"
            ],
            "default": "ready",
            "description": "Estado con el que nace. Por API nace en ready salvo que se pida draft."
          },
          "statusHistory": {
            "type": "array",
            "description": "Una entrada por cambio de estado; al crear trae la inicial (from null).",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "viewCount": {
            "type": "integer",
            "const": 0
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "__v": {
            "type": "integer"
          }
        },
        "additionalProperties": true
      },
      "QuotationResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/Quotation"
          }
        }
      },
      "QuotationListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination",
          "statusCounts"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Quotation"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          },
          "statusCounts": {
            "type": "object",
            "description": "Cuántas cotizaciones hay en cada estado con los MISMOS filtros del listado (search, contactId, fechas) pero sin el de status. Las viejas sin estado cuentan como draft. sent_unviewed es un subconjunto de sent; total es la suma de los estados.",
            "required": [
              "draft",
              "ready",
              "sent",
              "accepted",
              "rejected",
              "expired",
              "invoiced",
              "canceled",
              "sent_unviewed",
              "total"
            ],
            "properties": {
              "draft": {
                "type": "integer",
                "minimum": 0
              },
              "ready": {
                "type": "integer",
                "minimum": 0
              },
              "sent": {
                "type": "integer",
                "minimum": 0
              },
              "accepted": {
                "type": "integer",
                "minimum": 0
              },
              "rejected": {
                "type": "integer",
                "minimum": 0
              },
              "expired": {
                "type": "integer",
                "minimum": 0
              },
              "invoiced": {
                "type": "integer",
                "minimum": 0
              },
              "canceled": {
                "type": "integer",
                "minimum": 0
              },
              "sent_unviewed": {
                "type": "integer",
                "minimum": 0
              },
              "total": {
                "type": "integer",
                "minimum": 0
              }
            }
          }
        }
      },
      "QuotationCreatedResult": {
        "type": "object",
        "description": "Contrato legacy del POST; no incluye success.",
        "required": [
          "message",
          "data",
          "quotationNumber"
        ],
        "properties": {
          "message": {
            "type": "string",
            "const": "Cotización creada exitosamente"
          },
          "data": {
            "$ref": "#/components/schemas/RawQuotation"
          },
          "quotationNumber": {
            "type": "integer",
            "description": "El correlativo asignado (igual a data.quotationNumber)."
          }
        }
      },
      "QuotationDeleteResult": {
        "type": "object",
        "required": [
          "success",
          "message",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "message": {
            "type": "string",
            "const": "Quotation deleted successfully"
          },
          "data": {
            "type": "object",
            "required": [
              "id"
            ],
            "properties": {
              "id": {
                "type": "string"
              }
            }
          }
        }
      },
      "QuotationError": {
        "type": "object",
        "description": "Contrato de error de GET, PUT y DELETE.",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          }
        }
      },
      "QuotationCodedError": {
        "type": "object",
        "description": "Contrato de error de POST /quotations y GET /quotations/{number}/download (message + code).",
        "required": [
          "message",
          "code"
        ],
        "properties": {
          "message": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "enum": [
              "INVALID_BODY_FORMAT",
              "MISSING_REQUIRED_FIELDS",
              "INVALID_CLIENT_DATA",
              "EMPTY_QUOTATION_ITEMS",
              "ORGANIZATION_NOT_FOUND",
              "ORGANIZATION_UPDATE_FAILED",
              "MISSING_QUOTATION_NUMBER",
              "QUOTATION_NOT_FOUND",
              "INVALID_API_TOKEN",
              "INVALID_STATUS",
              "INVALID_QUOTATION_ITEMS",
              "INVALID_QUOTATION_TOTALS",
              "MISSING_QUOTATION_TERMS",
              "INVALID_EXPIRATION_DATE",
              "QUOTATION_VALIDATION_FAILED",
              "INTERNAL_SERVER_ERROR"
            ]
          },
          "fields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Campos del body que fallaron (errores de validación de POST /quotations)."
          },
          "error": {
            "description": "Solo con NODE_ENV=development; detalle del error interno."
          }
        }
      },
      "QuotationSendWhatsApp": {
        "type": "object",
        "description": "Envío por WhatsApp. Sin conversationId se usa la conversación del contacto de la cotización (enlazada al contacto o con su celular).",
        "properties": {
          "conversationId": {
            "type": "string",
            "description": "Conversación de WhatsApp de la organización."
          },
          "message": {
            "type": "string",
            "maxLength": 900,
            "description": "Pie del documento. Default \"Cotización <número> — <empresa>\"; el link público se agrega solo."
          }
        }
      },
      "QuotationSendEmail": {
        "type": "object",
        "required": [
          "to"
        ],
        "properties": {
          "to": {
            "type": "string",
            "format": "email",
            "description": "Destinatario."
          },
          "subject": {
            "type": "string",
            "description": "Default \"Cotización <número> — <empresa>\"."
          },
          "message": {
            "type": "string",
            "description": "Texto del correo (el link público se agrega solo)."
          }
        }
      },
      "QuotationSendResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "properties": {
              "statusChanged": {
                "type": "boolean",
                "description": "true si este envío la movió a sent."
              },
              "status": {
                "type": "string",
                "description": "Estado después del envío (solo WhatsApp)."
              },
              "publicUrl": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Link público /q/<token> incluido en el envío."
              },
              "messageId": {
                "type": "string",
                "description": "Id del proveedor (wamid de Meta o del correo)."
              },
              "provider": {
                "type": "string",
                "description": "Proveedor de correo (ses o brevo)."
              },
              "conversationId": {
                "type": "string",
                "description": "Conversación por la que salió (WhatsApp)."
              },
              "chatMessageId": {
                "type": "string",
                "description": "Mensaje guardado en la conversación (WhatsApp)."
              }
            }
          }
        }
      },
      "QuotationSendError": {
        "type": "object",
        "description": "Error de un envío. Los de la regla de envío traen currentStatus; los 400 de validación no traen code.",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "enum": [
              "QUOTATION_NOT_FOUND",
              "QUOTATION_CANCELED",
              "EXPIRATION_REQUIRED",
              "UNKNOWN_STATUS",
              "CONVERSATION_NOT_FOUND",
              "CONTACT_WITHOUT_MOBILE",
              "NOT_A_WHATSAPP_CONVERSATION",
              "WHATSAPP_NOT_CONNECTED",
              "WHATSAPP_OPTED_OUT",
              "WHATSAPP_WINDOW_CLOSED",
              "WHATSAPP_SEND_FAILED"
            ]
          },
          "currentStatus": {
            "type": "string"
          },
          "lastInboundAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Último mensaje del cliente (WHATSAPP_WINDOW_CLOSED)."
          }
        }
      },
      "Product": {
        "type": "object",
        "description": "Representación pública del producto (GET lista, POST, PUT). No expone organizationId, userId ni baseVariantId. Los campos de texto opcionales vuelven como '' cuando no existen; cost, categoryId y dimensions vuelven como null.",
        "required": [
          "id",
          "name",
          "description",
          "taxes",
          "cost",
          "sku",
          "categoryId",
          "imageUrl",
          "notes",
          "dimensions",
          "hasVariants",
          "variantAttributes",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "description": "'' si no tiene."
          },
          "unitPrice": {
            "type": "number",
            "description": "Precio unitario. Siempre presente en productos creados por API; puede faltar en productos legado guardados sin precio (el modelo no lo exige)."
          },
          "taxes": {
            "type": "number",
            "description": "Impuesto del producto; 0 si no se definió."
          },
          "cost": {
            "type": [
              "number",
              "null"
            ],
            "description": "Costo; null si no se definió."
          },
          "sku": {
            "type": "string",
            "description": "'' si no tiene."
          },
          "categoryId": {
            "type": [
              "string",
              "null"
            ],
            "description": "ID de categoría como texto libre (no se valida); null si no tiene."
          },
          "imageUrl": {
            "type": "string",
            "description": "'' si no tiene."
          },
          "notes": {
            "type": "string",
            "description": "'' si no tiene."
          },
          "dimensions": {
            "$ref": "#/components/schemas/ProductDimensions"
          },
          "hasVariants": {
            "type": "boolean",
            "description": "Solo se activa desde la app (variantes); la API externa no puede cambiarlo."
          },
          "variantAttributes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Nombres de los atributos de variante (Color, Talla…). [] si no hay."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ProductDetail": {
        "description": "Producto con sus variantes activas (GET /products/{id}). variants es [] cuando hasVariants es false, aunque existan variantes guardadas.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Product"
          },
          {
            "type": "object",
            "required": [
              "variants"
            ],
            "properties": {
              "variants": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ProductVariant"
                }
              }
            }
          }
        ]
      },
      "ProductVariant": {
        "type": "object",
        "description": "Variante activa del producto (solo se listan las que tienen isActive true).",
        "required": [
          "id",
          "sku",
          "price",
          "imageUrl",
          "stock",
          "attributeValues"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "sku": {
            "type": "string",
            "description": "Único por organización."
          },
          "price": {
            "type": "number"
          },
          "imageUrl": {
            "type": "string",
            "description": "'' si no tiene."
          },
          "stock": {
            "type": [
              "number",
              "null"
            ],
            "description": "null si no se lleva inventario."
          },
          "attributeValues": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProductVariantAttributeValue"
            }
          }
        }
      },
      "ProductVariantAttributeValue": {
        "type": "object",
        "description": "Valor de un atributo en una variante (p. ej. Color = Rojo).",
        "required": [
          "attributeName",
          "value",
          "displayName"
        ],
        "properties": {
          "_id": {
            "type": "string",
            "description": "ID del subdocumento; lo asigna Mongo."
          },
          "attributeName": {
            "type": "string"
          },
          "valueId": {
            "type": "string"
          },
          "value": {
            "type": "string"
          },
          "displayName": {
            "type": "string"
          },
          "price": {
            "type": "number",
            "description": "Sobreprecio del valor",
            "si aplica.": null
          },
          "imageUrl": {
            "type": "string"
          }
        },
        "additionalProperties": true
      },
      "ProductDimensions": {
        "type": [
          "object",
          "null"
        ],
        "description": "Dimensiones del producto. Se guardan tal cual se envían (width, height, length numéricos; ninguna es obligatoria). null si el producto no las tiene.",
        "properties": {
          "width": {
            "type": "number"
          },
          "height": {
            "type": "number"
          },
          "length": {
            "type": "number"
          }
        },
        "additionalProperties": true
      },
      "ProductCreate": {
        "type": "object",
        "description": "name y unitPrice son obligatorios (si falta alguno responde 400 con la lista required). unitPrice, taxes y cost admiten número o cadena numérica: unitPrice se valida con Number() (no numérico → 400); taxes y cost se castean y si el resultado no es numérico el modelo falla con 400 \"Validation error\". hasVariants y variantAttributes NO se pueden establecer por API (se ignoran). El producto queda a nombre del usuario dueño del token.",
        "required": [
          "name",
          "unitPrice"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1
          },
          "unitPrice": {
            "type": [
              "number",
              "string"
            ]
          },
          "description": {
            "type": "string"
          },
          "taxes": {
            "type": [
              "number",
              "string"
            ],
            "description": "Por defecto 0."
          },
          "cost": {
            "type": [
              "number",
              "string"
            ]
          },
          "sku": {
            "type": "string",
            "description": "No se valida unicidad."
          },
          "categoryId": {
            "type": "string",
            "description": "Texto libre; no se valida contra un catálogo."
          },
          "imageUrl": {
            "type": "string"
          },
          "notes": {
            "type": "string"
          },
          "dimensions": {
            "type": "object",
            "properties": {
              "width": {
                "type": "number"
              },
              "height": {
                "type": "number"
              },
              "length": {
                "type": "number"
              }
            },
            "additionalProperties": true
          }
        }
      },
      "ProductUpdate": {
        "type": "object",
        "description": "Actualización parcial: solo se tocan los campos enviados (name, description, unitPrice, taxes, cost, sku, categoryId, imageUrl, notes, dimensions); cualquier otro campo se ignora (hasVariants y variantAttributes no se pueden cambiar). Un valor null limpia el campo (p. ej. cost: null). Los montos se castean a número al guardar: un valor no numérico falla la validación del modelo y responde 500, NO 400 (a diferencia de POST). Siempre actualiza updatedAt.",
        "properties": {
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "unitPrice": {
            "type": [
              "number",
              "string"
            ]
          },
          "taxes": {
            "type": [
              "number",
              "string",
              "null"
            ]
          },
          "cost": {
            "type": [
              "number",
              "string",
              "null"
            ]
          },
          "sku": {
            "type": [
              "string",
              "null"
            ]
          },
          "categoryId": {
            "type": [
              "string",
              "null"
            ]
          },
          "imageUrl": {
            "type": [
              "string",
              "null"
            ]
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "dimensions": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "width": {
                "type": "number"
              },
              "height": {
                "type": "number"
              },
              "length": {
                "type": "number"
              }
            },
            "additionalProperties": true
          }
        }
      },
      "ProductResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/Product"
          }
        }
      },
      "ProductDetailResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/ProductDetail"
          }
        }
      },
      "ProductCreatedResult": {
        "type": "object",
        "required": [
          "success",
          "message",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "message": {
            "type": "string",
            "const": "Product created successfully"
          },
          "data": {
            "$ref": "#/components/schemas/Product"
          }
        }
      },
      "ProductListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Product"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "ProductError": {
        "type": "object",
        "description": "Contrato de error de los controllers de productos (success:false + error).",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          },
          "required": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Solo en \"Missing required fields\" de POST."
          },
          "details": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Solo en \"Validation error\" de POST (mensajes del modelo Mongoose)."
          }
        }
      },
      "Invoice": {
        "type": "object",
        "description": "Representación pública de la factura (copia local). No expone resolution_number, establishment_*, html_*, with_holding_tax_total, billing_reference, payments, radian_events, externalId ni organizationId/userId. payment_form y notes faltan cuando el documento no los tiene (notas crédito, entrantes).",
        "required": [
          "id",
          "number",
          "prefix",
          "fullNumber",
          "direction",
          "type_document_id",
          "date",
          "time",
          "status",
          "customer",
          "contact",
          "invoice_lines",
          "legal_monetary_totals",
          "tax_totals",
          "dian_response",
          "withholdings",
          "netPayable",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "number": {
            "type": "integer",
            "description": "Consecutivo asignado por el servidor dentro de la resolución."
          },
          "prefix": {
            "type": "string",
            "description": "Prefijo de la resolución DIAN (FS",
            "SETP…).": null
          },
          "fullNumber": {
            "type": "string",
            "description": "\"<prefix>-<number>\", p. ej. FS-623."
          },
          "direction": {
            "type": "string",
            "enum": [
              "outgoing",
              "incoming"
            ],
            "description": "outgoing = documentos emitidos por la organización (los que crea POST /invoices); incoming = facturas de proveedores recibidas por correo/IMAP. En incoming el campo customer representa al proveedor que emitió la factura."
          },
          "type_document_id": {
            "type": "integer",
            "description": "Tipo de documento local: 1 factura de venta, 91 nota crédito, 92 nota débito. Por esta API solo se crean facturas (tipo 1); las notas se listan si existen. Es el id local del CRM, no el de la tabla type_documents del proveedor.",
            "example": 1
          },
          "date": {
            "type": "string",
            "description": "Fecha de emisión YYYY-MM-DD tal como se envió a la DIAN (cadena",
            "sin hora).": null
          },
          "time": {
            "type": "string",
            "description": "Hora de emisión HH:mm:ss (cadena)."
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "sent",
              "paid",
              "overdue",
              "cancelled",
              "valido",
              "rechazado"
            ],
            "description": "Estado local. Al crear por API queda valido (DIAN StatusCode 00 e IsValid true), rechazado (StatusCode distinto de 00 o IsValid false sin haber lanzado error) o sent (el proveedor no devolvió respuesta DIAN). paid/overdue no se persisten: la cartera se deriva de los abonos en la API interna."
          },
          "customer": {
            "$ref": "#/components/schemas/InvoiceCustomer"
          },
          "contact": {
            "$ref": "#/components/schemas/InvoiceContactSummary"
          },
          "invoice_lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InvoiceLine"
            }
          },
          "legal_monetary_totals": {
            "$ref": "#/components/schemas/InvoiceLegalMonetaryTotals"
          },
          "tax_totals": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InvoiceTaxTotal"
            }
          },
          "payment_form": {
            "$ref": "#/components/schemas/InvoicePaymentForm"
          },
          "dian_response": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/InvoiceDianResponse"
              },
              {
                "type": "null"
              }
            ],
            "description": "null si el proveedor no devolvió respuesta DIAN (status sent) o en facturas entrantes."
          },
          "notes": {
            "type": "string"
          },
          "withholdings": {
            "oneOf": [
              {
                "type": "object",
                "required": [
                  "base",
                  "iva",
                  "items",
                  "byKind",
                  "total",
                  "netPayable",
                  "appliedAt",
                  "source"
                ],
                "properties": {
                  "base": {
                    "type": "number",
                    "description": "Base gravada de la factura al aplicar (tax_exclusive_amount)."
                  },
                  "iva": {
                    "type": "number",
                    "description": "IVA de la factura (ReteIVA se calcula sobre él)."
                  },
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "key",
                        "kind",
                        "amount"
                      ],
                      "properties": {
                        "key": {
                          "type": "string",
                          "description": "Concepto del catálogo",
                          "o clave heredada en registros migrados.": null
                        },
                        "kind": {
                          "type": "string",
                          "enum": [
                            "rteFuente",
                            "rteIva",
                            "rteIca"
                          ],
                          "description": "Retención en la fuente (renta), retención de IVA o retención de ICA."
                        },
                        "label": {
                          "type": "string",
                          "description": "Rótulo congelado al aplicar (no cambia si se renombra el concepto)."
                        },
                        "pct": {
                          "type": "number",
                          "description": "Porcentaje con el que se sugirió el monto",
                          "si se usó.": null
                        },
                        "amount": {
                          "type": "number",
                          "description": "Monto retenido en COP."
                        }
                      }
                    }
                  },
                  "byKind": {
                    "type": "object",
                    "required": [
                      "rteFuente",
                      "rteIva",
                      "rteIca"
                    ],
                    "properties": {
                      "rteFuente": {
                        "type": "number"
                      },
                      "rteIva": {
                        "type": "number"
                      },
                      "rteIca": {
                        "type": "number"
                      }
                    }
                  },
                  "total": {
                    "type": "number"
                  },
                  "netPayable": {
                    "type": "number",
                    "description": "payable_amount − total."
                  },
                  "appliedAt": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "appliedBy": {
                    "type": "string",
                    "description": "Usuario que las registró (id)."
                  },
                  "source": {
                    "type": "string",
                    "enum": [
                      "ui",
                      "api",
                      "migration"
                    ],
                    "description": "Desde dónde se registraron. migration = migradas del modelo anterior."
                  },
                  "legacyIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Solo en registros migrados; ids del modelo anterior."
                  }
                }
              },
              {
                "type": "null"
              }
            ],
            "description": "Retenciones que el CLIENTE practicó al pagar (retefuente, ReteIVA, ReteICA). null si no se han registrado. Se administran con PUT/DELETE /invoices/{id}/withholdings. No confundir con with_holding_tax_total (retenciones autoliquidadas en el XML DIAN), que no se expone."
          },
          "netPayable": {
            "type": "number",
            "description": "payable_amount − withholdings.total: lo que el cliente consigna de verdad. Igual a payable_amount cuando no hay retenciones. La cartera cobra sobre este valor."
          },
          "quotationId": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "Cotización de la que salió la factura (quotationId del POST); null si no viene de una."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "InvoiceCustomer": {
        "type": "object",
        "description": "Adquirente tal como se guardó (en incoming, el proveedor). identification_number es Mixed: número para clientes colombianos, cadena alfanumérica para identificadores del exterior (CIF español). Un cliente colombiano trae municipality_id; uno del exterior trae country_id + municipality_name/state_name en texto libre y no lleva dv.",
        "required": [
          "identification_number",
          "name",
          "type_document_identification_id"
        ],
        "properties": {
          "identification_number": {
            "type": [
              "number",
              "string"
            ]
          },
          "name": {
            "type": "string"
          },
          "type_document_identification_id": {
            "type": "integer",
            "description": "Id del catálogo del proveedor (no el código DIAN): 1 registro civil, 2 tarjeta de identidad, 3 cédula, 4 tarjeta de extranjería, 5 cédula de extranjería, 6 NIT, 7 pasaporte, 8 documento extranjero, 9 NIT de otro país, 10 NUIP, 11 PEP, 12 PPT. 7, 8 y 9 identifican no residentes."
          },
          "dv": {
            "type": [
              "string",
              "number"
            ],
            "description": "Dígito de verificación del NIT colombiano. Ausente en no residentes."
          },
          "phone": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "merchant_registration": {
            "type": "string"
          },
          "type_organization_id": {
            "type": "integer",
            "description": "1 persona jurídica",
            "2 persona natural.": null
          },
          "type_liability_id": {
            "type": "integer"
          },
          "type_regime_id": {
            "type": "integer"
          },
          "municipality_id": {
            "type": "integer",
            "description": "Municipio colombiano (id del catálogo del proveedor, no el código DANE). Si se omite y el cliente existe en el CRM (idNumber = identification_number) con cityCode DANE válido, se completa desde ahí."
          },
          "country_id": {
            "type": "integer",
            "description": "País del catálogo del proveedor; ausente = Colombia (46)."
          },
          "municipality_name": {
            "type": "string",
            "description": "Ciudad en texto libre (solo exterior)."
          },
          "state_name": {
            "type": "string",
            "description": "Provincia/estado en texto libre (solo exterior)."
          }
        },
        "additionalProperties": true
      },
      "InvoiceCustomerInput": {
        "type": "object",
        "description": "Adquirente en el formato del proveedor de facturación. El controller solo exige identification_number; name y type_document_identification_id son obligatorios del modelo local y del proveedor (si faltan, el proveedor responde 422 o el guardado local falla con 500 DESPUÉS de emitir). Si dv viene vacío y el tipo NO es de no residente (7, 8, 9), el servidor lo calcula desde identification_number; un identificador no numérico con tipo colombiano hace que el cálculo lance y responda 500. Sin email, el servidor apaga sendmail y send_customer_credentials antes de llamar al proveedor. El contacto del CRM se asocia buscando la property idNumber igual al identification_number, comparando sin puntos, espacios ni DV (con el tipo del payload una cédula de 10 dígitos no pierde su último dígito); si no hay match, contact queda null (no se crea contacto).",
        "required": [
          "identification_number"
        ],
        "properties": {
          "identification_number": {
            "type": [
              "number",
              "string"
            ]
          },
          "name": {
            "type": "string"
          },
          "type_document_identification_id": {
            "type": "integer"
          },
          "dv": {
            "type": [
              "string",
              "number"
            ]
          },
          "phone": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "description": "Si falta",
            "el proveedor no envía el documento al cliente.": null
          },
          "merchant_registration": {
            "type": "string"
          },
          "type_organization_id": {
            "type": "integer"
          },
          "type_liability_id": {
            "type": "integer"
          },
          "type_regime_id": {
            "type": "integer"
          },
          "municipality_id": {
            "type": "integer"
          },
          "country_id": {
            "type": "integer"
          },
          "municipality_name": {
            "type": "string"
          },
          "state_name": {
            "type": "string"
          }
        },
        "additionalProperties": true
      },
      "InvoiceLine": {
        "type": "object",
        "description": "Línea tal como se guardó (formato del proveedor). El backend no recalcula nada.",
        "required": [
          "unit_measure_id",
          "invoiced_quantity",
          "line_extension_amount",
          "free_of_charge_indicator",
          "description",
          "type_item_identification_id",
          "price_amount",
          "base_quantity"
        ],
        "properties": {
          "unit_measure_id": {
            "type": "integer",
            "description": "Unidad de medida del catálogo del proveedor (70 = unidad)."
          },
          "invoiced_quantity": {
            "type": "string"
          },
          "line_extension_amount": {
            "type": "string",
            "description": "Subtotal de la línea sin impuestos."
          },
          "free_of_charge_indicator": {
            "type": "boolean"
          },
          "allowance_charges": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "Descuento o cargo de la línea. charge_indicator false = descuento.",
              "required": [
                "charge_indicator",
                "allowance_charge_reason",
                "amount",
                "base_amount"
              ],
              "properties": {
                "charge_indicator": {
                  "type": "boolean"
                },
                "allowance_charge_reason": {
                  "type": "string"
                },
                "amount": {
                  "type": "string"
                },
                "base_amount": {
                  "type": "string"
                }
              }
            }
          },
          "tax_totals": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InvoiceTaxTotal"
            }
          },
          "description": {
            "type": "string"
          },
          "notes": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "type_item_identification_id": {
            "type": "integer",
            "description": "Tipo de identificación del ítem (4 = estándar)."
          },
          "price_amount": {
            "type": "string"
          },
          "base_quantity": {
            "type": "string"
          }
        }
      },
      "InvoiceLineInput": {
        "type": "object",
        "description": "Línea en el formato del proveedor. El controller solo valida que invoice_lines no esté vacío: los campos marcados obligatorios lo son del modelo local y del proveedor. Un faltante lo rechaza el proveedor (422, número quemado) o, si el proveedor lo acepta, falla el guardado local con 500 después de emitir.",
        "required": [
          "unit_measure_id",
          "invoiced_quantity",
          "line_extension_amount",
          "free_of_charge_indicator",
          "description",
          "type_item_identification_id",
          "price_amount",
          "base_quantity"
        ],
        "properties": {
          "unit_measure_id": {
            "type": "integer"
          },
          "invoiced_quantity": {
            "type": [
              "string",
              "number"
            ]
          },
          "line_extension_amount": {
            "type": [
              "string",
              "number"
            ]
          },
          "free_of_charge_indicator": {
            "type": "boolean"
          },
          "allowance_charges": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "charge_indicator",
                "allowance_charge_reason",
                "amount",
                "base_amount"
              ],
              "properties": {
                "charge_indicator": {
                  "type": "boolean"
                },
                "allowance_charge_reason": {
                  "type": "string"
                },
                "amount": {
                  "type": [
                    "string",
                    "number"
                  ]
                },
                "base_amount": {
                  "type": [
                    "string",
                    "number"
                  ]
                }
              }
            }
          },
          "tax_totals": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InvoiceTaxTotalInput"
            }
          },
          "description": {
            "type": "string"
          },
          "notes": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "type_item_identification_id": {
            "type": "integer"
          },
          "price_amount": {
            "type": [
              "string",
              "number"
            ]
          },
          "base_quantity": {
            "type": [
              "string",
              "number"
            ]
          }
        }
      },
      "InvoiceTaxTotal": {
        "type": "object",
        "description": "Impuesto o retención. tax_id 1 = IVA; 21/22/23 son ReteFuente/ReteIVA/ReteICA (retenciones).",
        "required": [
          "tax_id",
          "tax_amount",
          "percent",
          "taxable_amount"
        ],
        "properties": {
          "tax_id": {
            "type": "integer"
          },
          "tax_amount": {
            "type": "string"
          },
          "percent": {
            "type": "string"
          },
          "taxable_amount": {
            "type": "string"
          }
        }
      },
      "InvoiceTaxTotalInput": {
        "type": "object",
        "description": "Los montos se guardan como cadena; un número se convierte al guardar. Los cuatro campos son obligatorios del modelo local.",
        "required": [
          "tax_id",
          "tax_amount",
          "percent",
          "taxable_amount"
        ],
        "properties": {
          "tax_id": {
            "type": "integer"
          },
          "tax_amount": {
            "type": [
              "string",
              "number"
            ]
          },
          "percent": {
            "type": [
              "string",
              "number"
            ]
          },
          "taxable_amount": {
            "type": [
              "string",
              "number"
            ]
          }
        }
      },
      "InvoiceLegalMonetaryTotals": {
        "type": "object",
        "required": [
          "line_extension_amount",
          "tax_exclusive_amount",
          "tax_inclusive_amount",
          "payable_amount"
        ],
        "properties": {
          "line_extension_amount": {
            "type": "string"
          },
          "tax_exclusive_amount": {
            "type": "string"
          },
          "tax_inclusive_amount": {
            "type": "string"
          },
          "allowance_total_amount": {
            "type": "string"
          },
          "payable_amount": {
            "type": "string"
          }
        }
      },
      "InvoiceLegalMonetaryTotalsInput": {
        "type": "object",
        "description": "Totales en el formato del proveedor; el backend no los recalcula. El controller solo valida payable_amount (numérico y mayor que 0); los otros tres son obligatorios del modelo local.",
        "required": [
          "payable_amount"
        ],
        "properties": {
          "line_extension_amount": {
            "type": [
              "string",
              "number"
            ]
          },
          "tax_exclusive_amount": {
            "type": [
              "string",
              "number"
            ]
          },
          "tax_inclusive_amount": {
            "type": [
              "string",
              "number"
            ]
          },
          "allowance_total_amount": {
            "type": [
              "string",
              "number"
            ]
          },
          "payable_amount": {
            "type": [
              "string",
              "number"
            ]
          }
        }
      },
      "InvoicePaymentForm": {
        "type": "object",
        "required": [
          "payment_form_id",
          "payment_method_id",
          "payment_due_date"
        ],
        "properties": {
          "payment_form_id": {
            "type": "integer",
            "description": "1 contado",
            "2 crédito.": null
          },
          "payment_method_id": {
            "type": "integer",
            "description": "Medio de pago del catálogo del proveedor (10 = efectivo",
            "47 = transferencia…).": null
          },
          "payment_due_date": {
            "type": "string",
            "description": "YYYY-MM-DD."
          },
          "duration_measure": {
            "type": "string",
            "description": "Días de plazo",
            "como cadena.": null
          }
        }
      },
      "InvoicePaymentFormInput": {
        "type": "object",
        "description": "Opcional. Si payment_form_id falta o no es 1/2, el servidor lo deriva: vencimiento posterior a date = crédito (2), si no contado (1). payment_method_id y payment_due_date son obligatorios del modelo local cuando se envía el objeto.",
        "required": [
          "payment_method_id",
          "payment_due_date"
        ],
        "properties": {
          "payment_form_id": {
            "type": [
              "integer",
              "string"
            ]
          },
          "payment_method_id": {
            "type": "integer"
          },
          "payment_due_date": {
            "type": "string"
          },
          "duration_measure": {
            "type": [
              "string",
              "number"
            ]
          }
        }
      },
      "InvoiceDianResponse": {
        "type": "object",
        "description": "Respuesta de la DIAN extraída de SendBillSyncResult. Además de las claves normalizadas en camelCase, se guardan tal cual las claves originales del proveedor (IsValid, StatusCode, StatusDescription, StatusMessage, ResponseList…), por eso aparecen duplicadas en dos casings.",
        "properties": {
          "isValid": {
            "type": "string",
            "description": "\"true\" o \"false\" como cadena."
          },
          "statusCode": {
            "type": "string",
            "description": "\"00\" = aceptada."
          },
          "statusDescription": {
            "type": "string"
          },
          "statusMessage": {
            "type": "string"
          },
          "cufe": {
            "type": "string",
            "description": "Código único de factura electrónica; identifica el documento ante la DIAN."
          },
          "qr": {
            "type": "string",
            "description": "URL del QR de validación de la DIAN."
          }
        },
        "additionalProperties": true
      },
      "InvoiceContactSummary": {
        "type": [
          "object",
          "null"
        ],
        "description": "Contacto del CRM asociado (customerId), aplanado: firstName y lastName se funden en fullName, companyName se expone como company y el resto de properties (email, mobile, phone, city, idType, idNumber, dv…) van como claves planas. Para claves repetidas gana la marcada isPrimary o, sin marca, la primera. null si la factura no tiene contacto asociado o este ya no existe — es lo habitual en facturas creadas por API, ver InvoiceCustomerInput.",
        "required": [
          "id",
          "fullName",
          "company"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "fullName": {
            "type": "string"
          },
          "company": {
            "type": "string",
            "description": "Texto de la property companyName; '' si no hay."
          },
          "email": {
            "type": "string"
          },
          "mobile": {
            "type": "string"
          },
          "phone": {
            "type": "string"
          },
          "idType": {
            "type": "string",
            "description": "Código DIAN del tipo de documento (31 NIT",
            "13 cédula…).": null
          },
          "idNumber": {
            "type": "string",
            "description": "Número de documento sin puntos ni DV."
          },
          "dv": {
            "type": "string",
            "description": "DV del NIT (solo con idType 31)."
          },
          "city": {
            "type": "string"
          }
        },
        "additionalProperties": true
      },
      "InvoiceCreate": {
        "type": "object",
        "description": "Factura de venta en el formato del proveedor de facturación electrónica (APIDIAN / Facturalatam, endpoint /ubl2.1/invoice); el backend la reenvía casi tal cual. Campos que el servidor decide o completa: number lo asigna SIEMPRE el servidor (reserva atómica del consecutivo de la resolución; el enviado se ignora); prefix y resolution_number se completan desde la configuración de facturación si faltan; establishment_* se completan desde companyInfo si faltan; html_header/html_buttons/html_footer se SOBRESCRIBEN con el correo estándar de la organización; type_document_id se guarda tal cual (enviar 1). Cualquier otro campo del formato del proveedor (annexes, head_note, foot_note, with_holding_tax_total, sendmail, sendmailtome, send_customer_credentials, disable_confirmation_text…) pasa al proveedor y se guarda en la copia local.",
        "required": [
          "customer",
          "invoice_lines",
          "legal_monetary_totals"
        ],
        "properties": {
          "type_document_id": {
            "type": "integer",
            "description": "1 = factura de venta. No usar esta operación para notas."
          },
          "date": {
            "type": "string",
            "description": "YYYY-MM-DD. Obligatoria para el proveedor y el modelo local (el controller no la valida)."
          },
          "time": {
            "type": "string",
            "description": "HH:mm:ss. Obligatoria para el proveedor y el modelo local (el controller no la valida)."
          },
          "prefix": {
            "type": "string",
            "description": "Se completa con el de la resolución configurada si falta."
          },
          "resolution_number": {
            "type": "string",
            "description": "Se completa con el de la resolución configurada si falta."
          },
          "number": {
            "type": "integer",
            "description": "Ignorado; el servidor asigna el consecutivo."
          },
          "notes": {
            "type": "string"
          },
          "customer": {
            "$ref": "#/components/schemas/InvoiceCustomerInput"
          },
          "invoice_lines": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/InvoiceLineInput"
            }
          },
          "legal_monetary_totals": {
            "$ref": "#/components/schemas/InvoiceLegalMonetaryTotalsInput"
          },
          "tax_totals": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InvoiceTaxTotalInput"
            },
            "description": "Impuestos del documento. El modelo lo exige como array (puede ir vacío)."
          },
          "with_holding_tax_total": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InvoiceTaxTotalInput"
            },
            "description": "Retenciones. Se guardan pero no se exponen en la respuesta."
          },
          "payment_form": {
            "$ref": "#/components/schemas/InvoicePaymentFormInput"
          },
          "establishment_name": {
            "type": "string"
          },
          "establishment_address": {
            "type": "string"
          },
          "establishment_phone": {
            "type": "string"
          },
          "establishment_municipality": {
            "type": "integer"
          },
          "establishment_email": {
            "type": "string"
          },
          "sendmail": {
            "type": "boolean",
            "description": "Que el proveedor envíe el documento al cliente por correo. Se apaga si el cliente no tiene email."
          },
          "sendmailtome": {
            "type": "boolean",
            "description": "Copia del correo al emisor."
          },
          "send_customer_credentials": {
            "type": "boolean",
            "description": "Se apaga si el cliente no tiene email."
          },
          "disable_confirmation_text": {
            "type": "boolean"
          },
          "head_note": {
            "type": "string"
          },
          "foot_note": {
            "type": "string"
          },
          "quotationId": {
            "type": "string",
            "description": "Opcional. _id de la cotización de la que sale la factura (Épica CO). Se valida ANTES de reservar consecutivo y de llamar al proveedor: debe existir en la misma organización y no estar anulada ni rechazada; si no, 400 QUOTATION_NOT_FOUND / QUOTATION_CANCELED / QUOTATION_REJECTED / INVALID_QUOTATION_ID sin emitir nada. No viaja al proveedor. Si la factura queda válida ante la DIAN, la cotización pasa a Facturada (con la Aceptada implícita si no lo estaba, actor system) y la factura se suma a sus invoiceIds. Si después una nota crédito anula la última factura válida, la cotización vuelve a Aceptada."
          }
        },
        "additionalProperties": true
      },
      "InvoiceResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/Invoice"
          }
        }
      },
      "InvoiceListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Invoice"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "InvoiceCreatedResult": {
        "type": "object",
        "required": [
          "success",
          "message",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "message": {
            "type": "string",
            "const": "Invoice created successfully"
          },
          "data": {
            "$ref": "#/components/schemas/Invoice"
          }
        }
      },
      "InvoiceError": {
        "type": "object",
        "description": "Contrato de error de los controllers de facturas (success:false + error).",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string",
            "description": "Solo en el 500 genérico del POST; detalle del error interno."
          }
        }
      },
      "InvoiceNumberingError": {
        "type": "object",
        "description": "Error al reservar el consecutivo antes de llamar al proveedor (no se quema ningún número). 400 INVOICE_CONFIG_MISSING si la organización no tiene configuración de facturación; 409 RESOLUTION_EXHAUSTED si el siguiente número supera el tope de la resolución (trae next y to).",
        "required": [
          "success",
          "error",
          "code"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "enum": [
              "INVOICE_CONFIG_MISSING",
              "RESOLUTION_EXHAUSTED"
            ]
          },
          "next": {
            "type": "integer",
            "description": "Solo en RESOLUTION_EXHAUSTED; el número que se habría asignado."
          },
          "to": {
            "type": "integer",
            "description": "Solo en RESOLUTION_EXHAUSTED; tope autorizado de la resolución."
          }
        }
      },
      "InvoiceProviderError": {
        "type": "object",
        "description": "El proveedor de facturación rechazó o no pudo procesar el documento. El código HTTP es el que devolvió el proveedor (422 validación, 401 token de facturación inválido, …) o 500 si fue error de red, fallo lógico (success:false del proveedor, p. ej. certificado vencido) o rechazo de la DIAN (IsValid false, error \"Invalid invoice: …\"). El consecutivo reservado se QUEMA (no se reutiliza) y se devuelve en burnedNumber; queda un registro InvoiceNumberAttempt para justificar el hueco. No se guarda copia local.",
        "required": [
          "success",
          "error",
          "burnedNumber"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string",
            "description": "\"API Error (<status>): <mensaje>\", \"Invalid invoice: …\", \"Facturación: …\" o \"Network error: …\"."
          },
          "burnedNumber": {
            "type": "object",
            "required": [
              "prefix",
              "number"
            ],
            "properties": {
              "prefix": {
                "type": "string"
              },
              "number": {
                "type": "integer"
              }
            }
          },
          "errors": {
            "description": "Errores de validación del proveedor (objeto campo → mensajes)",
            "cuando los devuelve.": null
          },
          "details": {
            "type": "string",
            "description": "message del cuerpo de error del proveedor",
            "cuando existe.": null
          }
        }
      },
      "WhatsAppSendRequest": {
        "type": "object",
        "description": "Mensaje de sesión (ventana de 24 h) por la Cloud API de Meta. Un solo tipo por request; el texto va en message y los medios en mediaUrl (URL pública directa: Meta la descarga; las URLs de drive.google.com se rechazan con 400).",
        "required": [
          "to"
        ],
        "properties": {
          "to": {
            "type": "string",
            "description": "Número destino en formato internacional con código de país y sin '+' (ej. 573001234567). Se envía a Meta tal cual y se usa literalmente para buscar la conversación por participants.contact.reference, así que conviene el mismo formato que guardan los mensajes entrantes (solo dígitos).",
            "example": "573001234567"
          },
          "organizationId": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$",
            "description": "Opcional. La organización siempre es la del token de API; si se envía y no coincide, 403 (el tool MCP send_whatsapp_message lo rellena solo con la del token)."
          },
          "type": {
            "type": "string",
            "enum": [
              "text",
              "image",
              "document",
              "audio",
              "video"
            ],
            "default": "text",
            "description": "Se normaliza a minúsculas y sin espacios antes de evaluarse."
          },
          "message": {
            "type": "string",
            "description": "Texto del mensaje. Obligatorio (y no vacío) cuando type es text; en medios se ignora."
          },
          "mediaUrl": {
            "type": "string",
            "format": "uri",
            "description": "URL pública del archivo. Obligatoria para image, document, audio y video."
          },
          "caption": {
            "type": "string",
            "description": "Pie para image, document y video. Se guarda como texto del mensaje en el CRM; si falta se guarda \"[Imagen]\", \"[Documento]\" o \"[Video]\"."
          },
          "replyToMessageId": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$",
            "description": "_id (Mongo) de un mensaje de la misma organización al que se responde. Se manda a Meta como context.message_id (cita) y el mensaje guardado lleva replyToMessage y replyToSnapshot. 404 si no existe; 400 si el original no tiene wamid."
          }
        }
      },
      "WhatsAppSendResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "message"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "message": {
            "type": "string",
            "const": "Mensaje enviado exitosamente"
          },
          "data": {
            "type": "object",
            "required": [
              "messageId",
              "conversationId",
              "message"
            ],
            "properties": {
              "messageId": {
                "type": "string",
                "description": "wamid asignado por Meta."
              },
              "conversationId": {
                "type": "string",
                "description": "Conversación existente o recién creada para ese número."
              },
              "message": {
                "$ref": "#/components/schemas/Message"
              }
            }
          }
        }
      },
      "WhatsAppTemplateParameter": {
        "type": "object",
        "description": "Parámetro de un componente, en el formato oficial de Meta (text, currency, date_time, image, document, video, payload…).",
        "required": [
          "type"
        ],
        "properties": {
          "type": {
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "payload": {
            "type": "string"
          },
          "currency": {
            "type": "object",
            "additionalProperties": true
          },
          "date_time": {
            "type": "object",
            "additionalProperties": true
          },
          "image": {
            "type": "object",
            "additionalProperties": true
          },
          "document": {
            "type": "object",
            "additionalProperties": true
          },
          "video": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "additionalProperties": true
      },
      "WhatsAppTemplateComponent": {
        "type": "object",
        "description": "Componente con parámetros, en el formato oficial de Meta (se reenvía sin transformar). Para el texto guardado en el CRM solo se leen los parameters del componente body, en orden posicional, para sustituir {{1}}, {{2}}… del BODY de la plantilla.",
        "required": [
          "type"
        ],
        "properties": {
          "type": {
            "type": "string",
            "description": "header",
            "body o button.": null
          },
          "sub_type": {
            "type": "string",
            "description": "Solo en button (quick_reply",
            "url).": null
          },
          "index": {
            "type": [
              "integer",
              "string"
            ],
            "description": "Solo en button."
          },
          "parameters": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WhatsAppTemplateParameter"
            }
          }
        },
        "additionalProperties": true
      },
      "WhatsAppOutboundContact": {
        "type": "object",
        "description": "Segmentación del contacto. Si no existe un contacto con ese móvil se crea SIEMPRE (mínimo con mobile) y estos campos se fijan al crear o se actualizan en el existente. Los valores vacíos se ignoran; cualquier otra clave se descarta.",
        "properties": {
          "firstName": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "company": {
            "type": "string"
          },
          "companyType": {
            "type": "string"
          },
          "source": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "position": {
            "type": "string"
          },
          "idNumber": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "notes": {
            "type": "string"
          }
        }
      },
      "WhatsAppSendTemplateRequest": {
        "type": "object",
        "required": [
          "to",
          "templateName"
        ],
        "properties": {
          "to": {
            "type": "string",
            "minLength": 10,
            "pattern": "^[^+A-Za-z]*$",
            "description": "Número con código de país, sin '+' y sin letras; se conservan solo los dígitos y deben ser al menos 10 (400 INVALID_PHONE si no).",
            "example": "573001234567"
          },
          "templateName": {
            "type": "string",
            "description": "Nombre exacto de la plantilla aprobada en Meta (ver GET /whatsapp/templates)."
          },
          "language": {
            "type": "string",
            "default": "es",
            "description": "Código de idioma de la plantilla tal como está en Meta (es, es_CO, en_US…)."
          },
          "components": {
            "type": "array",
            "description": "Variables de la plantilla en formato Meta. Se omite del payload si va vacío.",
            "items": {
              "$ref": "#/components/schemas/WhatsAppTemplateComponent"
            }
          },
          "stageId": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$",
            "description": "Etapa inicial si se CREA la conversación; debe pertenecer al pipeline default de conversaciones (400 INVALID_STAGE si no, validado antes de enviar). Si la conversación ya existía se ignora y se avisa en warnings."
          },
          "assignTo": {
            "type": "string",
            "enum": [
              "human",
              "agent"
            ],
            "default": "human",
            "description": "Quién atiende una conversación NUEVA: human (dueño del token) o agent (el agente IA con auto-asignación activa, que responderá a todo lo entrante, incluidos autorespondedores). No cambia la asignación de una conversación existente."
          },
          "contact": {
            "$ref": "#/components/schemas/WhatsAppOutboundContact"
          }
        }
      },
      "WhatsAppSendTemplateResult": {
        "type": "object",
        "required": [
          "success",
          "message",
          "data",
          "warnings"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "message": {
            "type": "string",
            "const": "Plantilla enviada exitosamente"
          },
          "data": {
            "type": "object",
            "required": [
              "to",
              "templateName",
              "language",
              "contactFound",
              "contactId",
              "contactCreated",
              "contactUpdatedFields",
              "contactMissingFields",
              "marketingEventLogged",
              "conversationMessageLogged",
              "conversation"
            ],
            "properties": {
              "messageId": {
                "type": "string",
                "description": "wamid asignado por Meta. Ausente si Meta aceptó sin devolver id."
              },
              "to": {
                "type": "string",
                "description": "Número normalizado (solo dígitos) al que se envió."
              },
              "templateName": {
                "type": "string"
              },
              "language": {
                "type": "string"
              },
              "contactFound": {
                "type": "boolean",
                "description": "true si al final del proceso hay un contacto ligado al número (creado o existente)."
              },
              "contactId": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "contactCreated": {
                "type": "boolean"
              },
              "contactUpdatedFields": {
                "type": "array",
                "description": "Claves del bloque contact que se fijaron o cambiaron.",
                "items": {
                  "type": "string"
                }
              },
              "contactMissingFields": {
                "type": "array",
                "description": "Campos de segmentación que siguen vacíos en el contacto.",
                "items": {
                  "type": "string",
                  "enum": [
                    "firstName",
                    "source",
                    "companyType",
                    "company",
                    "city"
                  ]
                }
              },
              "marketingEventLogged": {
                "type": "boolean",
                "description": "Se registró el MarketingEvent whatsapp_sent en el contacto."
              },
              "conversationMessageLogged": {
                "type": "boolean",
                "description": "El envío quedó como mensaje outgoing en la conversación del chat."
              },
              "conversation": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "null si no se pudo registrar la conversación (el envío ya ocurrió igual).",
                "required": [
                  "id",
                  "created",
                  "stageId",
                  "stageName",
                  "assignedAgent"
                ],
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "created": {
                    "type": "boolean"
                  },
                  "stageId": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "stageName": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "assignedAgent": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "required": [
                      "id",
                      "name"
                    ],
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "name": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "description": "Avisos accionables en texto (contacto sin segmentación, conversación nueva y su etapa, agente IA asignado, stageId ignorado, registro fallido).",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "WhatsAppTemplate": {
        "type": "object",
        "description": "Plantilla tal como la devuelve la Graph API de Meta (GET /{waba-id}/message_templates), sin transformar.",
        "required": [
          "name",
          "language",
          "status",
          "category",
          "components"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "language": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "description": "APPROVED",
            "PENDING": null,
            "REJECTED": null,
            "PAUSED": null,
            "DISABLED… Solo las APPROVED se pueden enviar.": null
          },
          "category": {
            "type": "string",
            "description": "MARKETING",
            "UTILITY o AUTHENTICATION.": null
          },
          "parameter_format": {
            "type": "string",
            "description": "POSITIONAL ({{1}}) o NAMED."
          },
          "components": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "type"
              ],
              "properties": {
                "type": {
                  "type": "string",
                  "description": "HEADER",
                  "BODY": null,
                  "FOOTER o BUTTONS.": null
                },
                "format": {
                  "type": "string",
                  "description": "En HEADER; TEXT",
                  "IMAGE": null,
                  "VIDEO": null,
                  "DOCUMENT.": null
                },
                "text": {
                  "type": "string"
                },
                "example": {
                  "type": "object",
                  "additionalProperties": true
                },
                "buttons": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "additionalProperties": true
                  }
                }
              },
              "additionalProperties": true
            }
          }
        },
        "additionalProperties": true
      },
      "WhatsAppTemplateListResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "description": "Respuesta cruda de Meta; la paginación (paging.cursors) es la de Meta y esta API no la reexpone como parámetros.",
            "required": [
              "data"
            ],
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/WhatsAppTemplate"
                }
              },
              "paging": {
                "type": "object",
                "properties": {
                  "cursors": {
                    "type": "object",
                    "properties": {
                      "before": {
                        "type": "string"
                      },
                      "after": {
                        "type": "string"
                      }
                    }
                  },
                  "next": {
                    "type": "string"
                  }
                },
                "additionalProperties": true
              }
            },
            "additionalProperties": true
          }
        }
      },
      "WhatsAppError": {
        "type": "object",
        "description": "Errores de este módulo: siempre success:false y message. code solo en send-template y templates. error/details llevan la respuesta cruda de Meta cuando el fallo viene de la Cloud API.",
        "required": [
          "success",
          "message"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "message": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "hint": {
            "type": "string"
          },
          "suggestion": {
            "type": "string"
          },
          "error": {
            "description": "Cuerpo de error de Meta (objeto error con message, type, code…) o texto de la excepción."
          },
          "details": {
            "description": "Contexto adicional (ventana de 24 h, status/url/payload enviados a Meta)."
          },
          "contactFound": {
            "type": "boolean",
            "description": "Solo en META_API_ERROR de send-template; si true se registró whatsapp_failed en el contacto."
          },
          "debug": {}
        }
      },
      "Project": {
        "type": "object",
        "description": "Representación pública del proyecto. No expone organizationId ni ownerId crudo (se reemplaza por owner poblado). description vuelve como '' cuando no existe; status, color, budget, startDate, endDate y owner vuelven como null.",
        "required": [
          "id",
          "name",
          "description",
          "status",
          "color",
          "budget",
          "startDate",
          "endDate",
          "owner",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "description": "'' si no tiene."
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "description": "Texto libre; la API no valida valores (no hay enum)."
          },
          "color": {
            "type": [
              "string",
              "null"
            ],
            "description": "Texto libre (p. ej. '#3b82f6'); no se valida."
          },
          "budget": {
            "type": [
              "number",
              "null"
            ]
          },
          "startDate": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "endDate": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "owner": {
            "$ref": "#/components/schemas/UserSummary",
            "description": "Usuario dueño del proyecto (el del token en los creados por API)."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ProjectDetail": {
        "description": "Proyecto con el conteo de sus tareas (solo GET /projects/{id}).",
        "allOf": [
          {
            "$ref": "#/components/schemas/Project"
          },
          {
            "type": "object",
            "required": [
              "taskCount"
            ],
            "properties": {
              "taskCount": {
                "type": "integer",
                "minimum": 0,
                "description": "Tareas de la organización con projectId igual a este proyecto."
              }
            }
          }
        ]
      },
      "ProjectCreate": {
        "type": "object",
        "description": "name es obligatorio (se recorta con trim; vacío → 400). startDate/endDate se validan con new Date(): cualquier valor que Date acepte pasa (incluye '2026-09-30' sin hora); si ambas vienen, startDate no puede ser posterior a endDate. budget se convierte con Number(): null o '' se guardan como 0 (Number(null) es 0), negativos o no numéricos responden 400. status y color son texto libre sin validación. El proyecto queda a nombre del usuario dueño del token (owner).",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1
          },
          "description": {
            "type": "string"
          },
          "startDate": {
            "type": "string",
            "description": "Cualquier cadena parseable por Date (con o sin hora)."
          },
          "endDate": {
            "type": "string",
            "description": "Cualquier cadena parseable por Date (con o sin hora)."
          },
          "budget": {
            "type": [
              "number",
              "string"
            ],
            "description": "Número o cadena numérica ≥ 0."
          },
          "status": {
            "type": "string",
            "description": "Texto libre."
          },
          "color": {
            "type": "string",
            "description": "Texto libre."
          }
        }
      },
      "ProjectUpdate": {
        "type": "object",
        "description": "Actualización parcial: solo se tocan name, description, startDate, endDate, budget, status y color; cualquier otro campo se ignora, y si no llega ninguno de esos responde 400. name no puede quedar vacío. null limpia startDate, endDate, budget, status o color (null pasa las validaciones: la de fecha solo corre con valor truthy y Number(null) es 0). La comparación startDate ≤ endDate solo se hace entre los valores ENVIADOS, no contra los guardados: se puede dejar un proyecto con startDate posterior a su endDate actualizando una sola fecha.",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "startDate": {
            "type": [
              "string",
              "null"
            ],
            "description": "Cualquier cadena parseable por Date; null limpia."
          },
          "endDate": {
            "type": [
              "string",
              "null"
            ],
            "description": "Cualquier cadena parseable por Date; null limpia."
          },
          "budget": {
            "type": [
              "number",
              "string",
              "null"
            ]
          },
          "status": {
            "type": [
              "string",
              "null"
            ]
          },
          "color": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "ProjectResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/Project"
          }
        }
      },
      "ProjectDetailResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/ProjectDetail"
          }
        }
      },
      "ProjectListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Project"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "ProjectDeleteResult": {
        "type": "object",
        "description": "No devuelve el id borrado, solo cuántas tareas se eliminaron en cascada.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "deletedTasksCount"
            ],
            "properties": {
              "deletedTasksCount": {
                "type": "integer",
                "minimum": 0
              }
            }
          }
        }
      },
      "ProjectError": {
        "type": "object",
        "description": "Contrato de error de los controllers de proyectos (success:false + error, mensajes de validación en español).",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          }
        }
      },
      "UserSummary": {
        "type": [
          "object",
          "null"
        ],
        "description": "Usuario referenciado (owner del proyecto, responsable de la tarea). Se rellena con populate; null si el documento no tiene referencia o el usuario ya no existe.",
        "required": [
          "id",
          "firstName",
          "lastName",
          "email"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "firstName": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "email": {
            "type": "string"
          }
        }
      },
      "Task": {
        "type": "object",
        "description": "Representación pública de la tarea. No expone organizationId, ownerId ni dueDateReminderNotifiedAt. notes, description, timeline vuelven como null cuando no existen; budget null; tags [].",
        "required": [
          "id",
          "title",
          "status",
          "priority",
          "dueDate",
          "notes",
          "description",
          "budget",
          "timeline",
          "tags",
          "project",
          "responsible",
          "contact",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "description": "Uno de No Iniciado, En Progreso, Revisión, Completado en tareas gestionadas por API (el modelo no lo restringe, tareas legado pueden traer otro texto)."
          },
          "priority": {
            "type": "string",
            "enum": [
              "Alta",
              "Media",
              "Baja"
            ]
          },
          "dueDate": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Obligatoria al crear; puede quedar null si un PUT envía dueDate null (ver PUT)."
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "budget": {
            "type": [
              "number",
              "null"
            ]
          },
          "timeline": {
            "type": [
              "string",
              "null"
            ],
            "description": "Texto libre."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "project": {
            "$ref": "#/components/schemas/TaskProjectSummary"
          },
          "responsible": {
            "$ref": "#/components/schemas/UserSummary",
            "description": "Usuario responsable; null si fue borrado."
          },
          "contact": {
            "$ref": "#/components/schemas/TaskContactSummary"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "TaskProjectSummary": {
        "type": [
          "object",
          "null"
        ],
        "description": "Proyecto de la tarea (poblado). null si la tarea no tiene proyecto o el proyecto fue borrado.",
        "required": [
          "id",
          "name",
          "color"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "color": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "TaskContactSummary": {
        "type": [
          "object",
          "null"
        ],
        "description": "Contacto asociado (poblado y aplanado). Además de id, fullName y company, incluye TODAS las demás properties del contacto como claves planas (email, mobile, source…) con el valor que tengan (keys duplicadas: gana la marcada isPrimary o la primera). null si la tarea no tiene contacto o el contacto fue borrado.",
        "required": [
          "id",
          "fullName",
          "company"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "fullName": {
            "type": "string",
            "description": "firstName + lastName recortado; '' si no tiene."
          },
          "company": {
            "type": "string",
            "description": "Valor de la property companyName; '' si no tiene."
          }
        },
        "additionalProperties": true
      },
      "TaskCreate": {
        "type": "object",
        "description": "title, dueDate y responsibleId son obligatorios. dueDate acepta cualquier cadena parseable por Date. responsibleId solo se valida como ObjectId: NO se comprueba que exista ni que pertenezca a la organización (responsible vuelve null si no existe). projectId se valida contra la organización (400 si no existe); en POST /projects/{id}/tasks el projectId sale de la ruta y el del body se ignora. contactId solo se valida como ObjectId. status por defecto \"No Iniciado\", priority por defecto \"Baja\". budget con Number(): null o '' se guardan como 0. tags: si llega una cadena suelta el modelo la convierte en array de un elemento. La tarea queda con ownerId = usuario dueño del token (no expuesto).",
        "required": [
          "title",
          "dueDate",
          "responsibleId"
        ],
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1
          },
          "dueDate": {
            "type": "string",
            "description": "Cualquier cadena parseable por Date (con o sin hora)."
          },
          "responsibleId": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$",
            "description": "ID de usuario; no se verifica su existencia."
          },
          "projectId": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$",
            "description": "Debe existir en la organización. Ignorado en POST /projects/{id}/tasks."
          },
          "contactId": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$",
            "description": "No se verifica su existencia."
          },
          "status": {
            "type": "string",
            "enum": [
              "No Iniciado",
              "En Progreso",
              "Revisión",
              "Completado"
            ],
            "default": "No Iniciado"
          },
          "priority": {
            "type": "string",
            "enum": [
              "Alta",
              "Media",
              "Baja"
            ],
            "default": "Baja"
          },
          "description": {
            "type": "string"
          },
          "notes": {
            "type": "string"
          },
          "budget": {
            "type": [
              "number",
              "string"
            ],
            "description": "Número o cadena numérica ≥ 0."
          },
          "timeline": {
            "type": "string"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "TaskUpdate": {
        "type": "object",
        "description": "Actualización parcial: solo se tocan title, status, dueDate, priority, notes, description, budget, timeline, tags, responsibleId, projectId y contactId; sin ninguno responde 400. status y priority se validan contra el enum SIEMPRE que la clave esté presente (status null → 400). responsibleId no admite null (400 Invalid responsibleId). projectId y contactId null sí desvinculan. dueDate null pasa la validación (solo corre con valor truthy) y se guarda null aunque el modelo la marque required (findOneAndUpdate no corre validadores). Cambiar dueDate o status reinicia el recordatorio de vencimiento (se borra dueDateReminderNotifiedAt) y la tarea volverá a notificarse.",
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1
          },
          "status": {
            "type": "string",
            "enum": [
              "No Iniciado",
              "En Progreso",
              "Revisión",
              "Completado"
            ]
          },
          "dueDate": {
            "type": [
              "string",
              "null"
            ],
            "description": "Cualquier cadena parseable por Date; null se guarda tal cual."
          },
          "priority": {
            "type": "string",
            "enum": [
              "Alta",
              "Media",
              "Baja"
            ]
          },
          "responsibleId": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "projectId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Debe existir en la organización; null desvincula."
          },
          "contactId": {
            "type": [
              "string",
              "null"
            ],
            "description": "null desvincula."
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "budget": {
            "type": [
              "number",
              "string",
              "null"
            ]
          },
          "timeline": {
            "type": [
              "string",
              "null"
            ]
          },
          "tags": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            }
          }
        }
      },
      "TaskResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/Task"
          }
        }
      },
      "TaskListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Task"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "TaskDeleteResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "id"
            ],
            "properties": {
              "id": {
                "type": "string"
              }
            }
          }
        }
      },
      "TaskError": {
        "type": "object",
        "description": "Contrato de error de los controllers de tareas (success:false + error, mensajes de validación en español).",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          }
        }
      },
      "ContactList": {
        "type": "object",
        "description": "Representación pública de la lista. Nunca incluye los contactIds (ver GET /lists/{id}/contacts). contactCount es la longitud del array contactIds guardado (incluye ids de contactos ya borrados o de otra organización); es 0 en listas dinámicas y también en GET /lists (la consulta excluye contactIds antes de contar).",
        "required": [
          "id",
          "name",
          "description",
          "type",
          "filters",
          "contactCount",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "static",
              "dynamic"
            ]
          },
          "filters": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ListFilter"
            },
            "description": "[] en listas estáticas."
          },
          "contactCount": {
            "type": "integer",
            "minimum": 0,
            "description": "Cantidad de contactIds guardados (real en todas las respuestas desde el 05-sep-2026); 0 en dinámicas, que no guardan ids."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ListFilter": {
        "type": "object",
        "description": "Filtro de una lista dinámica sobre las properties del contacto. key es la key de la property (email, city, source…) o leadScore; operator uno de contains, equals, starts with, ends with, is empty, is not empty, greater_than, less_than. Para leadScore solo se aplican greater_than, less_than y equals (numéricos); el resto se ignora en silencio. Un operator desconocido se guarda pero no filtra. Los valores de contains/starts with/ends with son regex sin escapar.",
        "required": [
          "id",
          "key",
          "operator",
          "value"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Se genera (filter-N) si no se envía."
          },
          "key": {
            "type": "string"
          },
          "operator": {
            "type": "string"
          },
          "value": {
            "type": "string",
            "description": "'' cuando el operador no necesita valor."
          }
        }
      },
      "ListFilterInput": {
        "type": "object",
        "description": "Filtro a guardar. key y operator son obligatorios; faltar uno u operator desconocido responde 400 (desde el 05-sep-2026). value se normaliza a cadena ('' si no viene).",
        "required": [
          "key",
          "operator"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "key": {
            "type": "string"
          },
          "operator": {
            "type": "string",
            "enum": [
              "contains",
              "equals",
              "starts with",
              "ends with",
              "is empty",
              "is not empty",
              "greater_than",
              "less_than"
            ],
            "description": "No se valida al guardar; un valor fuera del enum simplemente no filtra."
          },
          "value": {
            "type": "string"
          }
        }
      },
      "ListMember": {
        "type": "object",
        "description": "Contacto de la lista aplanado: id, todas las properties como claves planas (keys duplicadas: gana la marcada isPrimary o la primera) y las fechas. No incluye fullName ni companyId (a diferencia de GET /contacts).",
        "required": [
          "id",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "additionalProperties": true
      },
      "StaticListCreate": {
        "type": "object",
        "description": "name obligatorio (trim). contactIds opcional, máximo 2000 (se deduplican); cada uno debe ser el ObjectId de un contacto existente de la organización (si no, 400 con invalidContactIds). Cuenta contra el límite contactGroups del plan.",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1
          },
          "description": {
            "type": "string"
          },
          "contactIds": {
            "type": "array",
            "maxItems": 2000,
            "items": {
              "type": "string",
              "pattern": "^[a-fA-F0-9]{24}$"
            }
          }
        }
      },
      "DynamicListCreate": {
        "type": "object",
        "description": "name obligatorio (trim) y al menos un filtro. Cada filtro exige key y un operator del catálogo; si no, 400 (desde el 05-sep-2026). Cuenta contra el límite contactGroups del plan.",
        "required": [
          "name",
          "filters"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1
          },
          "description": {
            "type": "string"
          },
          "filters": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/ListFilterInput"
            }
          }
        }
      },
      "ListUpdate": {
        "type": "object",
        "description": "Actualización parcial de name, description y filters; cualquier otro campo se ignora y sin ninguno responde 400. name debe ser una cadena no vacía (otro tipo → 400). filters se valida y normaliza igual que en POST /lists/dynamic (ids filter-N, value a cadena); también se acepta en listas ESTÁTICAS, donde no tiene efecto. No se puede cambiar type; para quitar contactos, DELETE /lists/{id}/contacts.",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "filters": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ListFilterInput"
            }
          }
        }
      },
      "AddContactsRequest": {
        "type": "object",
        "required": [
          "contactIds"
        ],
        "properties": {
          "contactIds": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string",
              "pattern": "^[a-fA-F0-9]{24}$"
            },
            "description": "Cada id debe ser un contacto existente de la organización del token; si no, 400 con invalidContactIds."
          }
        }
      },
      "ListDetailResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/ContactList"
          }
        }
      },
      "ListPageResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ContactList"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "ListMembersResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ListMember"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination",
            "description": "En listas estáticas total es la longitud de contactIds (puede superar los contactos realmente devueltos si algunos ya no existen)."
          }
        }
      },
      "AddContactsResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "id",
              "added",
              "skipped",
              "totalContacts"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "added": {
                "type": "integer",
                "minimum": 0,
                "description": "Ids nuevos agregados."
              },
              "skipped": {
                "type": "integer",
                "minimum": 0,
                "description": "Ids que ya estaban en la lista (duplicados dentro del mismo body se cuentan como agregados)."
              },
              "totalContacts": {
                "type": "integer",
                "minimum": 0
              }
            }
          }
        }
      },
      "RemoveContactsResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "id",
              "removed",
              "skipped",
              "totalContacts"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "removed": {
                "type": "integer",
                "minimum": 0,
                "description": "Ids que estaban en la lista y se quitaron."
              },
              "skipped": {
                "type": "integer",
                "minimum": 0,
                "description": "Ids enviados que no estaban en la lista."
              },
              "totalContacts": {
                "type": "integer",
                "minimum": 0
              }
            }
          }
        }
      },
      "ListDeleteResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "id"
            ],
            "properties": {
              "id": {
                "type": "string"
              }
            }
          }
        }
      },
      "ListError": {
        "type": "object",
        "description": "Contrato de error de los controllers de listas (success:false + error, mensajes en español).",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          },
          "invalidContactIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Solo en \"Contactos inexistentes o de otra organización\" — ids rechazados."
          }
        }
      },
      "Fragment": {
        "type": "object",
        "description": "Respuesta rápida / plantilla de texto reutilizable por atajo (p. ej. /saludo). Expone userId (id del usuario creador, como cadena) pero no organizationId. format text (plano, WhatsApp) o html (correo); asunto solo tiene sentido con html.",
        "required": [
          "id",
          "name",
          "text",
          "atajo",
          "format",
          "asunto",
          "userId",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "atajo": {
            "type": "string",
            "description": "Único por organización."
          },
          "format": {
            "type": "string",
            "enum": [
              "text",
              "html"
            ]
          },
          "asunto": {
            "type": "string",
            "description": "'' si no tiene."
          },
          "userId": {
            "type": "string",
            "description": "Usuario creador (el dueño del token en los creados por API)."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "FragmentCreate": {
        "type": "object",
        "description": "name, text y atajo obligatorios (cadenas no vacías; name y atajo se recortan, text no). format distinto de 'html' se guarda como 'text' sin error. asunto no string → ''. El atajo debe ser único en la organización, pero la comprobación previa busca el atajo en TODAS las organizaciones: un atajo que exista en otra org responde 409 aunque el índice único sí lo permitiría.",
        "required": [
          "name",
          "text",
          "atajo"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1
          },
          "text": {
            "type": "string",
            "minLength": 1
          },
          "atajo": {
            "type": "string",
            "minLength": 1
          },
          "format": {
            "type": "string",
            "enum": [
              "text",
              "html"
            ],
            "default": "text"
          },
          "asunto": {
            "type": "string"
          }
        }
      },
      "FragmentUpdate": {
        "type": "object",
        "description": "Actualización parcial de name, text, atajo, format y asunto; sin ninguno responde 400. name/text/atajo no pueden quedar vacíos; format debe ser text o html (aquí sí 400, a diferencia de POST). asunto no se valida (cualquier tipo se castea). Un atajo que ya exista en la organización responde 409 por el índice único.",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1
          },
          "text": {
            "type": "string",
            "minLength": 1
          },
          "atajo": {
            "type": "string",
            "minLength": 1
          },
          "format": {
            "type": "string",
            "enum": [
              "text",
              "html"
            ]
          },
          "asunto": {
            "type": "string"
          }
        }
      },
      "FragmentResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/Fragment"
          }
        }
      },
      "FragmentListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Fragment"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "FragmentDeleteResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "id"
            ],
            "properties": {
              "id": {
                "type": "string"
              }
            }
          }
        }
      },
      "FragmentError": {
        "type": "object",
        "description": "Contrato de error de los controllers de fragments (success:false + error, mensajes en español).",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          }
        }
      },
      "LibraryFile": {
        "type": "object",
        "description": "Metadatos de un archivo de la Biblioteca (el binario vive en S3; mediaURL apunta a él). No expone user ni organization. isVisible false identifica adjuntos de mensajes que la app no muestra en la Biblioteca.",
        "required": [
          "id",
          "name",
          "fileType",
          "mediaURL",
          "tags",
          "isVisible",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "description": "Nombre visible; se puede cambiar con PUT."
          },
          "fileType": {
            "type": "string",
            "description": "Tipo tal como lo guardó la subida (MIME o extensión, texto libre; p. ej. application/pdf o image/png)."
          },
          "mediaURL": {
            "type": "string",
            "description": "URL pública del archivo en S3."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Etiquetas normalizadas (minúsculas, sin espacios repetidos). [] si no tiene."
          },
          "isVisible": {
            "type": "boolean",
            "description": "true = archivo de Biblioteca; false = adjunto de mensaje guardado sin registro visible."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "LibraryFileUpdate": {
        "type": "object",
        "description": "Solo metadatos. Debe venir name y/o tags; si faltan ambos responde 400. tags acepta array o cadena separada por comas y se normaliza (trim, minúsculas, sin vacíos ni duplicados); una cadena vacía o un valor que no es array/cadena deja el archivo sin tags. Los tags nuevos se agregan además al catálogo de la organización (settings.library.tags). name se recorta; vacío responde 400. No reemplaza el archivo: la subida solo existe en la API interna (multipart).",
        "minProperties": 1,
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1
          },
          "tags": {
            "oneOf": [
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              {
                "type": "string"
              }
            ],
            "description": "Array de tags o cadena \"a, b, c\"."
          }
        }
      },
      "LibraryFileResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/LibraryFile"
          }
        }
      },
      "LibraryFileListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LibraryFile"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "LibraryTagListResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Tags en uso por archivos visibles de la organización, ordenados alfabéticamente."
          }
        }
      },
      "LibraryError": {
        "type": "object",
        "description": "Contrato de error de los controllers de Biblioteca (success:false + error en español).",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          }
        }
      },
      "RagSearchRequest": {
        "type": "object",
        "description": "query es obligatorio (cadena no vacía; se recorta). topK y minScore solo se aplican si son números JSON (una cadena se ignora en silencio): topK se trunca y se acota a 1..20 (por defecto 5); minScore se acota a 0..1 (por defecto 0.25). tag se recorta y pasa a minúsculas. documentId restringe a un documento de la organización y debe ser un ObjectId válido (400 si no). Solo se buscan documentos con status ready.",
        "required": [
          "query"
        ],
        "properties": {
          "query": {
            "type": "string",
            "minLength": 1
          },
          "topK": {
            "type": "integer",
            "minimum": 1,
            "maximum": 20,
            "default": 5
          },
          "minScore": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "default": 0.25
          },
          "tag": {
            "type": "string"
          },
          "documentId": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          }
        }
      },
      "RagSearchHit": {
        "type": "object",
        "required": [
          "content",
          "score",
          "documentId",
          "documentTitle",
          "chunkIndex"
        ],
        "properties": {
          "content": {
            "type": "string",
            "description": "Texto del fragmento (chunk)."
          },
          "score": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Similitud coseno redondeada a 3 decimales."
          },
          "documentId": {
            "type": "string"
          },
          "documentTitle": {
            "type": "string",
            "description": "'' si el documento ya no existe."
          },
          "chunkIndex": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "RagSearchResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "query",
              "totalResults",
              "results"
            ],
            "properties": {
              "query": {
                "type": "string",
                "description": "La consulta recortada."
              },
              "totalResults": {
                "type": "integer",
                "minimum": 0
              },
              "results": {
                "type": "array",
                "description": "Ordenados por score descendente.",
                "items": {
                  "$ref": "#/components/schemas/RagSearchHit"
                }
              }
            }
          }
        }
      },
      "RagDocumentSummary": {
        "type": "object",
        "description": "Resumen de un documento en el listado (sin sourceType, mimeType, sizeBytes, tokenCount ni updatedAt).",
        "required": [
          "id",
          "title",
          "fileName",
          "status",
          "chunkCount",
          "tags",
          "createdAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "fileName": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "processing",
              "ready",
              "failed"
            ]
          },
          "chunkCount": {
            "type": "integer",
            "minimum": 0,
            "description": "0 mientras está en processing."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RagDocument": {
        "type": "object",
        "description": "Documento completo (respuesta de POST). statusError solo aparece cuando la ingesta falló (status failed); recién creado no viene.",
        "required": [
          "id",
          "title",
          "sourceType",
          "fileName",
          "mimeType",
          "sizeBytes",
          "status",
          "chunkCount",
          "tokenCount",
          "tags",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "sourceType": {
            "type": "string",
            "enum": [
              "file",
              "manual_text"
            ],
            "description": "Por esta API siempre manual_text."
          },
          "fileName": {
            "type": "string",
            "description": "Derivado del título (sin acentos",
            "minúsculas": null,
            "_ en vez de símbolos) + .txt.": null
          },
          "mimeType": {
            "type": "string",
            "description": "Por esta API siempre text/plain."
          },
          "sizeBytes": {
            "type": "integer",
            "minimum": 0,
            "description": "Bytes UTF-8 del texto."
          },
          "status": {
            "type": "string",
            "enum": [
              "processing",
              "ready",
              "failed"
            ]
          },
          "statusError": {
            "type": "string",
            "description": "Mensaje legible cuando status es failed."
          },
          "chunkCount": {
            "type": "integer",
            "minimum": 0
          },
          "tokenCount": {
            "type": "integer",
            "minimum": 0,
            "description": "Tokens estimados (caracteres/4) tras la ingesta."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RagDocumentCreate": {
        "type": "object",
        "description": "Solo texto plano en JSON: no hay subida de archivos binarios por esta API (multipart vive en la API interna). text es obligatorio (cadena no vacía; máximo 2 MB en UTF-8 → 413). title se recorta a 300 caracteres; vacío o ausente queda \"texto-manual\". tags acepta array o cadena separada por comas; se recortan, pasan a minúsculas y se deduplican.",
        "required": [
          "text"
        ],
        "properties": {
          "title": {
            "type": "string",
            "maxLength": 300
          },
          "text": {
            "type": "string",
            "minLength": 1
          },
          "tags": {
            "oneOf": [
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              {
                "type": "string"
              }
            ]
          }
        }
      },
      "RagDocumentListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RagDocumentSummary"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "RagDocumentAcceptedResult": {
        "type": "object",
        "required": [
          "success",
          "message",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "message": {
            "type": "string",
            "const": "Documento recibido; la indexación corre en segundo plano."
          },
          "data": {
            "$ref": "#/components/schemas/RagDocument"
          }
        }
      },
      "RagDeleteResult": {
        "type": "object",
        "required": [
          "success",
          "message"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "message": {
            "type": "string",
            "const": "Documento eliminado correctamente"
          }
        }
      },
      "RagError": {
        "type": "object",
        "description": "Contrato de error de los controllers de RAG (success:false + error en español). Los 403 de límite de plan agregan code PLAN_LIMIT_REACHED y, en el de cantidad, details.",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "enum": [
              "PLAN_LIMIT_REACHED",
              "RAG_QUEUE_UNAVAILABLE"
            ]
          },
          "details": {
            "type": "object",
            "description": "Resultado de checkPlanLimit (solo en el 403 de cantidad de documentos)."
          },
          "data": {
            "type": "object",
            "description": "Solo en el 503 de cola — el documento que quedó en failed.",
            "properties": {
              "allowed": {
                "type": "boolean"
              },
              "limit": {
                "type": "integer",
                "description": "-1 = ilimitado."
              },
              "used": {
                "type": "integer"
              },
              "remaining": {
                "type": "integer",
                "description": "-1 si ilimitado."
              },
              "planName": {
                "type": "string"
              }
            }
          }
        }
      },
      "Agent": {
        "type": "object",
        "description": "Documento completo del agente tal como sale de Mongo (sin transformar): _id, __v, organizationId, createdBy y updatedBy incluidos. integrationId, description y updatedBy pueden faltar.",
        "required": [
          "_id",
          "organizationId",
          "name",
          "systemPrompt",
          "provider",
          "aiModel",
          "temperature",
          "maxTokens",
          "enabledTools",
          "maxToolCallsPerRun",
          "handoffKeywords",
          "triggers",
          "autoAssignNewConversations",
          "contextSettings",
          "leadStateSchema",
          "status",
          "stats",
          "createdBy",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "_id": {
            "type": "string"
          },
          "organizationId": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "maxLength": 100
          },
          "description": {
            "type": "string",
            "maxLength": 500
          },
          "systemPrompt": {
            "type": "string",
            "maxLength": 30000
          },
          "integrationId": {
            "type": "string",
            "description": "Integración (credenciales del proveedor) que usa el agente."
          },
          "provider": {
            "type": "string",
            "enum": [
              "openai",
              "anthropic",
              "google"
            ]
          },
          "aiModel": {
            "type": "string"
          },
          "temperature": {
            "type": "number",
            "minimum": 0,
            "maximum": 2
          },
          "maxTokens": {
            "type": "integer",
            "minimum": 50,
            "maximum": 12000
          },
          "enabledTools": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 30,
            "description": "Nombres de GET /agents/tools. Un nombre desconocido se guarda y se ignora en ejecución."
          },
          "maxToolCallsPerRun": {
            "type": "integer",
            "minimum": 0,
            "maximum": 15
          },
          "handoffKeywords": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 60
            },
            "maxItems": 30
          },
          "handoffMessage": {
            "type": "string",
            "maxLength": 400
          },
          "triggers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AgentTrigger"
            },
            "maxItems": 10
          },
          "autoAssignNewConversations": {
            "type": "boolean",
            "description": "Solo surte efecto con status active."
          },
          "contextSettings": {
            "$ref": "#/components/schemas/AgentContextSettings"
          },
          "leadStateSchema": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AgentLeadStateField"
            },
            "maxItems": 20
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "paused",
              "draft"
            ]
          },
          "stats": {
            "$ref": "#/components/schemas/AgentStats"
          },
          "createdBy": {
            "type": "string",
            "description": "Usuario dueño del token que lo creó."
          },
          "updatedBy": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "__v": {
            "type": "integer"
          }
        },
        "additionalProperties": true
      },
      "AgentSummary": {
        "type": "object",
        "description": "Proyección del listado (GET /agents). Usa _id, no id; no viene systemPrompt ni configuración.",
        "required": [
          "_id",
          "name",
          "status",
          "provider",
          "aiModel",
          "stats",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "paused",
              "draft"
            ]
          },
          "provider": {
            "type": "string",
            "enum": [
              "openai",
              "anthropic",
              "google"
            ]
          },
          "aiModel": {
            "type": "string"
          },
          "stats": {
            "$ref": "#/components/schemas/AgentStats"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AgentTrigger": {
        "type": "object",
        "description": "Condición que dispara al agente. _id lo asigna Mongo al guardar.",
        "required": [
          "type"
        ],
        "properties": {
          "_id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "message_received",
              "stage_entered",
              "no_response_timeout",
              "manual"
            ]
          },
          "isActive": {
            "type": "boolean",
            "default": true
          },
          "priority": {
            "type": "integer",
            "default": 10
          },
          "sourceFilters": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "whatsapp",
                "facebook",
                "instagram"
              ]
            }
          },
          "stageIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "IDs de PipelineStage."
          },
          "pipelineIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "IDs de ConversationPipeline."
          },
          "keywords": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 80
            }
          },
          "excludeIfAssignedToHuman": {
            "type": "boolean",
            "default": true
          },
          "excludeIfTagsPresent": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 50
            }
          },
          "requireTagsPresent": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 50
            }
          },
          "schedule": {
            "$ref": "#/components/schemas/AgentTriggerSchedule"
          },
          "noResponseMinutes": {
            "type": "integer",
            "minimum": 5,
            "maximum": 10080,
            "description": "Solo para no_response_timeout."
          }
        }
      },
      "AgentTriggerSchedule": {
        "type": "object",
        "description": "Horario en que el trigger está activo.",
        "properties": {
          "timezone": {
            "type": "string",
            "default": "America/Bogota"
          },
          "hours": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "from",
                "to"
              ],
              "properties": {
                "from": {
                  "type": "string",
                  "description": "HH:mm",
                  "example": "08:00"
                },
                "to": {
                  "type": "string",
                  "description": "HH:mm",
                  "example": "18:00"
                }
              }
            }
          },
          "days": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "mon",
                "tue",
                "wed",
                "thu",
                "fri",
                "sat",
                "sun"
              ]
            }
          }
        }
      },
      "AgentContextSettings": {
        "type": "object",
        "description": "Ajustes que el orquestador pasa al constructor de contexto en cada turno. Ojo en PUT: es un subdocumento y se reemplaza completo — enviar solo una clave deja las demás en sus valores por defecto.",
        "properties": {
          "messagesLimit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 50,
            "default": 20
          },
          "patternWindow": {
            "type": "integer",
            "minimum": 5,
            "maximum": 200,
            "default": 50
          },
          "dedupeSimilarityThreshold": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "default": 0.85,
            "description": "0 desactiva la deduplicación de respuestas."
          },
          "batchWindowSeconds": {
            "type": "integer",
            "minimum": 0,
            "maximum": 20,
            "default": 8,
            "description": "0 responde mensaje a mensaje."
          },
          "batchMaxWaitSeconds": {
            "type": "integer",
            "minimum": 5,
            "maximum": 60,
            "default": 20
          }
        }
      },
      "AgentLeadStateField": {
        "type": "object",
        "description": "Campo estructurado que el agente captura del lead (tool update_lead_state).",
        "required": [
          "key",
          "label"
        ],
        "properties": {
          "key": {
            "type": "string",
            "maxLength": 40
          },
          "label": {
            "type": "string",
            "maxLength": 80
          },
          "description": {
            "type": "string",
            "maxLength": 200
          },
          "required": {
            "type": "boolean",
            "default": false
          }
        }
      },
      "AgentStats": {
        "type": "object",
        "description": "Contadores acumulados por las ejecuciones. No se pueden escribir por API (POST los ignora en la práctica al nacer en 0; PUT los descarta).",
        "required": [
          "totalRuns",
          "totalInputTokens",
          "totalOutputTokens",
          "totalCostUsd",
          "handoffsToHuman",
          "errorsCount"
        ],
        "properties": {
          "totalRuns": {
            "type": "integer"
          },
          "totalInputTokens": {
            "type": "integer"
          },
          "totalOutputTokens": {
            "type": "integer"
          },
          "totalCostUsd": {
            "type": "number"
          },
          "lastRunAt": {
            "type": "string",
            "format": "date-time"
          },
          "handoffsToHuman": {
            "type": "integer"
          },
          "errorsCount": {
            "type": "integer"
          }
        }
      },
      "AgentLimits": {
        "type": "object",
        "description": "Constantes de validación del modelo (AGENT_LIMITS), útiles para armar formularios.",
        "additionalProperties": {
          "type": "integer"
        },
        "example": {
          "NAME_MAX": 100,
          "DESCRIPTION_MAX": 500,
          "SYSTEM_PROMPT_MAX": 30000,
          "HANDOFF_MESSAGE_MAX": 400,
          "WELCOME_MESSAGE_MAX": 600,
          "HANDOFF_KEYWORD_MAX": 60,
          "MAX_HANDOFF_KEYWORDS": 30,
          "MAX_TRIGGERS": 10,
          "MAX_ENABLED_TOOLS": 30,
          "MAX_TOKENS_HARD_CAP": 12000,
          "MAX_TOOL_CALLS_HARD_CAP": 15,
          "MESSAGES_LIMIT_MIN": 1,
          "MESSAGES_LIMIT_MAX": 50,
          "PATTERN_WINDOW_MIN": 5,
          "PATTERN_WINDOW_MAX": 200,
          "DEDUPE_SIMILARITY_MIN": 0,
          "DEDUPE_SIMILARITY_MAX": 1,
          "BATCH_WINDOW_MIN": 0,
          "BATCH_WINDOW_MAX": 20,
          "BATCH_MAX_WAIT_MIN": 5,
          "BATCH_MAX_WAIT_MAX": 60,
          "LEAD_STATE_SCHEMA_MAX_FIELDS": 20,
          "LEAD_STATE_FIELD_KEY_MAX": 40,
          "LEAD_STATE_FIELD_LABEL_MAX": 80,
          "LEAD_STATE_FIELD_DESC_MAX": 200
        }
      },
      "AgentCreate": {
        "type": "object",
        "description": "name y systemPrompt obligatorios (400 con message si falta alguno; systemPrompt > 30000 caracteres → 400). El resto toma los valores por defecto del modelo (provider openai, aiModel gpt-4o-mini, temperature 0.4, maxTokens 400, status draft, handoffKeywords y handoffMessage en español, contextSettings por defecto). El body se pasa entero al modelo: organizationId y createdBy se sobrescriben con los del token, los campos que no existen en el schema se descartan en silencio, y stats SÍ se acepta (se puede crear un agente con contadores inventados). Las validaciones del modelo (min/max, enums, tamaños de arrays) responden 400 \"Datos inválidos\" con errors por campo.",
        "required": [
          "name",
          "systemPrompt"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "description": {
            "type": "string",
            "maxLength": 500
          },
          "systemPrompt": {
            "type": "string",
            "minLength": 1,
            "maxLength": 30000
          },
          "integrationId": {
            "type": "string"
          },
          "provider": {
            "type": "string",
            "enum": [
              "openai",
              "anthropic",
              "google"
            ],
            "default": "openai"
          },
          "aiModel": {
            "type": "string",
            "default": "gpt-4o-mini"
          },
          "temperature": {
            "type": "number",
            "minimum": 0,
            "maximum": 2,
            "default": 0.4
          },
          "maxTokens": {
            "type": "integer",
            "minimum": 50,
            "maximum": 12000,
            "default": 400
          },
          "enabledTools": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 30
          },
          "maxToolCallsPerRun": {
            "type": "integer",
            "minimum": 0,
            "maximum": 15,
            "default": 5
          },
          "handoffKeywords": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 60
            },
            "maxItems": 30
          },
          "handoffMessage": {
            "type": "string",
            "maxLength": 400
          },
          "triggers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AgentTrigger"
            },
            "maxItems": 10
          },
          "autoAssignNewConversations": {
            "type": "boolean",
            "default": false
          },
          "contextSettings": {
            "$ref": "#/components/schemas/AgentContextSettings"
          },
          "leadStateSchema": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AgentLeadStateField"
            },
            "maxItems": 20
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "paused",
              "draft"
            ],
            "default": "draft"
          }
        }
      },
      "AgentUpdate": {
        "type": "object",
        "description": "Actualización parcial con $set de primer nivel: solo cambian las claves enviadas, pero un subdocumento (contextSettings, triggers, leadStateSchema) se reemplaza completo. organizationId, createdBy y stats se descartan del body. systemPrompt > 30000 → 400. Corre las validaciones del modelo (400 \"Datos inválidos\"). Se puede cambiar status aquí, aunque exista PATCH /agents/{id}/status. Los campos fuera del schema se ignoran.",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "description": {
            "type": "string",
            "maxLength": 500
          },
          "systemPrompt": {
            "type": "string",
            "minLength": 1,
            "maxLength": 30000
          },
          "integrationId": {
            "type": "string"
          },
          "provider": {
            "type": "string",
            "enum": [
              "openai",
              "anthropic",
              "google"
            ]
          },
          "aiModel": {
            "type": "string"
          },
          "temperature": {
            "type": "number",
            "minimum": 0,
            "maximum": 2
          },
          "maxTokens": {
            "type": "integer",
            "minimum": 50,
            "maximum": 12000
          },
          "enabledTools": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 30
          },
          "maxToolCallsPerRun": {
            "type": "integer",
            "minimum": 0,
            "maximum": 15
          },
          "handoffKeywords": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 60
            },
            "maxItems": 30
          },
          "handoffMessage": {
            "type": "string",
            "maxLength": 400
          },
          "triggers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AgentTrigger"
            },
            "maxItems": 10
          },
          "autoAssignNewConversations": {
            "type": "boolean"
          },
          "contextSettings": {
            "$ref": "#/components/schemas/AgentContextSettings"
          },
          "leadStateSchema": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AgentLeadStateField"
            },
            "maxItems": 20
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "paused",
              "draft"
            ]
          }
        }
      },
      "AgentStatusUpdate": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "active",
              "paused",
              "draft"
            ]
          }
        }
      },
      "AgentTool": {
        "type": "object",
        "required": [
          "name",
          "description"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          }
        }
      },
      "AgentListResult": {
        "type": "object",
        "required": [
          "agents",
          "limits"
        ],
        "properties": {
          "agents": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AgentSummary"
            },
            "description": "Ordenados por updatedAt descendente. Sin paginación."
          },
          "limits": {
            "$ref": "#/components/schemas/AgentLimits"
          }
        }
      },
      "AgentDetailResult": {
        "type": "object",
        "required": [
          "agent",
          "limits"
        ],
        "properties": {
          "agent": {
            "$ref": "#/components/schemas/Agent"
          },
          "limits": {
            "$ref": "#/components/schemas/AgentLimits"
          }
        }
      },
      "AgentResult": {
        "type": "object",
        "required": [
          "agent"
        ],
        "properties": {
          "agent": {
            "$ref": "#/components/schemas/Agent"
          }
        }
      },
      "AgentToolListResult": {
        "type": "object",
        "required": [
          "tools"
        ],
        "properties": {
          "tools": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AgentTool"
            }
          }
        }
      },
      "AgentDeleteResult": {
        "type": "object",
        "required": [
          "message",
          "releasedConversations"
        ],
        "properties": {
          "message": {
            "type": "string",
            "const": "Agente eliminado"
          },
          "releasedConversations": {
            "type": "integer",
            "minimum": 0,
            "description": "Conversaciones a las que se les quitó assignedAgentId."
          }
        }
      },
      "AgentError": {
        "type": "object",
        "description": "Contrato de error del controller de agentes (message en español, sin success). errors solo en \"Datos inválidos\".",
        "required": [
          "message"
        ],
        "properties": {
          "message": {
            "type": "string"
          },
          "errors": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Mensaje de validación de Mongoose por campo."
          }
        }
      },
      "AgentContextContact": {
        "type": "object",
        "description": "Contacto por propiedad mobile. Si no existe ficha de contacto, todos los textos vienen '' salvo fullName y mobile (el número) y leadScore 0. tags es siempre [] (no se llena).",
        "required": [
          "id",
          "firstName",
          "lastName",
          "fullName",
          "email",
          "mobile",
          "company",
          "position",
          "leadScore",
          "tags"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "'' si no hay contacto."
          },
          "firstName": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "fullName": {
            "type": "string",
            "description": "Nombre y apellido",
            "o el número si no hay nombre.": null
          },
          "email": {
            "type": "string"
          },
          "mobile": {
            "type": "string"
          },
          "company": {
            "type": "string"
          },
          "position": {
            "type": "string"
          },
          "leadScore": {
            "type": "number"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "AgentContextConversation": {
        "type": "object",
        "required": [
          "id",
          "title",
          "source",
          "priority",
          "pipeline",
          "currentStage",
          "assignedTo",
          "tags",
          "isPausedByHuman",
          "unreadCount",
          "createdAt",
          "lastMessageAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "source": {
            "type": "string",
            "description": "Canal (whatsapp por defecto)."
          },
          "priority": {
            "type": "string",
            "description": "medium por defecto."
          },
          "pipeline": {
            "type": "object",
            "required": [
              "id",
              "name"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "currentStage": {
            "type": "object",
            "required": [
              "id",
              "name",
              "order"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "order": {
                "type": "integer"
              }
            }
          },
          "assignedTo": {
            "type": [
              "string",
              "null"
            ],
            "description": "Nombre completo del asesor asignado",
            "o null.": null
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "isPausedByHuman": {
            "type": "boolean",
            "description": "automationSettings.isPaused de la conversación."
          },
          "unreadCount": {
            "type": "integer"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "lastMessageAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "AgentContextMessage": {
        "type": "object",
        "required": [
          "direction",
          "type",
          "message",
          "timestamp",
          "hasMedia"
        ],
        "properties": {
          "direction": {
            "type": "string",
            "enum": [
              "incoming",
              "outgoing"
            ]
          },
          "type": {
            "type": "string",
            "description": "Tipo del mensaje (text",
            "image": null,
            "document…).": null
          },
          "message": {
            "type": "string",
            "description": "'' si no tiene texto."
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          },
          "hasMedia": {
            "type": "boolean"
          }
        }
      },
      "AgentContextDeal": {
        "type": "object",
        "required": [
          "title",
          "amount",
          "closingDate",
          "status"
        ],
        "properties": {
          "title": {
            "type": "string"
          },
          "amount": {
            "type": "number"
          },
          "closingDate": {
            "type": "string",
            "format": "date-time"
          },
          "saleDate": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Fecha de venta (wonAt",
            "o closingDate/createdAt en negocios viejos).": null
          },
          "status": {
            "type": "string",
            "description": "Nombre del estado",
            "o \"sin estado\".": null
          }
        }
      },
      "AgentContextDeals": {
        "type": "object",
        "description": "Negocios del contacto ordenados por fecha de venta descendente (máximo deals; [] si no hay contacto o deals=0).",
        "required": [
          "total",
          "totalRevenue",
          "avgTicket",
          "lastDeal",
          "daysSinceLastDeal",
          "list"
        ],
        "properties": {
          "total": {
            "type": "integer",
            "description": "Cantidad devuelta (acotada por deals)",
            "no el total histórico.": null
          },
          "totalRevenue": {
            "type": "number",
            "description": "Suma de amount de los devueltos."
          },
          "avgTicket": {
            "type": "number"
          },
          "lastDeal": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/AgentContextDeal"
              },
              {
                "type": "null"
              }
            ]
          },
          "daysSinceLastDeal": {
            "type": [
              "integer",
              "null"
            ]
          },
          "list": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AgentContextDeal"
            }
          }
        }
      },
      "AgentContextPattern": {
        "type": "object",
        "description": "Heurísticas por palabras clave sobre los últimos patternWindow mensajes (50 por defecto).",
        "required": [
          "tone",
          "isRecurringCustomer",
          "autoReopenCount",
          "hoursSinceLastIncoming",
          "lastIncomingHasReply",
          "mentionedPendingPayment",
          "mentionsPickup",
          "mentionsDispatch"
        ],
        "properties": {
          "tone": {
            "type": "string",
            "enum": [
              "formal",
              "informal",
              "neutral"
            ],
            "description": "informal con ≥2 marcadores coloquiales colombianos (parce",
            "rey…); formal solo con >5 entrantes sin marcadores.": null
          },
          "isRecurringCustomer": {
            "type": "boolean",
            "description": "Más de 1 deal devuelto o ≥2 reaperturas automáticas."
          },
          "autoReopenCount": {
            "type": "integer"
          },
          "hoursSinceLastIncoming": {
            "type": [
              "integer",
              "null"
            ]
          },
          "lastIncomingHasReply": {
            "type": "boolean"
          },
          "mentionedPendingPayment": {
            "type": "boolean"
          },
          "mentionsPickup": {
            "type": "boolean"
          },
          "mentionsDispatch": {
            "type": "boolean"
          }
        }
      },
      "AgentContextData": {
        "type": "object",
        "description": "Contexto estructurado. Por esta ruta NUNCA vienen availableStages, availableAdvisors ni leadState (solo los recibe el orquestador de agentes internamente).",
        "required": [
          "contact",
          "conversation",
          "recentMessages",
          "deals",
          "pattern"
        ],
        "properties": {
          "contact": {
            "$ref": "#/components/schemas/AgentContextContact"
          },
          "conversation": {
            "$ref": "#/components/schemas/AgentContextConversation"
          },
          "recentMessages": {
            "type": "array",
            "description": "Los últimos messages mensajes, del más antiguo al más reciente.",
            "items": {
              "$ref": "#/components/schemas/AgentContextMessage"
            }
          },
          "deals": {
            "$ref": "#/components/schemas/AgentContextDeals"
          },
          "pattern": {
            "$ref": "#/components/schemas/AgentContextPattern"
          }
        }
      },
      "AgentContextFullResult": {
        "type": "object",
        "description": "Respuesta con format=full (por defecto).",
        "required": [
          "found",
          "data",
          "formattedPromptBlock",
          "estimatedTokens"
        ],
        "properties": {
          "found": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/AgentContextData"
          },
          "formattedPromptBlock": {
            "type": "string",
            "description": "El mismo contexto renderizado como bloque de texto para un system prompt."
          },
          "estimatedTokens": {
            "type": "integer",
            "description": "Caracteres del bloque / 4."
          }
        }
      },
      "AgentContextCompactResult": {
        "type": "object",
        "description": "Respuesta con format=compact.",
        "required": [
          "found",
          "estimatedTokens",
          "promptBlock"
        ],
        "properties": {
          "found": {
            "type": "boolean",
            "const": true
          },
          "estimatedTokens": {
            "type": "integer"
          },
          "promptBlock": {
            "type": "string"
          }
        }
      },
      "AgentContextNotFound": {
        "type": "object",
        "required": [
          "found",
          "reason",
          "mobile"
        ],
        "properties": {
          "found": {
            "type": "boolean",
            "const": false
          },
          "reason": {
            "type": "string",
            "const": "conversation_not_found"
          },
          "mobile": {
            "type": "string",
            "description": "El número ya normalizado a dígitos."
          }
        }
      },
      "AgentTestRequest": {
        "type": "object",
        "description": "mobile y message obligatorios (cadenas; 400 con message si faltan). El resto es opcional: systemPromptTemplate reemplaza la plantilla por defecto (que está escrita para Manillas de Control, no es genérica) y admite variables {{FECHA_ACTUAL}}, {{FECHA_CORTA}}, {{HORA_ACTUAL}}, {{HORA_AM_PM}}, {{DIA_SEMANA}}, {{ES_LABORAL}}, {{ES_FIN_DE_SEMANA}}; model (gpt-4o-mini), temperature (0.4), maxTokens (400), messagesLimit (6, acotado 1..30) y patternWindow (50, acotado a ≥ messagesLimit y ≤ 200). No usa la configuración de ningún agente guardado.",
        "required": [
          "mobile",
          "message"
        ],
        "properties": {
          "mobile": {
            "type": "string"
          },
          "message": {
            "type": "string",
            "description": "Último mensaje del cliente al que debe responder."
          },
          "systemPromptTemplate": {
            "type": "string"
          },
          "model": {
            "type": "string",
            "default": "gpt-4o-mini"
          },
          "temperature": {
            "type": "number",
            "default": 0.4
          },
          "maxTokens": {
            "type": "integer",
            "default": 400
          },
          "messagesLimit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 30,
            "default": 6
          },
          "patternWindow": {
            "type": "integer",
            "minimum": 5,
            "maximum": 200,
            "default": 50
          }
        }
      },
      "AgentTestResult": {
        "type": "object",
        "required": [
          "ok",
          "reply",
          "meta",
          "systemPromptPreview"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "reply": {
            "type": "string",
            "description": "Respuesta generada ('' si el modelo no devolvió contenido)."
          },
          "meta": {
            "type": "object",
            "required": [
              "model",
              "timings",
              "tokens"
            ],
            "properties": {
              "model": {
                "type": "string"
              },
              "contactName": {
                "type": "string"
              },
              "stage": {
                "type": "string"
              },
              "pattern": {
                "$ref": "#/components/schemas/AgentContextPattern"
              },
              "timings": {
                "type": "object",
                "required": [
                  "contextMs",
                  "llmMs",
                  "totalMs"
                ],
                "properties": {
                  "contextMs": {
                    "type": "integer"
                  },
                  "llmMs": {
                    "type": "integer"
                  },
                  "totalMs": {
                    "type": "integer"
                  }
                }
              },
              "tokens": {
                "type": "object",
                "required": [
                  "systemEstimated",
                  "userEstimated",
                  "contextOnlyEstimated"
                ],
                "properties": {
                  "systemEstimated": {
                    "type": "integer",
                    "description": "Caracteres del system prompt / 4."
                  },
                  "userEstimated": {
                    "type": "integer"
                  },
                  "contextOnlyEstimated": {
                    "type": "integer"
                  },
                  "openaiUsage": {
                    "type": "object",
                    "description": "Objeto usage de OpenAI tal cual (prompt_tokens, completion_tokens, total_tokens…).",
                    "additionalProperties": true
                  }
                }
              }
            }
          },
          "systemPromptPreview": {
            "type": "string",
            "description": "System prompt final (plantilla renderizada + contexto)",
            "truncado a 4000 caracteres.": null
          }
        }
      },
      "AgentContextError": {
        "type": "object",
        "description": "Contrato de error de agent-context (message en español, sin success). error/stack solo en algunos 500.",
        "required": [
          "message"
        ],
        "properties": {
          "message": {
            "type": "string"
          },
          "error": {
            "type": "string"
          },
          "stack": {
            "type": "string"
          }
        }
      },
      "ConversationStageEnteredPayload": {
        "description": "Cuerpo del POST que FusionCRM envía a la URL configurada en una etapa del\npipeline de conversaciones (Ajustes → Conversaciones → columna → \"Webhook\")\ncuando una conversación ENTRA a esa etapa, sin importar por dónde se movió\n(app, API externa, MCP o una automatización).\n",
        "type": "object",
        "required": [
          "event",
          "organizationId",
          "conversationId",
          "conversationTitle",
          "contact",
          "fromStage",
          "toStage",
          "pipeline",
          "triggeredBy",
          "timestamp"
        ],
        "properties": {
          "event": {
            "type": "string",
            "const": "conversation.stage.entered"
          },
          "organizationId": {
            "$ref": "#/components/schemas/AutomationTriggerBase/properties/organizationId"
          },
          "conversationId": {
            "$ref": "#/components/schemas/AutomationTriggerBase/properties/organizationId"
          },
          "conversationTitle": {
            "type": "string"
          },
          "contact": {
            "description": "`participants.contact.displayInfo` de la conversación (nombre, teléfono\ny demás datos visibles). Si la conversación no tiene displayInfo llega\n`{ reference }` con el id o teléfono del contacto.\n",
            "type": "object",
            "additionalProperties": true,
            "properties": {
              "name": {
                "type": "string"
              },
              "lastName": {
                "type": "string"
              },
              "mobile": {
                "type": "string"
              },
              "email": {
                "type": "string"
              },
              "position": {
                "type": "string"
              },
              "contactId": {
                "type": "string"
              },
              "reference": {
                "type": "string"
              }
            }
          },
          "assignedTo": {
            "description": "Id del usuario asignado, o null.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/AutomationTriggerBase/properties/organizationId"
              },
              {
                "type": "null"
              }
            ]
          },
          "priority": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high"
            ]
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "source": {
            "type": "string",
            "description": "Canal de origen de la conversación (whatsapp, messenger, instagram...)."
          },
          "fromStage": {
            "description": "Etapa anterior, o null si la conversación es nueva o venía de otro pipeline sin etapa.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/ConversationStageEnteredPayload/properties/toStage"
              },
              {
                "type": "null"
              }
            ]
          },
          "toStage": {
            "type": "object",
            "required": [
              "_id",
              "name",
              "order"
            ],
            "properties": {
              "_id": {
                "$ref": "#/components/schemas/AutomationTriggerBase/properties/organizationId"
              },
              "name": {
                "type": "string"
              },
              "order": {
                "type": "integer",
                "minimum": 0
              }
            }
          },
          "pipeline": {
            "type": "object",
            "required": [
              "_id",
              "name"
            ],
            "properties": {
              "_id": {
                "$ref": "#/components/schemas/AutomationTriggerBase/properties/organizationId"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "triggeredBy": {
            "type": "string",
            "enum": [
              "user",
              "automation",
              "system"
            ]
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AutomationTriggerBase": {
        "description": "Claves comunes de `trigger` en toda automatización. Las de cada evento se\ndocumentan en los schemas siguientes; en el builder de automatizaciones se\nreferencian como `{{trigger.<clave>}}`.\n",
        "type": "object",
        "required": [
          "organizationId"
        ],
        "properties": {
          "organizationId": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "contactId": {
            "description": "Id del contacto del CRM asociado al evento (ausente si no hay contacto).",
            "type": "string"
          },
          "conversationId": {
            "$ref": "#/components/schemas/AutomationTriggerBase/properties/organizationId"
          },
          "eventId": {
            "description": "Id estable del evento de origen. Sirve para deduplicar en el receptor:\n`deal:<dealId>:<toStatusId>`, `quotation:<quotationId>`,\n`invoice:<invoiceId>`, `form-response:<responseId>`,\n`convstage:<conversationId>:<toStageId>`.\n",
            "type": "string"
          },
          "correlation": {
            "type": "object",
            "properties": {
              "rootRunId": {
                "type": "string"
              },
              "correlationId": {
                "type": "string"
              },
              "depth": {
                "type": "integer",
                "minimum": 0
              }
            }
          }
        }
      },
      "DealStatusChangedTrigger": {
        "description": "Evento `deal_status_changed`: un negocio cambió de etapa (kanban, modal de\nedición, `PUT /deals/{id}` o MCP). Solo se emite si la etapa realmente cambió.\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/AutomationTriggerBase"
          },
          {
            "type": "object",
            "required": [
              "deal"
            ],
            "properties": {
              "deal": {
                "type": "object",
                "required": [
                  "dealId",
                  "toStatusId"
                ],
                "properties": {
                  "dealId": {
                    "$ref": "#/components/schemas/AutomationTriggerBase/properties/organizationId"
                  },
                  "fromStatusId": {
                    "description": "Etapa anterior. Ausente si el negocio no tenía etapa.",
                    "$ref": "#/components/schemas/AutomationTriggerBase/properties/organizationId"
                  },
                  "toStatusId": {
                    "$ref": "#/components/schemas/AutomationTriggerBase/properties/organizationId"
                  },
                  "pipelineId": {
                    "$ref": "#/components/schemas/AutomationTriggerBase/properties/organizationId"
                  }
                }
              }
            }
          }
        ]
      },
      "QuotationCreatedTrigger": {
        "description": "Evento `quotation_created`: se creó una cotización (app, `POST /quotations` o\nMCP). Claves PLANAS a propósito: cada una es una variable del builder.\n`quotation_total` viene formateado con la moneda de la organización;\n`quotation_total_raw` es el número que evalúan los filtros minTotal/maxTotal.\n`company_name` solo aparece cuando la cotización tiene empresa.\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/AutomationTriggerBase"
          },
          {
            "type": "object",
            "required": [
              "quotationId",
              "quotation_number",
              "quotation_name",
              "quotation_total",
              "quotation_total_raw",
              "quotation_status",
              "quotation_expiration",
              "quotation_items_summary",
              "quotation_items_count",
              "quotation_owner_id",
              "contact_name",
              "contact_email",
              "contact_mobile"
            ],
            "properties": {
              "quotationId": {
                "$ref": "#/components/schemas/AutomationTriggerBase/properties/organizationId"
              },
              "companyId": {
                "$ref": "#/components/schemas/AutomationTriggerBase/properties/organizationId"
              },
              "quotation_number": {
                "type": "integer"
              },
              "quotation_name": {
                "type": "string"
              },
              "quotation_total": {
                "type": "string",
                "examples": [
                  "$ 1.250.000"
                ]
              },
              "quotation_total_raw": {
                "type": "number"
              },
              "quotation_status": {
                "type": "string",
                "examples": [
                  "draft",
                  "sent",
                  "accepted",
                  "rejected"
                ]
              },
              "quotation_expiration": {
                "type": "string",
                "description": "Fecha de vencimiento formateada con el locale de la organización."
              },
              "quotation_items_summary": {
                "type": "string",
                "description": "\"2× Manilla tyvek, 1× Manilla vinilo\" (máximo 5 ítems, luego \"+N más\")."
              },
              "quotation_items_count": {
                "type": "integer",
                "minimum": 0
              },
              "quotation_owner_id": {
                "type": "string",
                "description": "Id del usuario dueño de la cotización."
              },
              "contact_name": {
                "type": "string"
              },
              "contact_email": {
                "type": "string"
              },
              "contact_mobile": {
                "type": "string"
              },
              "company_name": {
                "type": "string"
              }
            }
          }
        ]
      },
      "InvoiceCreatedTrigger": {
        "description": "Evento `invoice_created`: se emitió una factura de VENTA (`direction:\noutgoing`, tipo 1) desde la app, `POST /invoices` o MCP. Las notas crédito/\ndébito y las facturas de compra no disparan. Claves planas, mismo patrón que\n`quotation_created`.\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/AutomationTriggerBase"
          },
          {
            "type": "object",
            "required": [
              "invoiceId",
              "invoice_number",
              "invoice_reference",
              "invoice_prefix",
              "invoice_total",
              "invoice_total_raw",
              "invoice_date",
              "invoice_due_date",
              "invoice_payment_form",
              "invoice_status",
              "invoice_cufe",
              "invoice_items_count",
              "contact_name",
              "contact_email",
              "contact_mobile"
            ],
            "properties": {
              "invoiceId": {
                "$ref": "#/components/schemas/AutomationTriggerBase/properties/organizationId"
              },
              "invoice_number": {
                "type": "integer"
              },
              "invoice_reference": {
                "type": "string",
                "description": "Prefijo + número, como lo ve el cliente (\"FS-623\")."
              },
              "invoice_prefix": {
                "type": "string"
              },
              "invoice_total": {
                "type": "string"
              },
              "invoice_total_raw": {
                "type": "number"
              },
              "invoice_date": {
                "type": "string",
                "description": "Fecha de emisión enviada a la DIAN (YYYY-MM-DD)."
              },
              "invoice_due_date": {
                "type": "string",
                "description": "Vencimiento del pago; \"\" si es de contado."
              },
              "invoice_payment_form": {
                "type": "string",
                "enum": [
                  "contado",
                  "credito"
                ]
              },
              "invoice_status": {
                "type": "string",
                "description": "Estado tras la DIAN: valido | rechazado | sent."
              },
              "invoice_cufe": {
                "type": "string",
                "description": "CUFE de la DIAN; \"\" si aún no lo hay."
              },
              "invoice_items_count": {
                "type": "integer",
                "minimum": 0
              },
              "contact_name": {
                "type": "string"
              },
              "contact_email": {
                "type": "string"
              },
              "contact_mobile": {
                "type": "string"
              }
            }
          }
        ]
      },
      "FormSubmittedTrigger": {
        "description": "Evento `form_submitted`: alguien envió un formulario web de captación (página\npública, snippet embed, webhook de share o `POST /forms/{id}/submit`).\n`form_outcome` dice si el contacto se creó o ya existía. `responses` trae las\nrespuestas crudas por clave de campo.\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/AutomationTriggerBase"
          },
          {
            "type": "object",
            "required": [
              "formId",
              "responseId",
              "form_name",
              "form_slug",
              "form_outcome",
              "contact_name",
              "contact_email",
              "contact_mobile",
              "responses"
            ],
            "properties": {
              "formId": {
                "$ref": "#/components/schemas/AutomationTriggerBase/properties/organizationId"
              },
              "responseId": {
                "$ref": "#/components/schemas/AutomationTriggerBase/properties/organizationId"
              },
              "form_name": {
                "type": "string"
              },
              "form_slug": {
                "type": "string"
              },
              "form_outcome": {
                "type": "string",
                "enum": [
                  "created",
                  "updated"
                ]
              },
              "contact_name": {
                "type": "string"
              },
              "contact_email": {
                "type": "string"
              },
              "contact_mobile": {
                "type": "string"
              },
              "utm_source": {
                "type": "string"
              },
              "utm_medium": {
                "type": "string"
              },
              "utm_campaign": {
                "type": "string"
              },
              "page_url": {
                "type": "string"
              },
              "deal": {
                "description": "Presente solo si el formulario crea negocio.",
                "type": "object",
                "properties": {
                  "dealId": {
                    "$ref": "#/components/schemas/AutomationTriggerBase/properties/organizationId"
                  }
                }
              },
              "responses": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                }
              }
            }
          }
        ]
      },
      "AutomationHttpRequestBody": {
        "description": "Lo que recibe la URL de una acción \"Petición HTTP\" (`http_request`) de una\nautomatización. El cuerpo lo define quien arma la automatización, con\nvariables `{{trigger.*}}`, `{{contact.*}}` y `{{steps.*}}`; este schema\nmuestra la convención recomendada: reenviar el `trigger` completo.\n",
        "type": "object",
        "additionalProperties": true,
        "properties": {
          "event": {
            "type": "string",
            "description": "Tipo de disparador de la automatización.",
            "enum": [
              "inbound_message",
              "contact_created",
              "company_created",
              "deal_status_changed",
              "conversation_stage_changed",
              "stale_conversation",
              "calendar_event_upcoming",
              "quotation_created",
              "quotation_viewed",
              "quotation_status_changed",
              "invoice_created",
              "invoice_overdue",
              "form_submitted",
              "deal_stale",
              "repurchase_due"
            ]
          },
          "trigger": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DealStatusChangedTrigger"
              },
              {
                "description": "Evento `repurchase_due`: según la cadencia de sus negocios ganados, a un\ncontacto le \"toca\" volver a comprar (escaneo diario; un aviso por contacto\ny por ciclo de compra). Claves PLANAS: cada una es una variable del builder.\n`cadence_days` es la MEDIANA de los días entre compras; `days_until_expected`\nes negativo cuando la fecha esperada ya pasó. `deal.dealId` es el último\nnegocio ganado del contacto.\n",
                "allOf": [
                  {
                    "$ref": "#/components/schemas/AutomationTriggerBase"
                  },
                  {
                    "type": "object",
                    "required": [
                      "purchase_count",
                      "cadence_days",
                      "expected_purchase_date"
                    ],
                    "properties": {
                      "purchase_count": {
                        "type": "integer",
                        "example": 4
                      },
                      "cadence_days": {
                        "type": "integer",
                        "example": 50
                      },
                      "last_purchase_date": {
                        "type": "string",
                        "example": "6 sept 2026"
                      },
                      "last_purchase_amount": {
                        "type": "string",
                        "example": "$ 330.000"
                      },
                      "last_purchase_amount_raw": {
                        "type": "number",
                        "example": 330000
                      },
                      "expected_purchase_date": {
                        "type": "string",
                        "example": "26 oct 2026"
                      },
                      "days_since_last_purchase": {
                        "type": "integer",
                        "example": 43
                      },
                      "days_until_expected": {
                        "type": "integer",
                        "example": 7
                      },
                      "deal_owner_id": {
                        "description": "Dueño del último negocio ganado. Vacío si no tiene.",
                        "type": "string"
                      },
                      "contact_name": {
                        "type": "string"
                      },
                      "contact_email": {
                        "type": "string"
                      },
                      "contact_mobile": {
                        "type": "string"
                      },
                      "deal": {
                        "type": "object",
                        "properties": {
                          "dealId": {
                            "$ref": "#/components/schemas/AutomationTriggerBase/properties/organizationId"
                          },
                          "pipelineId": {
                            "$ref": "#/components/schemas/AutomationTriggerBase/properties/organizationId"
                          }
                        }
                      }
                    }
                  }
                ]
              },
              {
                "$ref": "#/components/schemas/QuotationCreatedTrigger"
              },
              {
                "$ref": "#/components/schemas/InvoiceCreatedTrigger"
              },
              {
                "$ref": "#/components/schemas/FormSubmittedTrigger"
              },
              {
                "$ref": "#/components/schemas/AutomationTriggerBase"
              }
            ]
          }
        }
      },
      "DealStage": {
        "type": "object",
        "description": "Etapa (columna del tablero) de un pipeline de negocios. Es el valor que va en el campo status de un negocio.",
        "required": [
          "id",
          "pipelineId",
          "name",
          "order",
          "color",
          "isWon",
          "isLost",
          "probability",
          "maxDaysInStage"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "pipelineId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "order": {
            "type": "integer",
            "description": "Posición en el tablero (0 = primera)."
          },
          "color": {
            "type": "string",
            "description": "Hexadecimal; #000000 si no se configuró."
          },
          "isWon": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "true/false marcado explícito; null = automático (se infiere del nombre: \"ganado\", \"won\"...)."
          },
          "isLost": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Igual que isWon, para negocios perdidos."
          },
          "probability": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 100,
            "description": "Probabilidad de cierre (forecast ponderado). null = sin configurar."
          },
          "maxDaysInStage": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "description": "Días máximos antes de marcar el negocio como estancado. null = sin límite."
          },
          "dealsCount": {
            "type": "integer",
            "minimum": 0,
            "description": "Negocios de la organización en la etapa (no viene en el reordenamiento)."
          }
        }
      },
      "DealPipeline": {
        "type": "object",
        "required": [
          "id",
          "title",
          "stageGateEnabled",
          "stageGateCumulative",
          "dealsCount",
          "stages",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "stageGateEnabled": {
            "type": "boolean",
            "description": "Requisitos por etapa (campos obligatorios y checklist) activos al mover negocios."
          },
          "stageGateCumulative": {
            "type": "boolean",
            "description": "Al avanzar a una etapa se exigen también los requisitos de las anteriores. Apágalo en pipelines con etapas paralelas."
          },
          "dealsCount": {
            "type": "integer",
            "minimum": 0,
            "description": "Negocios de la organización en el pipeline."
          },
          "stages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DealStage"
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "DealStageCreate": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "color": {
            "type": "string",
            "pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$"
          },
          "isWon": {
            "type": "boolean"
          },
          "isLost": {
            "type": "boolean"
          },
          "probability": {
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "description": "Se redondea a entero."
          },
          "maxDaysInStage": {
            "type": "integer",
            "minimum": 1
          }
        }
      },
      "DealStageUpdate": {
        "type": "object",
        "description": "Parcial; al menos un campo. null limpia isWon, isLost, probability o maxDaysInStage.",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "color": {
            "type": "string",
            "pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$"
          },
          "isWon": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "isLost": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "probability": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100
          },
          "maxDaysInStage": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1
          }
        }
      },
      "DealPipelineCreate": {
        "type": "object",
        "required": [
          "title"
        ],
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "stages": {
            "type": "array",
            "maxItems": 50,
            "description": "Etapas en orden; order se asigna por posición.",
            "items": {
              "$ref": "#/components/schemas/DealStageCreate"
            }
          }
        }
      },
      "DealPipelineUpdate": {
        "type": "object",
        "description": "Parcial; al menos un campo.",
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "stageGateEnabled": {
            "type": "boolean"
          },
          "stageGateCumulative": {
            "type": "boolean"
          }
        }
      },
      "DealStageReorder": {
        "type": "object",
        "required": [
          "stageIds"
        ],
        "properties": {
          "stageIds": {
            "type": "array",
            "description": "Todas las etapas del pipeline, cada una una vez, en el orden deseado.",
            "items": {
              "type": "string",
              "pattern": "^[a-fA-F0-9]{24}$"
            }
          }
        }
      },
      "DealPipelineResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/DealPipeline"
          }
        }
      },
      "DealPipelineListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DealPipeline"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "DealStageResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/DealStage"
          }
        }
      },
      "DealStageListResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DealStage"
            }
          }
        }
      },
      "DealPipelineDeleteResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "id",
              "deletedStages"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "deletedStages": {
                "type": "integer",
                "minimum": 0
              }
            }
          }
        }
      },
      "DealStageDeleteResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "id"
            ],
            "properties": {
              "id": {
                "type": "string"
              }
            }
          }
        }
      },
      "DealPipelineError": {
        "type": "object",
        "description": "Contrato de error de /deal-pipelines (success:false + error).",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          }
        }
      },
      "DealPipelineConflictError": {
        "type": "object",
        "description": "Borrado rechazado porque hay negocios; dealsCount dice cuántos.",
        "required": [
          "success",
          "error",
          "dealsCount"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          },
          "dealsCount": {
            "type": "integer",
            "minimum": 1
          }
        }
      },
      "ConversationPipeline": {
        "type": "object",
        "required": [
          "id",
          "name",
          "isDefault",
          "stages"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "isDefault": {
            "type": "boolean",
            "description": "Los mensajes entrantes nuevos caen en el pipeline predeterminado."
          },
          "stages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ConversationStage"
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ConversationStage": {
        "type": "object",
        "required": [
          "id",
          "pipelineId",
          "name",
          "order",
          "color",
          "autoAssign",
          "assignToTeam",
          "webhookEnabled",
          "webhookUrl",
          "reopenOnNewMessage"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "pipelineId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "order": {
            "type": "integer",
            "minimum": 0
          },
          "color": {
            "type": "string",
            "description": "Color de la columna (hex o clase heredada de la app)."
          },
          "autoAssign": {
            "type": "boolean",
            "description": "Al entrar a la etapa, la conversación se asigna a assignToTeam."
          },
          "assignToTeam": {
            "type": [
              "string",
              "null"
            ],
            "description": "userId de un empleado de la organización."
          },
          "webhookEnabled": {
            "type": "boolean"
          },
          "webhookUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Solo https. Recibe el webhook conversationStageEntered."
          },
          "webhookLastOkAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "webhookLastErrorAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "webhookLastError": {
            "type": [
              "string",
              "null"
            ]
          },
          "reopenOnNewMessage": {
            "type": "boolean",
            "description": "Un mensaje entrante en esta etapa devuelve la conversación a la primera."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ConversationStageInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 100
          },
          "order": {
            "type": "integer",
            "minimum": 0
          },
          "color": {
            "type": "string",
            "maxLength": 50
          },
          "autoAssign": {
            "type": "boolean"
          },
          "assignToTeam": {
            "type": [
              "string",
              "null"
            ]
          },
          "webhookEnabled": {
            "type": "boolean",
            "description": "true exige webhookUrl (nueva o ya guardada)."
          },
          "webhookUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "https obligatorio; null o \"\" la quita."
          },
          "reopenOnNewMessage": {
            "type": "boolean"
          }
        }
      },
      "ConversationPipelineError": {
        "type": "object",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "enum": [
              "VALIDATION",
              "LAST_PIPELINE",
              "PIPELINE_HAS_CONVERSATIONS",
              "LAST_STAGE",
              "TARGET_STAGE_REQUIRED"
            ]
          },
          "conversations": {
            "type": "integer",
            "description": "Conversaciones afectadas (en los 409 por conversaciones)."
          },
          "allowed": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "DealField": {
        "type": "object",
        "required": [
          "id",
          "pipelineId",
          "name",
          "key",
          "type",
          "options",
          "required",
          "requiredForStage",
          "enforcement"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "pipelineId": {
            "type": "string",
            "description": "Pipeline de negocios al que pertenece el campo (su tenant)."
          },
          "name": {
            "type": "string",
            "description": "Nombre visible."
          },
          "key": {
            "type": "string",
            "description": "Clave interna, única dentro del pipeline."
          },
          "type": {
            "type": "string",
            "description": "text, number, date, select o checkbox (campos antiguos pueden traer otros valores)."
          },
          "options": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Opciones de un campo select."
          },
          "required": {
            "type": "boolean",
            "description": "Obligatorio al crear el negocio y para cerrarlo ganado."
          },
          "requiredForStage": {
            "type": [
              "string",
              "null"
            ],
            "description": "Etapa (del mismo pipeline) a la que no se puede llegar sin este campo."
          },
          "enforcement": {
            "type": "string",
            "enum": [
              "block",
              "warn"
            ],
            "description": "block impide mover el negocio; warn solo avisa."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "DealFieldCreate": {
        "type": "object",
        "required": [
          "pipelineId",
          "name",
          "key"
        ],
        "properties": {
          "pipelineId": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "minLength": 1
          },
          "key": {
            "type": "string",
            "minLength": 1
          },
          "type": {
            "type": "string",
            "enum": [
              "text",
              "number",
              "date",
              "select",
              "checkbox"
            ],
            "default": "text"
          },
          "options": {
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1
            },
            "description": "Obligatorio (≥ 1) si type es select."
          },
          "required": {
            "type": "boolean",
            "default": false
          },
          "requiredForStage": {
            "type": [
              "string",
              "null"
            ]
          },
          "enforcement": {
            "type": "string",
            "enum": [
              "block",
              "warn"
            ],
            "default": "block"
          }
        }
      },
      "DealFieldUpdate": {
        "type": "object",
        "description": "Parcial — solo cambia lo que se envía.",
        "properties": {
          "pipelineId": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "minLength": 1
          },
          "key": {
            "type": "string",
            "minLength": 1
          },
          "type": {
            "type": "string",
            "enum": [
              "text",
              "number",
              "date",
              "select",
              "checkbox"
            ]
          },
          "options": {
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1
            }
          },
          "required": {
            "type": "boolean"
          },
          "requiredForStage": {
            "type": [
              "string",
              "null"
            ]
          },
          "enforcement": {
            "type": "string",
            "enum": [
              "block",
              "warn"
            ]
          }
        }
      },
      "DealFieldResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/DealField"
          }
        }
      },
      "DealFieldListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DealField"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "DealFieldDeleteResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "id",
              "deleted",
              "deletedValues"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "deleted": {
                "type": "boolean",
                "const": true
              },
              "deletedValues": {
                "type": "integer",
                "minimum": 0,
                "description": "Valores guardados en negocios que se borraron con el campo."
              }
            }
          }
        }
      },
      "DealFieldError": {
        "type": "object",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "description": "MISSING_FIELDS, DUPLICATE_KEY, INVALID_STAGE, PIPELINE_NOT_FOUND o FIELD_NOT_FOUND."
          }
        }
      },
      "Property": {
        "type": "object",
        "required": [
          "id",
          "key",
          "label",
          "type",
          "isVisible",
          "options",
          "groupId",
          "reserved",
          "required",
          "optionsLocked",
          "typeLocked",
          "systemRequired"
        ],
        "properties": {
          "id": {
            "type": [
              "string",
              "null"
            ]
          },
          "key": {
            "type": "string",
            "description": "Clave con la que se guarda el valor en el registro. Nunca cambia."
          },
          "label": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "input",
              "select",
              "textarea",
              "number",
              "date",
              "email",
              "phone",
              "catalog"
            ],
            "description": "`catalog` solo en las núcleo cityCode/stateCode: el valor es un código DANE; ver GET /catalogs/*."
          },
          "isVisible": {
            "type": "boolean",
            "deprecated": true,
            "description": "Deprecado (Épica Z): se devuelve tal como está guardado por compatibilidad. NO significa obligatorio; para eso está `required`. false = oculta (no hay DELETE)."
          },
          "options": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/PropertyOption"
            }
          },
          "catalog": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "co_municipalities",
              "co_states",
              null
            ],
            "description": "Solo cuando type es catalog: qué catálogo resuelve el valor."
          },
          "groupId": {
            "type": [
              "string",
              "null"
            ]
          },
          "reserved": {
            "type": "boolean",
            "description": "Propiedad núcleo del CRM: no cambia de type ni de key. Incluye las fiscales idType, idNumber y dv."
          },
          "required": {
            "type": "boolean",
            "description": "Obligatorio al crear/editar desde la UI web y la app; en API, MCP, importador y formularios solo avisa. Siempre presente (false por defecto)."
          },
          "optionsLocked": {
            "type": "boolean",
            "description": "true → las options las fija el registro del núcleo: cambiarlas responde 400 OPTIONS_LOCKED (reenviar las mismas es idempotente). true en cityCode/stateCode y en idType una vez migrada la org (select con los códigos DIAN)."
          },
          "typeLocked": {
            "type": "boolean",
            "description": "true → type/catalog los fija el registro: cambiarlos responde 400 TYPE_LOCKED. true en cityCode, stateCode, idType, dv, email, mobile y phone."
          },
          "systemRequired": {
            "type": "boolean",
            "description": "Solo lectura. true en firstName (contactos): required no se puede desmarcar."
          }
        }
      },
      "PropertyGroup": {
        "type": "object",
        "required": [
          "id",
          "title",
          "icon",
          "order"
        ],
        "properties": {
          "id": {
            "type": [
              "string",
              "null"
            ]
          },
          "title": {
            "type": "string"
          },
          "icon": {
            "type": [
              "string",
              "null"
            ]
          },
          "order": {
            "type": "integer"
          }
        }
      },
      "PropertyOption": {
        "oneOf": [
          {
            "type": "string",
            "minLength": 1
          },
          {
            "type": "object",
            "required": [
              "label",
              "value"
            ],
            "properties": {
              "label": {
                "type": "string",
                "minLength": 1
              },
              "value": {
                "type": "string",
                "minLength": 1
              }
            }
          }
        ]
      },
      "PropertyCreate": {
        "type": "object",
        "required": [
          "key",
          "label"
        ],
        "properties": {
          "key": {
            "type": "string",
            "pattern": "^[A-Za-z][A-Za-z0-9_]{0,63}$"
          },
          "label": {
            "type": "string",
            "minLength": 1
          },
          "type": {
            "type": "string",
            "enum": [
              "input",
              "select",
              "textarea",
              "number",
              "date",
              "email",
              "phone"
            ],
            "default": "input",
            "description": "`catalog` no se puede crear a mano (INVALID_TYPE): lo llevan solo las núcleo cityCode/stateCode."
          },
          "options": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PropertyOption"
            },
            "description": "Obligatorio (≥ 1) si type es select."
          },
          "isVisible": {
            "type": "boolean",
            "default": true,
            "deprecated": true,
            "description": "Deprecado: NO significa obligatorio (usar `required`)."
          },
          "required": {
            "type": "boolean",
            "default": false,
            "description": "Obligatorio al crear/editar desde la UI web y la app."
          },
          "groupId": {
            "type": "string",
            "description": "Default: el primer grupo."
          }
        }
      },
      "PropertyUpdate": {
        "type": "object",
        "description": "Parcial. La key va en la URL y no se puede cambiar; en propiedades reservadas tampoco el type.",
        "properties": {
          "label": {
            "type": "string",
            "minLength": 1
          },
          "isVisible": {
            "type": "boolean",
            "deprecated": true,
            "description": "Deprecado: false = ocultar. NO significa obligatorio (usar `required`)."
          },
          "required": {
            "type": "boolean",
            "description": "Obligatorio al crear/editar desde la UI web y la app. false en firstName (systemRequired) → 400 SYSTEM_REQUIRED; true en dv o en una de catálogo (cityCode/stateCode) → 400 REQUIRED_NOT_ALLOWED."
          },
          "type": {
            "type": "string",
            "enum": [
              "input",
              "select",
              "textarea",
              "number",
              "date",
              "email",
              "phone"
            ]
          },
          "options": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/PropertyOption"
            }
          },
          "groupId": {
            "type": "string"
          }
        }
      },
      "PropertyResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/Property"
          }
        }
      },
      "PropertyListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "groups",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Property"
            }
          },
          "groups": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PropertyGroup"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "PropertyError": {
        "type": "object",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "description": "INVALID_KEY, RESERVED_KEY, DUPLICATE_KEY, INVALID_TYPE, INVALID_GROUP, KEY_IMMUTABLE, RESERVED_TYPE, TYPE_LOCKED, OPTIONS_LOCKED, SYSTEM_REQUIRED, REQUIRED_NOT_ALLOWED, VALIDATION_ERROR o PROPERTY_NOT_FOUND."
          },
          "field": {
            "type": "string",
            "description": "Con TYPE_LOCKED (type), OPTIONS_LOCKED (options), SYSTEM_REQUIRED o REQUIRED_NOT_ALLOWED (required): el campo rechazado."
          },
          "allowed": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "reason": {
            "type": "string",
            "enum": [
              "dv",
              "catalog"
            ],
            "description": "Con REQUIRED_NOT_ALLOWED: por qué no puede ser obligatoria."
          }
        }
      },
      "EmailTemplate": {
        "type": "object",
        "required": [
          "id",
          "name",
          "saved",
          "blocksCount",
          "userId",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "maxLength": 200
          },
          "saved": {
            "type": "boolean",
            "description": "true = aparece en la galería de plantillas guardadas; false = borrador."
          },
          "blocksCount": {
            "type": "integer",
            "minimum": 0,
            "description": "Bloques del editor visual (0 si la plantilla es HTML crudo)."
          },
          "userId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Usuario que la creó (por API: el del token)."
          },
          "emailJson": {
            "type": "array",
            "description": "Solo en el detalle y en las respuestas de create/update. Bloques del editor visual, en orden.",
            "items": {
              "$ref": "#/components/schemas/EmailTemplateBlock"
            }
          },
          "emailHtml": {
            "type": "string",
            "description": "Solo en el detalle y en create/update. HTML final que se envía."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "EmailTemplateBlock": {
        "type": "object",
        "required": [
          "type"
        ],
        "description": "Un bloque del editor. Tipos: header {text}, text {content}, image {src, alt, widthMode?}, button {label, url, backgroundColor?, textColor?}, divider, spacer {height}, logo (pinta el logo de la organización), social {networks[]}, footer {text, unsubscribeMode?} (enlace de baja {{unsubscribe_url}}), twoCols {leftBlocks[], rightBlocks[]}.",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "description": "header, text, image, twoCols, divider, button, social, footer, spacer o logo. Un tipo desconocido no pinta nada."
          }
        },
        "additionalProperties": true
      },
      "EmailTemplateInput": {
        "type": "object",
        "description": "Precedencia: si llega emailJson, el HTML se RE-RENDERIZA desde los bloques con el logo de la organización y emailHtml se IGNORA. Si solo llega emailHtml, se sanitiza (fuera scripts, handlers on* y javascript:) y emailJson queda [] (el editor visual de la app no podrá mostrar ese contenido por bloques). El HTML resultante también se sanitiza.",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "emailJson": {
            "type": "array",
            "maxItems": 300,
            "description": "Máx 500 KB serializado.",
            "items": {
              "$ref": "#/components/schemas/EmailTemplateBlock"
            }
          },
          "emailHtml": {
            "type": "string",
            "description": "Máx 500 KB. Solo se usa si no llega emailJson."
          },
          "previewText": {
            "type": "string",
            "maxLength": 300,
            "description": "Texto de vista previa del cliente de correo. Solo aplica con emailJson."
          },
          "saved": {
            "type": "boolean"
          }
        }
      },
      "EmailTemplateCreateRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/EmailTemplateInput"
          },
          {
            "type": "object",
            "required": [
              "name"
            ],
            "anyOf": [
              {
                "required": [
                  "emailJson"
                ]
              },
              {
                "required": [
                  "emailHtml"
                ]
              }
            ]
          }
        ]
      },
      "EmailTemplateResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/EmailTemplate"
          }
        }
      },
      "EmailTemplateListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EmailTemplate"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "EmailTemplateDeleteResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "id",
              "deleted"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "deleted": {
                "type": "boolean",
                "const": true
              }
            }
          }
        }
      },
      "EmailTemplateError": {
        "type": "object",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          }
        }
      },
      "DocumentTemplate": {
        "type": "object",
        "required": [
          "id",
          "name",
          "category",
          "fileName",
          "isActive",
          "version",
          "etapasCount",
          "fieldsCount",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "fileName": {
            "type": "string",
            "description": "Nombre original del PDF base."
          },
          "isActive": {
            "type": "boolean"
          },
          "version": {
            "type": "integer",
            "minimum": 1,
            "description": "Sube cada vez que se reemplaza el PDF base."
          },
          "etapasCount": {
            "type": "integer",
            "minimum": 0
          },
          "fieldsCount": {
            "type": "integer",
            "minimum": 0
          },
          "etapas": {
            "type": "array",
            "description": "Solo en el detalle.",
            "items": {
              "$ref": "#/components/schemas/DocumentTemplateEtapa"
            }
          },
          "pdfFields": {
            "type": [
              "array",
              "null"
            ],
            "description": "Solo con includePdfFields=true. Campos AcroForm reales del PDF; null si no se pudo leer el archivo.",
            "items": {
              "type": "object",
              "required": [
                "name",
                "type"
              ],
              "properties": {
                "name": {
                  "type": "string"
                },
                "type": {
                  "type": "string"
                }
              }
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "DocumentTemplateField": {
        "type": "object",
        "required": [
          "label",
          "key",
          "pdfFieldName",
          "type",
          "required",
          "options",
          "valueTemplate"
        ],
        "properties": {
          "label": {
            "type": "string"
          },
          "key": {
            "type": "string"
          },
          "pdfFieldName": {
            "type": "string",
            "description": "Nombre del campo en el PDF: \"<etapa.label>.<key>\". Es la clave de values en generate."
          },
          "type": {
            "type": "string",
            "description": "Pista para la UI (Text, Select…); el motor solo escribe texto."
          },
          "required": {
            "type": "boolean"
          },
          "options": {
            "type": "string"
          },
          "valueTemplate": {
            "type": "string",
            "description": "Cómo se compone desde el contacto, p. ej. '{firstName} {lastName}'."
          }
        }
      },
      "DocumentTemplateEtapa": {
        "type": "object",
        "required": [
          "label",
          "fields"
        ],
        "properties": {
          "label": {
            "type": "string",
            "description": "Prefijo de los campos en el PDF."
          },
          "fields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DocumentTemplateField"
            }
          }
        }
      },
      "DocumentTemplateResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/DocumentTemplate"
          }
        }
      },
      "DocumentTemplateListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DocumentTemplate"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "DocumentTemplateGenerateRequest": {
        "type": "object",
        "required": [
          "values"
        ],
        "properties": {
          "values": {
            "type": "object",
            "minProperties": 1,
            "maxProperties": 500,
            "description": "Mapa plano { \"etapa.campo\": valor }. Una clave que termina en \"?\" es opcional: si el PDF no tiene ese campo no se reporta como faltante. Valores string, number, boolean o null (null = vacío); máx 5.000 caracteres cada uno.",
            "additionalProperties": {
              "type": [
                "string",
                "number",
                "boolean",
                "null"
              ]
            }
          },
          "flatten": {
            "type": "boolean",
            "default": false,
            "description": "true = aplana el formulario (el PDF queda no editable)."
          }
        }
      },
      "DocumentTemplateError": {
        "type": "object",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          },
          "missingFields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Solo en el 400 por campos requeridos que no existen en el PDF."
          }
        }
      },
      "ScoringRule": {
        "type": "object",
        "required": [
          "id",
          "name",
          "description",
          "isActive",
          "rules",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "isActive": {
            "type": "boolean",
            "description": "Solo las reglas activas suman puntos."
          },
          "rules": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ScoringRuleCondition"
            }
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "updatedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "ScoringRuleCondition": {
        "type": "object",
        "required": [
          "id",
          "propertyName",
          "condition",
          "value",
          "points"
        ],
        "properties": {
          "id": {
            "type": [
              "string",
              "null"
            ]
          },
          "propertyName": {
            "type": "string",
            "description": "Key de la propiedad de contacto; en condiciones de negocios, \"deal\" u otro texto informativo."
          },
          "condition": {
            "type": "string",
            "description": "Condición. De contacto (miran la propiedad propertyName): exists, equals, not_equals, contains, greater_than, less_than, in_list. De negocios (propertyName no se evalúa): last_deal_older_than, last_deal_newer_than, purchase_frequency_less_than (días), deal_amount_greater_than, deal_amount_less_than, total_deals_amount_greater_than (monto), total_deals_count_greater_than (cantidad), deal_status_is (id de etapa), deal_pipeline_is (id de pipeline), has_purchased_product (id de producto), deal_field_value_is ({ fieldId, value }).",
            "enum": [
              "exists",
              "equals",
              "not_equals",
              "contains",
              "greater_than",
              "less_than",
              "in_list",
              "last_deal_older_than",
              "last_deal_newer_than",
              "deal_amount_greater_than",
              "deal_amount_less_than",
              "deal_status_is",
              "deal_pipeline_is",
              "total_deals_count_greater_than",
              "total_deals_amount_greater_than",
              "has_purchased_product",
              "purchase_frequency_less_than",
              "deal_field_value_is"
            ]
          },
          "value": {
            "description": "Valor según la condición (ausente o null en exists).",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "number"
              },
              {
                "type": "boolean"
              },
              {
                "type": "null"
              },
              {
                "type": "array",
                "items": {
                  "oneOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "number"
                    },
                    {
                      "type": "boolean"
                    }
                  ]
                }
              },
              {
                "type": "object",
                "required": [
                  "fieldId",
                  "value"
                ],
                "properties": {
                  "fieldId": {
                    "type": "string"
                  },
                  "value": {
                    "oneOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "number"
                      },
                      {
                        "type": "boolean"
                      }
                    ]
                  }
                }
              }
            ]
          },
          "points": {
            "type": "number"
          }
        }
      },
      "ScoringRuleConditionInput": {
        "type": "object",
        "required": [
          "condition",
          "points"
        ],
        "properties": {
          "propertyName": {
            "type": "string",
            "description": "Obligatorio en condiciones de contacto; debe ser una propiedad de contacto de la organización (GET /contact-properties)."
          },
          "condition": {
            "$ref": "#/components/schemas/ScoringRuleCondition/properties/condition"
          },
          "value": {
            "$ref": "#/components/schemas/ScoringRuleCondition/properties/value"
          },
          "points": {
            "type": "number",
            "minimum": -10000,
            "maximum": 10000
          }
        }
      },
      "ScoringRuleCreate": {
        "type": "object",
        "required": [
          "name",
          "rules"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "description": {
            "type": "string",
            "maxLength": 1000
          },
          "isActive": {
            "type": "boolean",
            "default": true
          },
          "rules": {
            "type": "array",
            "minItems": 1,
            "maxItems": 50,
            "items": {
              "$ref": "#/components/schemas/ScoringRuleConditionInput"
            }
          }
        }
      },
      "ScoringRuleUpdate": {
        "type": "object",
        "minProperties": 1,
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 1000
          },
          "isActive": {
            "type": "boolean"
          },
          "rules": {
            "type": "array",
            "minItems": 1,
            "maxItems": 50,
            "description": "Reemplaza TODAS las condiciones de la regla.",
            "items": {
              "$ref": "#/components/schemas/ScoringRuleConditionInput"
            }
          }
        }
      },
      "ScoringRuleResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/ScoringRule"
          }
        }
      },
      "ScoringRuleListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ScoringRule"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "ScoringRuleDeleteResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "id"
            ],
            "properties": {
              "id": {
                "type": "string"
              }
            }
          }
        }
      },
      "ScoringRecalculationJob": {
        "type": "object",
        "required": [
          "jobId",
          "status",
          "startedAt",
          "finishedAt",
          "durationMs",
          "error"
        ],
        "properties": {
          "jobId": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "processing",
              "completed",
              "failed"
            ]
          },
          "startedAt": {
            "type": "string",
            "format": "date-time"
          },
          "finishedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "durationMs": {
            "type": [
              "integer",
              "null"
            ]
          },
          "error": {
            "type": [
              "string",
              "null"
            ]
          },
          "alreadyRunning": {
            "type": "boolean",
            "description": "Solo en POST /recalculate — true si ya había un recálculo en curso y se devolvió ese."
          }
        }
      },
      "ScoringRecalculationResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/ScoringRecalculationJob"
          }
        }
      },
      "ScoringRuleError": {
        "type": "object",
        "description": "Contrato de error de /scoring-rules (success:false + error).",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          }
        }
      },
      "DealChecklist": {
        "type": "object",
        "required": [
          "id",
          "pipelineId",
          "items",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": [
              "string",
              "null"
            ],
            "description": "null si el pipeline aún no tiene checklist."
          },
          "pipelineId": {
            "type": "string"
          },
          "pipelineTitle": {
            "type": [
              "string",
              "null"
            ],
            "description": "Solo en el listado."
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DealChecklistItem"
            }
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "updatedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "DealChecklistItem": {
        "type": "object",
        "required": [
          "id",
          "name",
          "weight",
          "activityType",
          "requiredForStage",
          "enforcement"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Id del ítem; las marcas de los negocios lo referencian."
          },
          "name": {
            "type": "string"
          },
          "weight": {
            "type": "integer",
            "minimum": 0,
            "maximum": 1000,
            "description": "Peso relativo; el % de avance es sobre la suma de pesos."
          },
          "activityType": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "llamada",
              "mensaje",
              "reunion",
              "visita",
              "correo",
              "whatsapp",
              "nota",
              null
            ],
            "description": "Registrar una actividad de este tipo en el negocio marca el ítem sola."
          },
          "requiredForStage": {
            "type": [
              "string",
              "null"
            ],
            "description": "Etapa del pipeline a la que el negocio no puede llegar sin el ítem marcado. null = informativo."
          },
          "enforcement": {
            "type": "string",
            "enum": [
              "block",
              "warn"
            ],
            "description": "block impide la transición; warn solo avisa."
          }
        }
      },
      "DealChecklistItemInput": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Id de un ítem existente para conservarlo (y sus marcas)."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "weight": {
            "type": "number",
            "minimum": 0,
            "maximum": 1000,
            "default": 1,
            "description": "Se redondea a entero."
          },
          "activityType": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "llamada",
              "mensaje",
              "reunion",
              "visita",
              "correo",
              "whatsapp",
              "nota",
              null
            ]
          },
          "requiredForStage": {
            "type": [
              "string",
              "null"
            ],
            "description": "Id de una etapa de ESTE pipeline."
          },
          "enforcement": {
            "type": "string",
            "enum": [
              "block",
              "warn"
            ],
            "default": "block"
          }
        }
      },
      "DealChecklistUpdate": {
        "type": "object",
        "required": [
          "items"
        ],
        "properties": {
          "items": {
            "type": "array",
            "maxItems": 50,
            "description": "Lista COMPLETA; los ítems que no vengan se eliminan.",
            "items": {
              "$ref": "#/components/schemas/DealChecklistItemInput"
            }
          }
        }
      },
      "DealChecklistResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/DealChecklist"
          }
        }
      },
      "DealChecklistListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DealChecklist"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "DealChecklistError": {
        "type": "object",
        "description": "Contrato de error de /deal-checklists (success:false + error).",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          }
        }
      },
      "ProductVariantRecord": {
        "type": "object",
        "required": [
          "id",
          "productId",
          "sku",
          "price",
          "stock",
          "imageUrl",
          "isActive",
          "attributeValues",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "productId": {
            "type": "string"
          },
          "sku": {
            "type": "string",
            "description": "Único en la organización."
          },
          "price": {
            "type": "number"
          },
          "stock": {
            "type": [
              "integer",
              "null"
            ]
          },
          "imageUrl": {
            "type": [
              "string",
              "null"
            ]
          },
          "isActive": {
            "type": "boolean"
          },
          "attributeValues": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProductVariantRecordAttribute"
            }
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "updatedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "ProductVariantRecordAttribute": {
        "type": "object",
        "required": [
          "attributeName",
          "value",
          "displayName",
          "valueId",
          "price",
          "imageUrl"
        ],
        "properties": {
          "attributeName": {
            "type": "string",
            "description": "p. ej. Color"
          },
          "value": {
            "type": "string",
            "description": "p. ej. Rojo"
          },
          "displayName": {
            "type": "string"
          },
          "valueId": {
            "type": [
              "string",
              "null"
            ]
          },
          "price": {
            "type": [
              "number",
              "null"
            ]
          },
          "imageUrl": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "ProductVariantAttributeInput": {
        "type": "object",
        "required": [
          "attributeName",
          "value"
        ],
        "properties": {
          "attributeName": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "value": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "displayName": {
            "type": "string",
            "maxLength": 200,
            "description": "Default = value."
          },
          "valueId": {
            "type": "string",
            "maxLength": 200
          },
          "price": {
            "type": "number",
            "minimum": 0
          },
          "imageUrl": {
            "type": "string",
            "maxLength": 200
          }
        }
      },
      "ProductVariantCreate": {
        "type": "object",
        "required": [
          "productId",
          "sku",
          "price"
        ],
        "properties": {
          "productId": {
            "type": "string",
            "description": "Producto padre (de la organización)."
          },
          "sku": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "price": {
            "type": "number",
            "minimum": 0
          },
          "stock": {
            "type": "integer",
            "minimum": 0
          },
          "imageUrl": {
            "type": "string",
            "maxLength": 200
          },
          "isActive": {
            "type": "boolean",
            "default": true
          },
          "attributeValues": {
            "type": "array",
            "maxItems": 20,
            "items": {
              "$ref": "#/components/schemas/ProductVariantAttributeInput"
            }
          }
        }
      },
      "ProductVariantUpdate": {
        "type": "object",
        "minProperties": 1,
        "properties": {
          "sku": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "price": {
            "type": "number",
            "minimum": 0
          },
          "stock": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0
          },
          "imageUrl": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200
          },
          "isActive": {
            "type": "boolean"
          },
          "attributeValues": {
            "type": "array",
            "maxItems": 20,
            "description": "Reemplaza la lista completa.",
            "items": {
              "$ref": "#/components/schemas/ProductVariantAttributeInput"
            }
          }
        }
      },
      "ProductVariantResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/ProductVariantRecord"
          }
        }
      },
      "ProductVariantListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProductVariantRecord"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "ProductVariantDeleteResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "id"
            ],
            "properties": {
              "id": {
                "type": "string"
              }
            }
          }
        }
      },
      "ProductVariantError": {
        "type": "object",
        "description": "Contrato de error de /product-variants (success:false + error; code DUPLICATE_SKU en el 409).",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string"
          }
        }
      },
      "InvoiceConfig": {
        "type": "object",
        "description": "Configuración de facturación electrónica (DIAN) en LISTA BLANCA. Ningún secreto sale (ni enmascarado): contraseñas de correo, clave técnica, token del proveedor, PIN del software y certificado quedan en el servidor; a lo sumo un booleano dice si existen. Los campos de texto vacíos salen como null.",
        "required": [
          "id",
          "environment",
          "company",
          "invoiceResolution",
          "creditNote",
          "debitNote",
          "supportDocument",
          "certificate",
          "software",
          "provider",
          "smtp",
          "imap",
          "placeholders"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Id del documento de configuración (inv_…)."
          },
          "environment": {
            "type": "string",
            "enum": [
              "production",
              "test"
            ],
            "description": "production = factura ante la DIAN en producción; test = pruebas/habilitación."
          },
          "company": {
            "type": "object",
            "description": "Empresa emisora.",
            "properties": {
              "businessName": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "nit": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "dv": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Dígito de verificación."
              },
              "email": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "phone": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "address": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "municipalityId": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Id del municipio en el catálogo del proveedor de facturación (149 = Bogotá)",
                "no el código DANE.": null
              },
              "typeDocumentIdentificationId": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Id del tipo de documento en el catálogo del proveedor de facturación (6 = NIT, 3 = cédula), no el código DIAN. Configuraciones anteriores al 01-oct-2026 pueden traer todavía el código DIAN (\"31\"); se corrige solo al guardar la empresa desde Ajustes → Facturación."
              },
              "typeOrganizationId": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Id del tipo de organización: 1 = persona jurídica, 2 = persona natural."
              },
              "typeRegimeId": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Id del régimen en el catálogo del proveedor: 1 = responsable de IVA (código DIAN 48), 2 = no responsable (49). Configuraciones anteriores al 01-oct-2026 pueden traer el código (\"48\")."
              },
              "typeLiabilityId": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Id de la responsabilidad fiscal en el catálogo del proveedor: 7 = O-13, 9 = O-15, 14 = O-23, 112 = O-47, 117 = R-99-PN. Configuraciones anteriores al 01-oct-2026 pueden traer el código (\"O-13\")."
              }
            }
          },
          "invoiceResolution": {
            "type": "object",
            "description": "Resolución de facturación vigente y el siguiente consecutivo.",
            "properties": {
              "typeDocumentId": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "prefix": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "resolution": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "resolutionDate": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "from": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Primer número autorizado."
              },
              "to": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Último número autorizado."
              },
              "dateFrom": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Inicio de vigencia (YYYY-MM-DD)."
              },
              "dateTo": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Fin de vigencia (YYYY-MM-DD)."
              },
              "nextNumber": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Número que llevará la próxima factura."
              },
              "hasTechnicalKey": {
                "type": "boolean",
                "description": "Si la clave técnica de la resolución está guardada (el valor nunca sale)."
              }
            }
          },
          "creditNote": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/InvoiceConfigNoteNumbering"
              },
              {
                "type": "null"
              }
            ]
          },
          "debitNote": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/InvoiceConfigNoteNumbering"
              },
              {
                "type": "null"
              }
            ]
          },
          "supportDocument": {
            "oneOf": [
              {
                "type": "object",
                "description": "Numeración del documento soporte (compras a no obligados a facturar).",
                "properties": {
                  "prefix": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "resolution": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "resolutionDate": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "from": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "to": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "dateFrom": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "dateTo": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "nextNumber": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "certificate": {
            "type": "object",
            "properties": {
              "uploaded": {
                "type": "boolean",
                "description": "Si hay un certificado digital real cargado."
              },
              "expirationDate": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Vencimiento (YYYY-MM-DD) si se registró."
              }
            }
          },
          "software": {
            "type": "object",
            "properties": {
              "configured": {
                "type": "boolean",
                "description": "Si el software DIAN (id y PIN) está registrado."
              }
            }
          },
          "provider": {
            "type": "object",
            "properties": {
              "hasToken": {
                "type": "boolean",
                "description": "Si hay token del proveedor de facturación."
              }
            }
          },
          "smtp": {
            "type": "object",
            "description": "Correo de ENVÍO de las facturas.",
            "properties": {
              "configured": {
                "type": "boolean",
                "description": "host, usuario y contraseña presentes."
              },
              "host": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "port": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "username": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "encryption": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "ssl | tls | none"
              },
              "hasPassword": {
                "type": "boolean"
              }
            }
          },
          "imap": {
            "description": "Buzón de RECEPCIÓN de facturas de proveedores; null si nunca se configuró.",
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  },
                  "configured": {
                    "type": "boolean"
                  },
                  "host": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "port": {
                    "type": [
                      "integer",
                      "null"
                    ]
                  },
                  "username": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "tls": {
                    "type": "boolean"
                  },
                  "mailbox": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "autoProcess": {
                    "type": "boolean"
                  },
                  "markAsRead": {
                    "type": "boolean"
                  },
                  "deleteAfterProcess": {
                    "type": "boolean"
                  },
                  "hasPassword": {
                    "type": "boolean"
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "placeholders": {
            "type": "object",
            "description": "Textos por defecto de la factura y del correo que la acompaña.",
            "properties": {
              "paymentTerms": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "currency": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "notes": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "logoImg": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "headNote": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "footNote": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "shippingTerms": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "subjectMail": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "messageMail": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          }
        }
      },
      "InvoiceConfigNoteNumbering": {
        "type": "object",
        "description": "Numeración de notas crédito o débito.",
        "properties": {
          "prefix": {
            "type": [
              "string",
              "null"
            ]
          },
          "resolution": {
            "type": [
              "string",
              "null"
            ]
          },
          "nextNumber": {
            "type": [
              "string",
              "null"
            ]
          },
          "headNote": {
            "type": [
              "string",
              "null"
            ]
          },
          "footNote": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "InvoiceConfigResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/InvoiceConfig"
          }
        }
      },
      "InvoiceConfigError": {
        "type": "object",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          }
        }
      },
      "Integration": {
        "type": "object",
        "description": "Integración conectada, en LISTA BLANCA. Nunca incluye credenciales (ni en claro, ni cifradas, ni enmascaradas) ni settings: configuredKeys trae solo los NOMBRES de las credenciales con valor.",
        "required": [
          "id",
          "service",
          "name",
          "isActive",
          "configuredKeys",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "service": {
            "type": "string",
            "description": "whatsapp, messenger, facebook, instagram, openai, sendinblue, notion, stripe, claude, n8n, brevo, elevenlabs, googleAnalytics."
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "isActive": {
            "type": "boolean"
          },
          "configuredKeys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Nombres de las credenciales guardadas, en orden alfabético."
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "updatedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "IntegrationResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/Integration"
          }
        }
      },
      "IntegrationListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Integration"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "IntegrationError": {
        "type": "object",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          }
        }
      },
      "MailAccount": {
        "type": "object",
        "description": "Cuenta de correo IMAP de la bandeja compartida, en LISTA BLANCA. Nunca incluye la contraseña.",
        "required": [
          "id",
          "email",
          "provider",
          "status",
          "isActive",
          "lastError",
          "lastErrorCode",
          "lastCheckedAt",
          "lastSyncAt",
          "syncing",
          "imapSettings",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "provider": {
            "type": [
              "string",
              "null"
            ],
            "description": "gmail, zoho, custom… (catálogo de la app)."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "error",
              "inactive"
            ]
          },
          "isActive": {
            "type": "boolean",
            "description": "status === 'active'."
          },
          "lastError": {
            "type": [
              "string",
              "null"
            ],
            "description": "Mensaje del último error de conexión o sincronización."
          },
          "lastErrorCode": {
            "type": [
              "string",
              "null"
            ],
            "description": "Código estable: auth_failed, oauth_required, timeout…"
          },
          "lastCheckedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "lastSyncAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "syncing": {
            "type": "boolean",
            "description": "Hay una sincronización en curso (se ignora si lleva más de 15 minutos)."
          },
          "imapSettings": {
            "type": "object",
            "properties": {
              "host": {
                "type": "string"
              },
              "port": {
                "type": "integer"
              },
              "user": {
                "type": "string"
              },
              "tls": {
                "type": "boolean"
              }
            }
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "updatedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "MailAccountResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/MailAccount"
          }
        }
      },
      "MailAccountListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MailAccount"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "MailAccountError": {
        "type": "object",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          }
        }
      },
      "Activity": {
        "type": "object",
        "description": "Seguimiento registrado en la ficha de un contacto y/o empresa (llamada, reunión, correo, nota o WhatsApp). Siempre tiene contactId o companyId (o ambos).",
        "required": [
          "id",
          "activityType",
          "title",
          "date",
          "notes",
          "status",
          "contactId",
          "companyId",
          "ownerId",
          "advisorId",
          "reminder",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "activityType": {
            "type": "string",
            "description": "Reunion, Llamada, Correo, Nota o Whatsapp (registros viejos pueden traer otro texto)."
          },
          "title": {
            "type": "string"
          },
          "date": {
            "type": "string",
            "description": "YYYY-MM-DD (todo el día, formularios de la web) o fecha-hora ISO 8601."
          },
          "notes": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "incomplete",
              "complete"
            ]
          },
          "contactId": {
            "type": [
              "string",
              "null"
            ]
          },
          "companyId": {
            "type": [
              "string",
              "null"
            ]
          },
          "ownerId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Quién lo registró (con la API, el dueño del token)."
          },
          "advisorId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Asesor responsable; recibe el recordatorio."
          },
          "reminder": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Fecha-hora del aviso (push + correo) al asesor, o a quien lo registró si no hay asesor."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ActivityCreate": {
        "type": "object",
        "description": "Debe traer contactId o companyId (de la organización del token).",
        "required": [
          "activityType",
          "title",
          "date"
        ],
        "properties": {
          "activityType": {
            "type": "string",
            "enum": [
              "Reunion",
              "Llamada",
              "Correo",
              "Nota",
              "Whatsapp"
            ]
          },
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "date": {
            "type": "string",
            "description": "YYYY-MM-DD o fecha-hora ISO 8601."
          },
          "notes": {
            "type": "string",
            "maxLength": 10000
          },
          "status": {
            "type": "string",
            "enum": [
              "incomplete",
              "complete"
            ],
            "default": "incomplete"
          },
          "contactId": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "companyId": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "advisorId": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$",
            "description": "Empleado de la organización."
          },
          "reminder": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "ActivityUpdate": {
        "type": "object",
        "description": "Parcial; al menos un campo. null en reminder lo quita; null en contactId, companyId o advisorId desvincula (siempre debe quedar contacto o empresa).",
        "properties": {
          "activityType": {
            "type": "string",
            "enum": [
              "Reunion",
              "Llamada",
              "Correo",
              "Nota",
              "Whatsapp"
            ]
          },
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "date": {
            "type": "string"
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "status": {
            "type": "string",
            "enum": [
              "incomplete",
              "complete"
            ]
          },
          "contactId": {
            "type": [
              "string",
              "null"
            ]
          },
          "companyId": {
            "type": [
              "string",
              "null"
            ]
          },
          "advisorId": {
            "type": [
              "string",
              "null"
            ]
          },
          "reminder": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "ActivityResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/Activity"
          }
        }
      },
      "ActivityListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Activity"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "ActivityDeleteResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "id"
            ],
            "properties": {
              "id": {
                "type": "string"
              }
            }
          }
        }
      },
      "ActivityError": {
        "type": "object",
        "description": "Contrato de error de /activities (success:false + error).",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          }
        }
      },
      "CalendarEvent": {
        "type": "object",
        "description": "Evento, ocurrencia de una serie (occurrenceStart no nulo; id = el de la serie) o seguimiento de contacto como evento de solo lectura (source activity, id activity:<id>).",
        "required": [
          "id",
          "source",
          "title",
          "description",
          "start",
          "end",
          "allDay",
          "timezone",
          "type",
          "tags",
          "location",
          "attendees",
          "assignedTo",
          "createdBy",
          "contactId",
          "contactName",
          "reminderEnabled",
          "reminderMinutesBefore",
          "recurrenceRule",
          "occurrenceStart",
          "recurringEventId",
          "externalId",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "source": {
            "type": "string",
            "enum": [
              "event",
              "activity"
            ]
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "start": {
            "type": "string",
            "format": "date-time"
          },
          "end": {
            "type": "string",
            "format": "date-time"
          },
          "allDay": {
            "type": "boolean"
          },
          "timezone": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "description": "reunion, tarea, recordatorio (o seguimiento si source es activity)."
          },
          "tags": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CalendarTag"
            }
          },
          "location": {
            "type": "string"
          },
          "attendees": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CalendarAttendee"
            }
          },
          "assignedTo": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdBy": {
            "type": [
              "string",
              "null"
            ]
          },
          "contactId": {
            "type": [
              "string",
              "null"
            ]
          },
          "contactName": {
            "type": [
              "string",
              "null"
            ]
          },
          "reminderEnabled": {
            "type": "boolean"
          },
          "reminderMinutesBefore": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "integer"
            }
          },
          "recurrenceRule": {
            "type": "string",
            "description": "RRULE sin prefijo; vacío = no se repite."
          },
          "occurrenceStart": {
            "type": [
              "string",
              "null"
            ],
            "description": "Solo en ocurrencias expandidas del listado."
          },
          "seriesStart": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "seriesEnd": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "recurringEventId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Serie de origen (ocurrencia expandida o desprendida)."
          },
          "originalStart": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "excludedDates": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "date-time"
            }
          },
          "externalId": {
            "type": "string"
          },
          "invitationSentAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "activityId": {
            "type": "string"
          },
          "activityType": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "companyId": {
            "type": [
              "string",
              "null"
            ]
          },
          "companyName": {
            "type": [
              "string",
              "null"
            ]
          },
          "invitations": {
            "$ref": "#/components/schemas/CalendarInvitationResult"
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "updatedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "CalendarTag": {
        "type": "object",
        "required": [
          "label",
          "color"
        ],
        "properties": {
          "label": {
            "type": "string",
            "maxLength": 50
          },
          "color": {
            "type": "string",
            "maxLength": 20
          }
        }
      },
      "CalendarAttendee": {
        "type": "object",
        "required": [
          "email",
          "name",
          "status",
          "contactId",
          "userId"
        ],
        "properties": {
          "email": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "needs_action",
              "accepted",
              "declined",
              "tentative"
            ]
          },
          "contactId": {
            "type": [
              "string",
              "null"
            ]
          },
          "userId": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "CalendarAttendeeInput": {
        "type": "object",
        "required": [
          "email"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "name": {
            "type": "string",
            "maxLength": 120
          },
          "contactId": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$",
            "description": "Contacto de la organización."
          },
          "userId": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$",
            "description": "Empleado de la organización."
          }
        }
      },
      "CalendarInvitationResult": {
        "type": "object",
        "description": "Resultado del envío de correos a los invitados (solo si se pidió enviar).",
        "required": [
          "sent",
          "failed",
          "skipped",
          "sequence"
        ],
        "properties": {
          "sent": {
            "type": "integer",
            "minimum": 0
          },
          "failed": {
            "type": "integer",
            "minimum": 0
          },
          "skipped": {
            "type": "integer",
            "minimum": 0
          },
          "sequence": {
            "type": "integer",
            "minimum": 0,
            "description": "Revisión (SEQUENCE) del .ics enviado."
          }
        }
      },
      "CalendarEventCreate": {
        "type": "object",
        "required": [
          "title",
          "start",
          "end"
        ],
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "start": {
            "type": "string",
            "format": "date-time"
          },
          "end": {
            "type": "string",
            "format": "date-time",
            "description": "No anterior a start."
          },
          "description": {
            "type": "string",
            "maxLength": 5000
          },
          "allDay": {
            "type": "boolean"
          },
          "timezone": {
            "type": "string",
            "default": "America/Bogota",
            "description": "Zona IANA; manda en la expansión de recurrencias."
          },
          "type": {
            "type": "string",
            "enum": [
              "reunion",
              "tarea",
              "recordatorio"
            ]
          },
          "location": {
            "type": "string",
            "maxLength": 500
          },
          "tags": {
            "type": "array",
            "maxItems": 20,
            "items": {
              "$ref": "#/components/schemas/CalendarTag"
            }
          },
          "attendees": {
            "type": "array",
            "maxItems": 50,
            "items": {
              "$ref": "#/components/schemas/CalendarAttendeeInput"
            }
          },
          "sendInvitations": {
            "type": "boolean",
            "default": false,
            "description": "true = ENVÍA un correo con la invitación (.ics) a cada invitado. Por defecto no se envía nada."
          },
          "assignedTo": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "contactId": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{24}$"
          },
          "reminderEnabled": {
            "type": "boolean"
          },
          "reminderMinutesBefore": {
            "type": "array",
            "minItems": 1,
            "maxItems": 5,
            "items": {
              "type": "integer",
              "minimum": 0,
              "maximum": 10080
            }
          },
          "recurrenceRule": {
            "type": "string",
            "description": "RRULE RFC 5545 sin prefijo, FREQ diario o mayor."
          },
          "externalId": {
            "type": "string",
            "maxLength": 255
          }
        }
      },
      "CalendarEventUpdate": {
        "type": "object",
        "description": "Parcial; al menos un campo. null en assignedTo/contactId los quita; recurrenceRule vacío o null deja de repetir. attendees reemplaza la lista completa.",
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "start": {
            "type": "string",
            "format": "date-time"
          },
          "end": {
            "type": "string",
            "format": "date-time"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 5000
          },
          "allDay": {
            "type": "boolean"
          },
          "timezone": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "reunion",
              "tarea",
              "recordatorio"
            ]
          },
          "location": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 500
          },
          "tags": {
            "type": "array",
            "maxItems": 20,
            "items": {
              "$ref": "#/components/schemas/CalendarTag"
            }
          },
          "attendees": {
            "type": "array",
            "maxItems": 50,
            "items": {
              "$ref": "#/components/schemas/CalendarAttendeeInput"
            }
          },
          "sendInvitations": {
            "type": "boolean",
            "default": false,
            "description": "true = reenvía la invitación actualizada y cancela a los invitados quitados (correos)."
          },
          "assignedTo": {
            "type": [
              "string",
              "null"
            ]
          },
          "contactId": {
            "type": [
              "string",
              "null"
            ]
          },
          "reminderEnabled": {
            "type": "boolean"
          },
          "reminderMinutesBefore": {
            "type": "array",
            "minItems": 1,
            "maxItems": 5,
            "items": {
              "type": "integer",
              "minimum": 0,
              "maximum": 10080
            }
          },
          "recurrenceRule": {
            "type": [
              "string",
              "null"
            ]
          },
          "externalId": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 255
          }
        }
      },
      "CalendarEventResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/CalendarEvent"
          }
        }
      },
      "CalendarEventListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination",
          "range"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CalendarEvent"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          },
          "range": {
            "type": "object",
            "required": [
              "from",
              "to",
              "clamped"
            ],
            "properties": {
              "from": {
                "type": "string",
                "format": "date-time"
              },
              "to": {
                "type": "string",
                "format": "date-time"
              },
              "clamped": {
                "type": "boolean",
                "description": "true si el rango pedido superaba el máximo y se recortó."
              }
            }
          }
        }
      },
      "CalendarEventDeleteResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "id",
              "scope"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "scope": {
                "type": "string",
                "enum": [
                  "event",
                  "series",
                  "occurrence"
                ],
                "description": "series = se borró la serie completa con sus ocurrencias desprendidas; occurrence = solo esa fecha."
              },
              "occurrenceStart": {
                "type": "string",
                "format": "date-time"
              },
              "detachedDeleted": {
                "type": "integer",
                "minimum": 0
              },
              "invitations": {
                "$ref": "#/components/schemas/CalendarInvitationResult"
              }
            }
          }
        }
      },
      "CalendarEventError": {
        "type": "object",
        "description": "Contrato de error de /calendar-events (success:false + error; code en rango inválido).",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string"
          }
        }
      },
      "Notification": {
        "type": "object",
        "required": [
          "id",
          "type",
          "title",
          "body",
          "targetUrl",
          "createdAt",
          "readAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "task_mention",
              "task_reply",
              "task_comment_reminder",
              "email_assigned",
              "calendar_reminder",
              "deal_reminder",
              "activity_reminder",
              "task_due_date",
              "form_submission",
              "deal_assigned",
              "task_assigned",
              "conversation_assigned",
              "quotation_viewed",
              "automation_error",
              "account_limit_warning",
              "account_restricted",
              "payment_failed",
              "quotation_accepted",
              "quotation_rejected"
            ]
          },
          "title": {
            "type": "string"
          },
          "body": {
            "type": "string",
            "description": "Extracto (máx. 140 caracteres)."
          },
          "entity": {
            "type": [
              "object",
              "null"
            ],
            "description": "Registro al que apunta la notificación.",
            "properties": {
              "kind": {
                "type": "string"
              },
              "id": {
                "type": "string"
              }
            }
          },
          "category": {
            "type": [
              "string",
              "null"
            ]
          },
          "taskId": {
            "type": [
              "string",
              "null"
            ]
          },
          "commentId": {
            "type": [
              "string",
              "null"
            ]
          },
          "emailId": {
            "type": [
              "string",
              "null"
            ]
          },
          "targetUrl": {
            "type": "string",
            "description": "Ruta relativa de la app web."
          },
          "actorId": {
            "type": [
              "string",
              "null"
            ]
          },
          "actor": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "_id": {
                "type": "string"
              },
              "firstName": {
                "type": "string"
              },
              "lastName": {
                "type": "string"
              }
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "readAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "null = sin leer."
          }
        }
      },
      "NotificationListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "unreadCount",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Notification"
            }
          },
          "unreadCount": {
            "type": "integer",
            "minimum": 0,
            "description": "No leídas del usuario (sin importar el filtro)."
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "NotificationUnreadCountResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "unread"
            ],
            "properties": {
              "unread": {
                "type": "integer",
                "minimum": 0
              }
            }
          }
        }
      },
      "NotificationMarkReadResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "id",
              "alreadyRead",
              "unread"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "alreadyRead": {
                "type": "boolean",
                "description": "true si ya estaba leída (la llamada es idempotente)."
              },
              "unread": {
                "type": "integer",
                "minimum": 0
              }
            }
          }
        }
      },
      "NotificationMarkAllReadResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "updated",
              "unread"
            ],
            "properties": {
              "updated": {
                "type": "integer",
                "minimum": 0
              },
              "unread": {
                "type": "integer",
                "const": 0
              }
            }
          }
        }
      },
      "NotificationError": {
        "type": "object",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          }
        }
      },
      "ExportRequestedResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "message"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "message": {
            "type": "string"
          },
          "data": {
            "type": "object",
            "required": [
              "status",
              "alreadyRunning",
              "notifyEmail"
            ],
            "properties": {
              "status": {
                "type": "string",
                "const": "queued"
              },
              "alreadyRunning": {
                "type": "boolean",
                "description": "true si ya había un export en curso para la organización (no se inició otro)."
              },
              "notifyEmail": {
                "type": "string",
                "format": "email",
                "description": "Correo del propietario al que llegará el enlace."
              }
            }
          }
        }
      },
      "ExportLatestResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "status",
              "downloadUrl",
              "expiresAt",
              "createdAt"
            ],
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "none",
                  "queued",
                  "ready"
                ],
                "description": "none = nunca se generó o el ZIP ya caducó (se guarda 90 días)."
              },
              "downloadUrl": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uri",
                "description": "URL firmada de S3, nueva en cada llamada; caduca en 24 h."
              },
              "expiresAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "createdAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "Cuándo se generó el ZIP."
              }
            }
          }
        }
      },
      "ExportError": {
        "type": "object",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "enum": [
              "OWNER_REQUIRED",
              "OWNER_EMAIL_MISSING"
            ]
          }
        }
      },
      "AutomationStep": {
        "type": "object",
        "description": "Paso del flujo. Acciones (send_whatsapp, send_whatsapp_template, send_email, create_task, add_tag, move_conversation_stage, reply_conversation, http_request, team_report) llevan config según su tipo; condition lleva config {match, rules} y las ramas yes/no; wait lleva config {mode, duration|until}. En las respuestas, los secretos de config (headers de autorización, apiKey, tokens) salen como ***REDACTED***.",
        "required": [
          "type"
        ],
        "properties": {
          "stepId": {
            "type": "string",
            "description": "Identificador único del paso; se autocompleta si falta."
          },
          "type": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "enabled": {
            "type": "boolean"
          },
          "onError": {
            "description": "stop | continue | retry, o política {policy, maxAttempts, ...}."
          },
          "config": {
            "type": "object",
            "additionalProperties": true
          },
          "yes": {
            "type": "array",
            "description": "Solo en condition. Pasos con esta misma forma (anidables hasta 5 niveles).",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "no": {
            "type": "array",
            "description": "Solo en condition. Pasos con esta misma forma.",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          }
        },
        "additionalProperties": true
      },
      "AutomationGraphInput": {
        "type": "object",
        "required": [
          "name",
          "triggers",
          "steps"
        ],
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 200
          },
          "triggers": {
            "type": "array",
            "maxItems": 20,
            "items": {
              "type": "object",
              "required": [
                "name",
                "type"
              ],
              "properties": {
                "triggerId": {
                  "type": "string",
                  "description": "Solo en PUT: id de un disparador del grafo actual para conservarlo. Un id ajeno se reemplaza por uno nuevo."
                },
                "name": {
                  "type": "string"
                },
                "type": {
                  "type": "string",
                  "enum": [
                    "inbound_message",
                    "contact_created",
                    "company_created",
                    "deal_status_changed",
                    "conversation_stage_changed",
                    "stale_conversation",
                    "scheduled",
                    "calendar_event_upcoming",
                    "quotation_created",
                    "quotation_viewed",
                    "quotation_status_changed",
                    "invoice_created",
                    "invoice_overdue",
                    "form_submitted",
                    "deal_stale",
                    "repurchase_due"
                  ]
                },
                "description": {
                  "type": "string"
                },
                "filters": {
                  "type": "object",
                  "additionalProperties": true,
                  "description": "Filtros del disparador. Uno que el disparador no guarda es error (se perdería al publicar)."
                },
                "schedule": {
                  "type": "object",
                  "description": "Solo para type scheduled.",
                  "properties": {
                    "cronExpression": {
                      "type": "string"
                    },
                    "timezone": {
                      "type": "string",
                      "default": "America/Bogota"
                    }
                  }
                }
              }
            }
          },
          "steps": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AutomationStep"
            }
          }
        }
      },
      "AutomationCreate": {
        "allOf": [
          {
            "$ref": "#/components/schemas/AutomationGraphInput"
          },
          {
            "type": "object",
            "properties": {
              "reEntry": {
                "$ref": "#/components/schemas/Automation/properties/reEntry"
              }
            }
          }
        ],
        "description": "Cualquier status, isActive o publish del body se IGNORA: la automatización nace en borrador y sus disparadores inactivos."
      },
      "AutomationDraftUpdate": {
        "type": "object",
        "description": "Parcial: lo que no se envía se toma del borrador actual o, si no hay, de lo publicado. triggers y steps reemplazan la lista completa. Envía al menos un campo.",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 200
          },
          "triggers": {
            "type": "array",
            "maxItems": 20,
            "items": {
              "$ref": "#/components/schemas/AutomationGraphInput/properties/triggers/items"
            }
          },
          "steps": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AutomationStep"
            }
          },
          "reEntry": {
            "$ref": "#/components/schemas/Automation/properties/reEntry"
          }
        }
      },
      "AutomationListItem": {
        "type": "object",
        "required": [
          "id",
          "name",
          "status",
          "publishedVersion",
          "hasUnpublishedChanges",
          "triggers",
          "draftTriggers",
          "lastRun",
          "runs"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "published",
              "paused"
            ],
            "description": "draft = nunca publicada (no dispara); published = activa; paused = pausada. Docs viejos sin estado = published."
          },
          "publishedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "publishedVersion": {
            "type": "integer"
          },
          "hasUnpublishedChanges": {
            "type": "boolean",
            "description": "Hay un borrador pendiente de publicar."
          },
          "reEntry": {
            "$ref": "#/components/schemas/Automation/properties/reEntry"
          },
          "triggers": {
            "type": "array",
            "description": "Disparadores reales (publicados).",
            "items": {
              "type": "object",
              "required": [
                "id",
                "name",
                "type",
                "isActive"
              ],
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "type": {
                  "type": "string"
                },
                "isActive": {
                  "type": "boolean"
                },
                "stats": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "properties": {
                    "totalRuns": {
                      "type": "integer"
                    },
                    "successCount": {
                      "type": "integer"
                    },
                    "failureCount": {
                      "type": "integer"
                    },
                    "lastRunAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "lastStatus": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "draftTriggers": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "triggerId": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "type": {
                  "type": "string"
                }
              }
            }
          },
          "lastRun": {
            "$ref": "#/components/schemas/Automation/properties/lastRun"
          },
          "runs": {
            "$ref": "#/components/schemas/Automation/properties/runs"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Automation": {
        "type": "object",
        "required": [
          "id",
          "name",
          "status",
          "publishedVersion",
          "hasUnpublishedChanges",
          "published",
          "draft",
          "lastRun",
          "runs"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "published",
              "paused"
            ]
          },
          "publishedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "publishedVersion": {
            "type": "integer"
          },
          "hasUnpublishedChanges": {
            "type": "boolean"
          },
          "reEntry": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "policy": {
                "type": "string",
                "enum": [
                  "one_active_per_contact",
                  "allow"
                ]
              },
              "cooldownMinutes": {
                "type": "integer",
                "minimum": 0
              }
            }
          },
          "published": {
            "type": [
              "object",
              "null"
            ],
            "description": "Lo que corre hoy. null si nunca se publicó.",
            "properties": {
              "name": {
                "type": "string"
              },
              "steps": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/AutomationStep"
                }
              },
              "triggers": {
                "type": "array",
                "items": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/AutomationListItem/properties/triggers/items"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "description": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "filters": {
                          "type": "object",
                          "additionalProperties": true
                        },
                        "schedule": {
                          "type": [
                            "object",
                            "null"
                          ],
                          "properties": {
                            "cronExpression": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "timezone": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "nextRunAt": {
                              "type": [
                                "string",
                                "null"
                              ],
                              "format": "date-time"
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "draft": {
            "type": [
              "object",
              "null"
            ],
            "description": "Borrador pendiente de publicar. null si no hay cambios sin publicar.",
            "properties": {
              "name": {
                "type": "string"
              },
              "steps": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/AutomationStep"
                }
              },
              "triggers": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "triggerId",
                    "name",
                    "type",
                    "isActive"
                  ],
                  "properties": {
                    "triggerId": {
                      "type": "string",
                      "description": "Será el id del disparador real al publicar."
                    },
                    "name": {
                      "type": "string"
                    },
                    "type": {
                      "type": "string"
                    },
                    "isActive": {
                      "type": "boolean",
                      "description": "Por API siempre false; se activa al publicar en la web."
                    },
                    "description": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "filters": {
                      "type": "object",
                      "additionalProperties": true
                    },
                    "schedule": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "properties": {
                        "cronExpression": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "timezone": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    }
                  }
                }
              },
              "updatedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          },
          "lastRun": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "status": {
                "type": "string"
              },
              "startedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "finishedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          },
          "runs": {
            "type": "object",
            "description": "Ejecuciones por estado en la retención del historial (~30 días).",
            "required": [
              "total",
              "success",
              "failed",
              "partial",
              "waiting",
              "running",
              "cancelled"
            ],
            "properties": {
              "total": {
                "type": "integer"
              },
              "success": {
                "type": "integer"
              },
              "failed": {
                "type": "integer"
              },
              "partial": {
                "type": "integer"
              },
              "waiting": {
                "type": "integer"
              },
              "running": {
                "type": "integer"
              },
              "cancelled": {
                "type": "integer"
              }
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AutomationValidation": {
        "type": "object",
        "required": [
          "valid",
          "errors",
          "warnings"
        ],
        "properties": {
          "valid": {
            "type": "boolean"
          },
          "errors": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AutomationResult/properties/warnings/items"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AutomationResult/properties/warnings/items"
            }
          }
        }
      },
      "AutomationListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AutomationListItem"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "AutomationResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/Automation"
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "stepId",
                "field",
                "message",
                "severity"
              ],
              "properties": {
                "stepId": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "null = automatización; trigger:<i> = disparador; si no, el paso."
                },
                "ruleId": {
                  "type": "string"
                },
                "field": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                },
                "severity": {
                  "type": "string",
                  "enum": [
                    "error",
                    "warning"
                  ]
                }
              }
            }
          }
        }
      },
      "AutomationRun": {
        "type": "object",
        "required": [
          "id",
          "automationId",
          "status",
          "steps"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "automationId": {
            "type": "string"
          },
          "triggerId": {
            "type": [
              "string",
              "null"
            ]
          },
          "conversationId": {
            "type": [
              "string",
              "null"
            ]
          },
          "contactId": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "running",
              "success",
              "partial",
              "failed",
              "waiting",
              "cancelled"
            ]
          },
          "startedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "finishedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "error": {
            "type": [
              "string",
              "null"
            ]
          },
          "replayOf": {
            "type": [
              "string",
              "null"
            ]
          },
          "replayCount": {
            "type": "integer"
          },
          "steps": {
            "type": "array",
            "description": "Sin input/output (datos del cliente y secretos ya interpolados).",
            "items": {
              "type": "object",
              "properties": {
                "stepId": {
                  "type": "string"
                },
                "type": {
                  "type": "string"
                },
                "name": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "success",
                    "failed",
                    "skipped"
                  ]
                },
                "ms": {
                  "type": [
                    "number",
                    "null"
                  ]
                },
                "attempts": {
                  "type": [
                    "integer",
                    "null"
                  ]
                },
                "path": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "error": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AutomationRunListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AutomationRun"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "AutomationValidationResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/AutomationValidation"
          }
        }
      },
      "AutomationVariablesResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "variables",
              "partial",
              "triggerTypes",
              "actionTypes"
            ],
            "properties": {
              "variables": {
                "type": "array",
                "description": "Variables garantizadas en al menos uno de los disparadores pedidos (más las globales).",
                "items": {
                  "type": "object",
                  "properties": {
                    "path": {
                      "type": "string",
                      "description": "Lo que va dentro de {{ }}."
                    },
                    "label": {
                      "type": "string"
                    },
                    "group": {
                      "type": "string"
                    },
                    "type": {
                      "type": "string"
                    },
                    "example": {
                      "type": "string"
                    },
                    "triggers": {
                      "description": "\"*\" o lista de tipos de disparador."
                    },
                    "options": {
                      "type": "array",
                      "description": "Solo en variables de valores cerrados (p. ej. trigger.quotation_status). Las condiciones y filtros comparan contra `value` (la clave); `label` es para mostrar.",
                      "items": {
                        "type": "object",
                        "required": [
                          "value",
                          "label"
                        ],
                        "properties": {
                          "value": {
                            "type": "string",
                            "examples": [
                              "invoiced"
                            ]
                          },
                          "label": {
                            "type": "string",
                            "examples": [
                              "Facturada"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              },
              "partial": {
                "type": "array",
                "description": "Con varios disparadores, las que solo existen en algunos.",
                "items": {
                  "type": "object",
                  "additionalProperties": true
                }
              },
              "triggerTypes": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "actionTypes": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "type": {
                      "type": "string"
                    },
                    "description": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "AutomationError": {
        "type": "object",
        "description": "Contrato de error de /automations (success:false + error).",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          },
          "details": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "AutomationValidationError": {
        "type": "object",
        "required": [
          "success",
          "error",
          "errors",
          "warnings"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          },
          "errors": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AutomationResult/properties/warnings/items"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AutomationResult/properties/warnings/items"
            }
          }
        }
      },
      "AutomationTrigger": {
        "type": "object",
        "description": "Disparador REAL (publicado) de una automatización.",
        "required": [
          "id",
          "automationId",
          "automation",
          "name",
          "type",
          "isActive",
          "filters",
          "stats"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "automationId": {
            "type": [
              "string",
              "null"
            ]
          },
          "automation": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "enum": [
                  "draft",
                  "published",
                  "paused"
                ]
              }
            }
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "type": {
            "type": "string"
          },
          "isActive": {
            "type": "boolean"
          },
          "filters": {
            "type": "object",
            "additionalProperties": true
          },
          "schedule": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "cronExpression": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "timezone": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "nextRunAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "lastRunAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          },
          "stats": {
            "type": "object",
            "properties": {
              "totalRuns": {
                "type": "integer"
              },
              "successCount": {
                "type": "integer"
              },
              "failureCount": {
                "type": "integer"
              },
              "lastRunAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "lastStatus": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "lastError": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AutomationTriggerListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AutomationTrigger"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "AutomationTriggerResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/AutomationTrigger"
          }
        }
      },
      "AutomationTriggerError": {
        "type": "object",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          }
        }
      },
      "ImportContactsRequest": {
        "type": "object",
        "required": [
          "rows"
        ],
        "properties": {
          "rows": {
            "type": "array",
            "minItems": 1,
            "maxItems": 500,
            "items": {
              "type": "object",
              "description": "Una fila = un contacto. Claves = las `key` de las propiedades de contacto de la organización (sin distinguir mayúsculas; ver GET /api/contact-properties) más `owner` (correo del usuario dueño). Una clave desconocida rechaza la fila.",
              "additionalProperties": {
                "type": [
                  "string",
                  "number",
                  "boolean",
                  "null"
                ],
                "description": "Valor de la celda. Números y booleanos se guardan como texto; null o \"\" = vacío (nunca borra un dato existente)."
              },
              "example": {
                "firstName": "Ana",
                "email": "ana@acme.co",
                "mobile": "3001112233",
                "company": "Acme SAS",
                "owner": "asesor@miempresa.co"
              }
            }
          },
          "createMissingCompanies": {
            "type": "boolean",
            "default": false,
            "description": "Crea las empresas de la columna `company` que no existan (respeta el límite de empresas del plan)."
          },
          "dryRun": {
            "type": "boolean",
            "default": false,
            "description": "Solo simula y devuelve el resumen; no escribe nada ni deja historial."
          },
          "locationMode": {
            "type": "string",
            "enum": [
              "lenient",
              "strict"
            ],
            "default": "lenient",
            "description": "Ciudades (Épica Y). lenient (default, como el Excel de la app): city que no resuelva contra el catálogo DANE queda como texto con cityCode vacío (\"sin confirmar\") y sale en warnings. strict (lo usa el tool MCP import_contacts): la fila cuya city sea ambigua, mal escrita o desconocida se RECHAZA (rejectedRows, con los candidatos en reason) y nunca se asigna un \"mejor candidato\"; solo se evalúan filas que traen city/cityCode con valor. La clave cityCode (5 dígitos) es la fuente de verdad y deriva city/state/stateCode."
          }
        }
      },
      "ImportContactsDryRunResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "dryRun",
              "totalRows",
              "totalRowsWithData",
              "wouldCreate",
              "wouldUpdate",
              "wouldSkipByPlan",
              "skippedEmptyRows",
              "mergedDuplicateRows",
              "rejectedCount",
              "rejectedRows",
              "preview",
              "companies",
              "planInfo",
              "warnings"
            ],
            "properties": {
              "dryRun": {
                "type": "boolean",
                "const": true
              },
              "totalRows": {
                "type": "integer",
                "description": "Filas recibidas."
              },
              "totalRowsWithData": {
                "type": "integer",
                "description": "Filas con identidad más las rechazadas."
              },
              "wouldCreate": {
                "type": "integer",
                "description": "Contactos nuevos que se crearían (ya descontado el límite del plan)."
              },
              "wouldUpdate": {
                "type": "integer",
                "description": "Contactos existentes que se actualizarían."
              },
              "wouldSkipByPlan": {
                "type": "integer",
                "description": "Contactos nuevos que no caben en el plan."
              },
              "skippedEmptyRows": {
                "type": "integer",
                "description": "Filas sin firstName",
                "lastName": null,
                "email": null,
                "mobile ni phone.": null
              },
              "mergedDuplicateRows": {
                "type": "integer",
                "description": "Filas fusionadas con otra del mismo lote."
              },
              "rejectedCount": {
                "type": "integer"
              },
              "rejectedRows": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ImportContactsResult/properties/data/properties/rejectedRows/items"
                }
              },
              "preview": {
                "type": "array",
                "maxItems": 3,
                "items": {
                  "type": "object",
                  "required": [
                    "rowNumber",
                    "properties",
                    "updatesExisting"
                  ],
                  "properties": {
                    "rowNumber": {
                      "type": "integer"
                    },
                    "properties": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      }
                    },
                    "updatesExisting": {
                      "type": "boolean"
                    },
                    "existingContactId": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "companyLinked": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              },
              "companies": {
                "type": "object",
                "required": [
                  "wouldLink",
                  "wouldCreate",
                  "skippedByPlan",
                  "missing"
                ],
                "properties": {
                  "wouldLink": {
                    "type": "integer"
                  },
                  "wouldCreate": {
                    "type": "integer"
                  },
                  "skippedByPlan": {
                    "type": "integer"
                  },
                  "missing": {
                    "type": "integer",
                    "description": "Empresas que no existen y no se crearían (createMissingCompanies apagado)."
                  }
                }
              },
              "planInfo": {
                "type": "object",
                "required": [
                  "limit",
                  "used",
                  "remaining",
                  "planName"
                ],
                "properties": {
                  "limit": {
                    "type": "integer",
                    "description": "-1 = sin límite."
                  },
                  "used": {
                    "type": "integer"
                  },
                  "remaining": {
                    "type": "integer",
                    "description": "-1 = sin límite."
                  },
                  "planName": {
                    "type": "string"
                  }
                }
              },
              "warnings": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "unconfirmedCities": {
                "type": "integer",
                "description": "Filas cuya ciudad quedó \"sin confirmar\" (modo lenient: cityCode vacío con city escrito; en la app caen en la lista dinámica \"Ciudades sin confirmar\"). El detalle legible va en warnings. En strict siempre 0 (esas filas salen en rejectedRows)."
              }
            }
          }
        }
      },
      "ImportContactsResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "dryRun",
              "importId",
              "totalRows",
              "totalRowsWithData",
              "importedCount",
              "updatedCount",
              "skippedByPlan",
              "skippedEmptyRows",
              "mergedDuplicateRows",
              "rejectedCount",
              "rejectedRows",
              "companiesLinked",
              "companiesCreated",
              "companiesSkippedByPlan",
              "warnings"
            ],
            "properties": {
              "dryRun": {
                "type": "boolean",
                "const": false
              },
              "importId": {
                "type": "string",
                "description": "Id del registro de historial (para GET /history/{id} y el rollback)."
              },
              "totalRows": {
                "type": "integer"
              },
              "totalRowsWithData": {
                "type": "integer"
              },
              "importedCount": {
                "type": "integer"
              },
              "updatedCount": {
                "type": "integer"
              },
              "skippedByPlan": {
                "type": "integer"
              },
              "skippedEmptyRows": {
                "type": "integer"
              },
              "mergedDuplicateRows": {
                "type": "integer"
              },
              "rejectedCount": {
                "type": "integer"
              },
              "rejectedRows": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "rowNumber",
                    "reason",
                    "values"
                  ],
                  "properties": {
                    "rowNumber": {
                      "type": "integer",
                      "minimum": 1,
                      "description": "Posición 1-based en rows[] (en importaciones de Excel, la fila de la hoja)."
                    },
                    "reason": {
                      "type": "string"
                    },
                    "values": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "companiesLinked": {
                "type": "integer"
              },
              "companiesCreated": {
                "type": "integer"
              },
              "companiesSkippedByPlan": {
                "type": "integer"
              },
              "warnings": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "unconfirmedCities": {
                "type": "integer",
                "description": "Filas cuya ciudad quedó \"sin confirmar\" (modo lenient: cityCode vacío con city escrito; en la app caen en la lista dinámica \"Ciudades sin confirmar\"). El detalle legible va en warnings. En strict siempre 0 (esas filas salen en rejectedRows)."
              }
            }
          }
        }
      },
      "ImportSummary": {
        "type": "object",
        "required": [
          "id",
          "type",
          "source",
          "fileName",
          "status",
          "userId",
          "totalRows",
          "importedCount",
          "updatedCount",
          "skippedByPlan",
          "skippedEmptyRows",
          "rejectedCount",
          "companiesLinked",
          "companiesCreated",
          "warnings",
          "errorMessage",
          "rolledBackAt",
          "rolledBackBy",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "contacts",
              "companies",
              "deals",
              "purchases"
            ]
          },
          "source": {
            "type": "string",
            "enum": [
              "web",
              "api"
            ],
            "description": "web = Excel/CSV de la app; api = filas JSON por la API o el MCP."
          },
          "fileName": {
            "type": "string",
            "description": "Nombre del archivo; en las de API, api:<nombre del token>."
          },
          "status": {
            "type": "string",
            "enum": [
              "completed",
              "failed",
              "rolled_back"
            ]
          },
          "userId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Autor de la importación."
          },
          "totalRows": {
            "type": "integer"
          },
          "importedCount": {
            "type": "integer"
          },
          "updatedCount": {
            "type": "integer"
          },
          "skippedByPlan": {
            "type": "integer"
          },
          "skippedEmptyRows": {
            "type": "integer"
          },
          "rejectedCount": {
            "type": "integer"
          },
          "companiesLinked": {
            "type": "integer"
          },
          "companiesCreated": {
            "type": "integer"
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "errorMessage": {
            "type": [
              "string",
              "null"
            ]
          },
          "rolledBackAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "rolledBackBy": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ImportDetailResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ImportSummary"
              },
              {
                "type": "object",
                "required": [
                  "rejectedRows"
                ],
                "properties": {
                  "rejectedRows": {
                    "type": "array",
                    "maxItems": 1000,
                    "items": {
                      "$ref": "#/components/schemas/ImportContactsResult/properties/data/properties/rejectedRows/items"
                    }
                  }
                }
              }
            ]
          }
        }
      },
      "ImportListResult": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ImportSummary"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "ImportRollbackResult": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "id",
              "deletedCount",
              "restoredCount",
              "deletedCompanies"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "deletedCount": {
                "type": "integer",
                "description": "Registros creados por la importación que se borraron."
              },
              "restoredCount": {
                "type": "integer",
                "description": "Registros actualizados que volvieron a su estado anterior."
              },
              "deletedCompanies": {
                "type": "integer",
                "description": "Empresas creadas por createMissingCompanies que se borraron."
              }
            }
          }
        }
      },
      "ImportError": {
        "type": "object",
        "description": "Contrato de error de /import (success:false + error).",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "maxRows": {
            "type": "integer"
          }
        }
      }
    },
    "examples": {
      "ConversationStageEntered": {
        "summary": "Conversación entra a \"Oportunidad\"",
        "value": {
          "event": "conversation.stage.entered",
          "organizationId": "507f1f77bcf86cd799439001",
          "conversationId": "507f1f77bcf86cd799439051",
          "conversationTitle": "Ana Ejemplo",
          "contact": {
            "name": "Ana",
            "lastName": "Ejemplo",
            "mobile": "573000000000",
            "email": "ana@example.com",
            "contactId": "507f1f77bcf86cd799439011"
          },
          "assignedTo": "507f1f77bcf86cd799439101",
          "priority": "medium",
          "tags": [
            "manillas",
            "mayorista"
          ],
          "source": "whatsapp",
          "fromStage": {
            "_id": "507f1f77bcf86cd799439061",
            "name": "Lead",
            "order": 0
          },
          "toStage": {
            "_id": "507f1f77bcf86cd799439062",
            "name": "Oportunidad",
            "order": 2
          },
          "pipeline": {
            "_id": "507f1f77bcf86cd799439060",
            "name": "Pipeline de Conversaciones"
          },
          "triggeredBy": "user",
          "timestamp": "2026-09-05T15:04:05.000Z"
        }
      },
      "DealStatusChanged": {
        "summary": "Negocio pasa a \"Negociación\"",
        "value": {
          "event": "deal_status_changed",
          "trigger": {
            "organizationId": "507f1f77bcf86cd799439001",
            "contactId": "507f1f77bcf86cd799439011",
            "eventId": "deal:507f1f77bcf86cd799439021:507f1f77bcf86cd799439032",
            "deal": {
              "dealId": "507f1f77bcf86cd799439021",
              "fromStatusId": "507f1f77bcf86cd799439031",
              "toStatusId": "507f1f77bcf86cd799439032",
              "pipelineId": "507f1f77bcf86cd799439030"
            }
          }
        }
      },
      "QuotationCreated": {
        "summary": "Cotización creada con empresa",
        "value": {
          "event": "quotation_created",
          "trigger": {
            "organizationId": "507f1f77bcf86cd799439001",
            "contactId": "507f1f77bcf86cd799439011",
            "companyId": "507f1f77bcf86cd799439013",
            "quotationId": "507f1f77bcf86cd799439041",
            "eventId": "quotation:507f1f77bcf86cd799439041",
            "quotation_number": 1042,
            "quotation_name": "Manillas evento octubre",
            "quotation_total": "$ 1.250.000",
            "quotation_total_raw": 1250000,
            "quotation_status": "draft",
            "quotation_expiration": "18 sept 2026",
            "quotation_items_summary": "2× Manilla tyvek, 1× Manilla vinilo",
            "quotation_items_count": 2,
            "quotation_owner_id": "507f1f77bcf86cd799439101",
            "contact_name": "Ana Ejemplo",
            "contact_email": "ana@example.com",
            "contact_mobile": "573000000000",
            "company_name": "Eventos Ejemplo SAS"
          }
        }
      },
      "InvoiceCreated": {
        "summary": "Factura de venta aceptada por la DIAN",
        "value": {
          "event": "invoice_created",
          "trigger": {
            "organizationId": "507f1f77bcf86cd799439001",
            "contactId": "507f1f77bcf86cd799439011",
            "invoiceId": "507f1f77bcf86cd799439071",
            "eventId": "invoice:507f1f77bcf86cd799439071",
            "invoice_number": 623,
            "invoice_reference": "FS-623",
            "invoice_prefix": "FS-",
            "invoice_total": "$ 1.250.000",
            "invoice_total_raw": 1250000,
            "invoice_date": "2026-09-05",
            "invoice_due_date": "2026-10-05",
            "invoice_payment_form": "credito",
            "invoice_status": "valido",
            "invoice_cufe": "8a1c0e6f5d3b2a4c9e7f1b3d5a7c9e1f3b5d7a9c1e3f5b7d9a1c3e5f7b9d1a3c5e7f",
            "invoice_items_count": 3,
            "contact_name": "Ana Ejemplo",
            "contact_email": "ana@example.com",
            "contact_mobile": "573000000000"
          }
        }
      },
      "FormSubmitted": {
        "summary": "Formulario de contacto enviado por un lead nuevo",
        "value": {
          "event": "form_submitted",
          "trigger": {
            "organizationId": "507f1f77bcf86cd799439001",
            "contactId": "507f1f77bcf86cd799439011",
            "formId": "66f1000000000000000000c2",
            "responseId": "66f1000000000000000000d5",
            "eventId": "form-response:66f1000000000000000000d5",
            "form_name": "Contacto web",
            "form_slug": "contacto-web",
            "form_outcome": "created",
            "contact_name": "Ana Ejemplo",
            "contact_email": "ana@example.com",
            "contact_mobile": "573000000000",
            "utm_source": "google",
            "utm_medium": "cpc",
            "utm_campaign": "manillas-septiembre",
            "page_url": "https://manillasdecontrol.com/contacto",
            "responses": {
              "nombre": "Ana Ejemplo",
              "email": "ana@example.com",
              "celular": "573000000000",
              "mensaje": "Necesito 500 manillas para el 20 de octubre"
            }
          }
        }
      }
    }
  },
  "x-mcp": {
    "endpoint": "https://api.fusioncol.com/api/mcp",
    "transport": "Streamable HTTP",
    "catalogUrl": "https://api.fusioncol.com/api/mcp-tools.json",
    "description": "Catálogo generado durante el build desde las descripciones y schemas Zod de\ncreateFusionMcpServer.ts y src/mcp/tools/*.ts. Incluye todos los tools, no solo contactos.\nMCP usa JSON-RPC: los tools no son endpoints REST individuales.\n",
    "source": "src/mcp/createFusionMcpServer.ts"
  }
}
