{
  "info": {
    "name": "KleeMesa - API Agente Autónomo",
    "description": "Colección para probar la API del agente autónomo de la mesa de ayuda.\n\n**Antes de empezar:**\n1. Cree una credencial: `php bin/agent-credential create --name=\"Pruebas Postman\"`\n2. Pegue el token en la variable de colección `token`.\n3. Ajuste `base_url` a su instalación.\n4. Ejecute `Health` para comprobar la integración.\n\n**Notas de uso:**\n- Las peticiones de escritura generan `request_id` automáticamente en un script de pre-request. Para probar la **idempotencia**, fije `request_id` a un valor constante y repita la petición: recibirá el mismo resultado con `replayed: true`.\n- El flujo de la carpeta `3. Ciclo completo` está en orden: ejecútelo de arriba abajo con el Runner.\n- Las operaciones de alto riesgo (comentario público, descarga de adjuntos, cierre) responden `403 APPROVAL_REQUIRED` mientras `AGENT_API_HIGH_RISK_MODE=deny`.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "variable": [
    {
      "key": "base_url",
      "value": "http://127.0.0.1:8000",
      "description": "URL del directorio público de la instalación."
    },
    {
      "key": "token",
      "value": "",
      "description": "Token kma_<prefijo>_<secreto>. Se muestra una sola vez al crear la credencial."
    },
    {
      "key": "case_id",
      "value": "1",
      "description": "Id numérico o NumeroCaso del caso a trabajar."
    },
    {
      "key": "agent_id",
      "value": "postman-agent-01",
      "description": "Identifica al proceso. Debe ser el mismo durante toda la vida de un caso."
    },
    { "key": "request_id", "value": "" },
    { "key": "case_version", "value": "" },
    { "key": "attachment_id", "value": "1" },
    { "key": "group_id", "value": "1" }
  ],
  "auth": {
    "type": "bearer",
    "bearer": [{ "key": "token", "value": "{{token}}", "type": "string" }]
  },
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Genera un request_id nuevo en cada envío, salvo que la variable de",
          "// colección ya tenga uno fijado a mano (útil para probar idempotencia).",
          "if (!pm.collectionVariables.get('request_id')) {",
          "    pm.collectionVariables.set('request_id', 'pm-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8));",
          "}"
        ]
      }
    },
    {
      "listen": "test",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Comprobaciones comunes a toda la colección.",
          "pm.test('La respuesta usa el envelope estándar', function () {",
          "    const d = pm.response.json();",
          "    pm.expect(d).to.have.property('success');",
          "    pm.expect(d).to.have.property('request_id');",
          "    pm.expect(d).to.have.property('data');",
          "    pm.expect(d).to.have.property('error');",
          "});",
          "",
          "pm.test('No se filtran secretos ni trazas', function () {",
          "    const raw = pm.response.text();",
          "    pm.expect(raw).to.not.include('TokenHash');",
          "    pm.expect(raw).to.not.include('Stack trace');",
          "    pm.expect(raw).to.not.include('DB_PASS');",
          "});"
        ]
      }
    }
  ],
  "item": [
    {
      "name": "1. Estado y capacidades",
      "item": [
        {
          "name": "Health",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/?c=agentapi&a=health",
              "host": ["{{base_url}}"],
              "path": [""],
              "query": [
                { "key": "c", "value": "agentapi" },
                { "key": "a", "value": "health" }
              ]
            },
            "description": "Comprueba conectividad, credencial y alineación de los estados configurados. Si `configured_states_missing` no está vacío, alguien renombró un estado en la mesa y hay que ajustar el .env."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Responde 200', () => pm.response.to.have.status(200));",
                  "const d = pm.response.json();",
                  "pm.test('La API está habilitada', () => pm.expect(d.data.api_enabled).to.be.true);",
                  "pm.test('Los estados configurados existen en la mesa', function () {",
                  "    pm.expect(d.data.configured_states_missing).to.eql([]);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Capabilities",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/?c=agentapi&a=capabilities",
              "host": ["{{base_url}}"],
              "path": [""],
              "query": [
                { "key": "c", "value": "agentapi" },
                { "key": "a", "value": "capabilities" }
              ]
            },
            "description": "Scopes concedidos, Ids reales de estado, matriz de transiciones, catálogos y política. El agente debe consultarla al arrancar y no adivinar identificadores."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Responde 200', () => pm.response.to.have.status(200));",
                  "const d = pm.response.json();",
                  "pm.test('Publica los estados y sus Ids', () => pm.expect(d.data.states.working.id).to.be.above(0));",
                  "pm.test('Cerrado figura como prohibido para el agente', function () {",
                  "    pm.expect(JSON.stringify(d.data.transitions.forbidden)).to.include('Cerrado');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Sin token (espera 401)",
          "request": {
            "auth": { "type": "noauth" },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/?c=agentapi&a=health",
              "host": ["{{base_url}}"],
              "path": [""],
              "query": [
                { "key": "c", "value": "agentapi" },
                { "key": "a", "value": "health" }
              ]
            },
            "description": "Debe responder 401 en JSON, nunca un redirect al login."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Responde 401', () => pm.response.to.have.status(401));",
                  "pm.test('Es JSON y no un redirect', function () {",
                  "    pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json');",
                  "    pm.expect(pm.response.json().error.code).to.eql('UNAUTHORIZED');",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "2. Lecturas",
      "item": [
        {
          "name": "Listar casos pendientes",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/?c=agentapi&a=cases&status=pending&sort=priority&order=DESC&limit=20",
              "host": ["{{base_url}}"],
              "path": [""],
              "query": [
                { "key": "c", "value": "agentapi" },
                { "key": "a", "value": "cases" },
                { "key": "status", "value": "pending" },
                { "key": "sort", "value": "priority" },
                { "key": "order", "value": "DESC" },
                { "key": "limit", "value": "20" },
                { "key": "priority", "value": "Alta", "disabled": true },
                { "key": "unassigned", "value": "1", "disabled": true },
                { "key": "client", "value": "usuario@ejemplo.local", "disabled": true },
                { "key": "date_from", "value": "2026-08-01", "disabled": true }
              ]
            },
            "description": "`status=pending` excluye los estados terminales. Active los parámetros deshabilitados para probar más filtros."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Responde 200', () => pm.response.to.have.status(200));",
                  "const d = pm.response.json();",
                  "pm.test('Trae paginación', () => pm.expect(d.data.pagination).to.have.property('total'));",
                  "pm.test('El asunto viene marcado como no confiable', function () {",
                  "    if (d.data.cases.length) {",
                  "        pm.expect(d.data.cases[0].user_content.untrusted).to.be.true;",
                  "    }",
                  "});",
                  "// Encadena el primer caso para las peticiones siguientes.",
                  "if (d.data.cases.length) {",
                  "    pm.collectionVariables.set('case_id', String(d.data.cases[0].id));",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "Filtro inválido (espera 422)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/?c=agentapi&a=cases&status=NoExiste&date_from=25-08-2026",
              "host": ["{{base_url}}"],
              "path": [""],
              "query": [
                { "key": "c", "value": "agentapi" },
                { "key": "a", "value": "cases" },
                { "key": "status", "value": "NoExiste" },
                { "key": "date_from", "value": "25-08-2026" }
              ]
            },
            "description": "La validación es estricta y devuelve el detalle campo a campo."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Responde 422', () => pm.response.to.have.status(422));",
                  "pm.test('Indica qué campo falló', function () {",
                  "    pm.expect(pm.response.json().error.details).to.have.property('status');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Detalle del caso",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/?c=agentapi&a=case&Id={{case_id}}",
              "host": ["{{base_url}}"],
              "path": [""],
              "query": [
                { "key": "c", "value": "agentapi" },
                { "key": "a", "value": "case" },
                { "key": "Id", "value": "{{case_id}}" }
              ]
            },
            "description": "Devuelve las zonas separadas por confianza. Guarda `case_version` para los cambios de estado."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Responde 200', () => pm.response.to.have.status(200));",
                  "const d = pm.response.json().data;",
                  "pm.test('Separa metadatos, contenido de usuario y política', function () {",
                  "    pm.expect(d).to.have.property('system_metadata');",
                  "    pm.expect(d).to.have.property('user_content');",
                  "    pm.expect(d).to.have.property('policy');",
                  "});",
                  "pm.test('El contenido de usuario está marcado', () => pm.expect(d.user_content.untrusted).to.be.true);",
                  "if (d.system_metadata.concurrency.version !== null) {",
                  "    pm.collectionVariables.set('case_version', String(d.system_metadata.concurrency.version));",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "Historial",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/?c=agentapi&a=history&Id={{case_id}}&limit=50&offset=0&order=desc",
              "host": ["{{base_url}}"],
              "path": [""],
              "query": [
                { "key": "c", "value": "agentapi" },
                { "key": "a", "value": "history" },
                { "key": "Id", "value": "{{case_id}}" },
                { "key": "limit", "value": "50" },
                { "key": "offset", "value": "0" },
                { "key": "order", "value": "desc" }
              ]
            }
          }
        },
        {
          "name": "Comentarios",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/?c=agentapi&a=comments&Id={{case_id}}",
              "host": ["{{base_url}}"],
              "path": [""],
              "query": [
                { "key": "c", "value": "agentapi" },
                { "key": "a", "value": "comments" },
                { "key": "Id", "value": "{{case_id}}" }
              ]
            },
            "description": "Las notas internas solo se entregan si la credencial tiene `comments:write`; el campo `incluye_internos` lo indica."
          }
        },
        {
          "name": "Adjuntos (metadatos)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/?c=agentapi&a=attachments&Id={{case_id}}",
              "host": ["{{base_url}}"],
              "path": [""],
              "query": [
                { "key": "c", "value": "agentapi" },
                { "key": "a", "value": "attachments" },
                { "key": "Id", "value": "{{case_id}}" }
              ]
            },
            "description": "Nunca expone rutas en disco. La descarga se hace por Id del adjunto."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('No expone rutas del servidor', function () {",
                  "    pm.expect(pm.response.text()).to.not.include('storage/helpdesk');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Descargar adjunto (alto riesgo)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/?c=agentapi&a=attachmentDownload&Id={{attachment_id}}",
              "host": ["{{base_url}}"],
              "path": [""],
              "query": [
                { "key": "c", "value": "agentapi" },
                { "key": "a", "value": "attachmentDownload" },
                { "key": "Id", "value": "{{attachment_id}}" }
              ]
            },
            "description": "Con `AGENT_API_HIGH_RISK_MODE=deny` responde 403 APPROVAL_REQUIRED. El `Id` es el del adjunto, no el del caso."
          }
        },
        {
          "name": "Resultado de una operación",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/?c=agentapi&a=operation&RequestId={{request_id}}",
              "host": ["{{base_url}}"],
              "path": [""],
              "query": [
                { "key": "c", "value": "agentapi" },
                { "key": "a", "value": "operation" },
                { "key": "RequestId", "value": "{{request_id}}" }
              ]
            },
            "description": "Sirve para averiguar qué pasó con una escritura cuya respuesta no llegó a recibirse."
          }
        }
      ]
    },
    {
      "name": "3. Ciclo completo (ejecutar en orden)",
      "item": [
        {
          "name": "1) Escribir sin reclamar (espera 412)",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{agent_id}}\",\n  \"request_id\": \"{{request_id}}\",\n  \"type\": \"internal\",\n  \"message\": \"Intento de escritura sin bloqueo.\"\n}"
            },
            "url": {
              "raw": "{{base_url}}/?c=agentapi&a=comment&Id={{case_id}}",
              "host": ["{{base_url}}"],
              "path": [""],
              "query": [
                { "key": "c", "value": "agentapi" },
                { "key": "a", "value": "comment" },
                { "key": "Id", "value": "{{case_id}}" }
              ]
            },
            "description": "Toda escritura exige bloqueo propio vigente."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Responde 412 CLAIM_REQUIRED', function () {",
                  "    pm.response.to.have.status(412);",
                  "    pm.expect(pm.response.json().error.code).to.eql('CLAIM_REQUIRED');",
                  "});",
                  "pm.collectionVariables.set('request_id', '');"
                ]
              }
            }
          ]
        },
        {
          "name": "2) Claim",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{agent_id}}\",\n  \"request_id\": \"{{request_id}}\",\n  \"lease_minutes\": 15\n}"
            },
            "url": {
              "raw": "{{base_url}}/?c=agentapi&a=claim&Id={{case_id}}",
              "host": ["{{base_url}}"],
              "path": [""],
              "query": [
                { "key": "c", "value": "agentapi" },
                { "key": "a", "value": "claim" },
                { "key": "Id", "value": "{{case_id}}" }
              ]
            },
            "description": "Reclamo atómico. Si otro agente lo tiene, responde 409 y hay que pasar al siguiente caso."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 409) {",
                  "    pm.test('El caso ya está reclamado por otro agente', function () {",
                  "        pm.expect(pm.response.json().error.code).to.be.oneOf(['CASE_ALREADY_CLAIMED', 'CASE_ASSIGNED_TO_HUMAN']);",
                  "    });",
                  "} else {",
                  "    pm.test('Responde 200 y el bloqueo es propio', function () {",
                  "        pm.response.to.have.status(200);",
                  "        const d = pm.response.json().data;",
                  "        pm.expect(d.claim.activo).to.be.true;",
                  "        pm.expect(d.claim.propio).to.be.true;",
                  "    });",
                  "}",
                  "pm.collectionVariables.set('request_id', '');"
                ]
              }
            }
          ]
        },
        {
          "name": "3) Heartbeat",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{agent_id}}\",\n  \"lease_minutes\": 15\n}"
            },
            "url": {
              "raw": "{{base_url}}/?c=agentapi&a=heartbeat&Id={{case_id}}",
              "host": ["{{base_url}}"],
              "path": [""],
              "query": [
                { "key": "c", "value": "agentapi" },
                { "key": "a", "value": "heartbeat" },
                { "key": "Id", "value": "{{case_id}}" }
              ]
            },
            "description": "Renueva el lease. No lleva request_id. Un lease vencido responde 410 y hay que volver a reclamar."
          }
        },
        {
          "name": "4) Comentario interno",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{agent_id}}\",\n  \"request_id\": \"{{request_id}}\",\n  \"type\": \"internal\",\n  \"message\": \"Inicio de análisis automático del caso.\"\n}"
            },
            "url": {
              "raw": "{{base_url}}/?c=agentapi&a=comment&Id={{case_id}}",
              "host": ["{{base_url}}"],
              "path": [""],
              "query": [
                { "key": "c", "value": "agentapi" },
                { "key": "a", "value": "comment" },
                { "key": "Id", "value": "{{case_id}}" }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Responde 201', () => pm.response.to.have.status(201));",
                  "pm.collectionVariables.set('request_id', '');"
                ]
              }
            }
          ]
        },
        {
          "name": "5) Comentario público (alto riesgo)",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{agent_id}}\",\n  \"request_id\": \"{{request_id}}\",\n  \"type\": \"public\",\n  \"message\": \"Estamos revisando su caso.\"\n}"
            },
            "url": {
              "raw": "{{base_url}}/?c=agentapi&a=comment&Id={{case_id}}",
              "host": ["{{base_url}}"],
              "path": [""],
              "query": [
                { "key": "c", "value": "agentapi" },
                { "key": "a", "value": "comment" },
                { "key": "Id", "value": "{{case_id}}" }
              ]
            },
            "description": "Con la política en `deny` responde 403 APPROVAL_REQUIRED: un texto enviado al cliente no se puede retirar."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Bloqueado por política de alto riesgo', function () {",
                  "    pm.response.to.have.status(403);",
                  "    pm.expect(pm.response.json().error.code).to.eql('APPROVAL_REQUIRED');",
                  "});",
                  "pm.collectionVariables.set('request_id', '');"
                ]
              }
            }
          ]
        },
        {
          "name": "6) Resolver sin diagnóstico (espera 422)",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{agent_id}}\",\n  \"request_id\": \"{{request_id}}\",\n  \"to_status\": \"Resuelto\",\n  \"reason\": \"Intento prematuro\"\n}"
            },
            "url": {
              "raw": "{{base_url}}/?c=agentapi&a=changeStatus&Id={{case_id}}",
              "host": ["{{base_url}}"],
              "path": [""],
              "query": [
                { "key": "c", "value": "agentapi" },
                { "key": "a", "value": "changeStatus" },
                { "key": "Id", "value": "{{case_id}}" }
              ]
            },
            "description": "La puerta de cierre devuelve la lista exacta de lo que falta."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Responde 422 SOLUTION_REQUIRED', function () {",
                  "    pm.response.to.have.status(422);",
                  "    const e = pm.response.json().error;",
                  "    pm.expect(e.code).to.eql('SOLUTION_REQUIRED');",
                  "    pm.expect(e.details.faltantes).to.be.an('array');",
                  "});",
                  "pm.collectionVariables.set('request_id', '');"
                ]
              }
            }
          ]
        },
        {
          "name": "7) Diagnóstico",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{agent_id}}\",\n  \"request_id\": \"{{request_id}}\",\n  \"summary\": \"El servicio de correo rechazaba las credenciales\",\n  \"diagnosis\": \"El token OAuth del buzón caducó; los reintentos fallaron con 401.\",\n  \"root_cause\": \"Token OAuth expirado sin renovación automática\",\n  \"actions_performed\": [\n    \"Revisé los logs del servicio\",\n    \"Verifiqué la vigencia del token\"\n  ],\n  \"evidence\": \"log: auth failed 401 invalid_grant\",\n  \"confidence\": 0.88\n}"
            },
            "url": {
              "raw": "{{base_url}}/?c=agentapi&a=diagnosis&Id={{case_id}}",
              "host": ["{{base_url}}"],
              "path": [""],
              "query": [
                { "key": "c", "value": "agentapi" },
                { "key": "a", "value": "diagnosis" },
                { "key": "Id", "value": "{{case_id}}" }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Responde 201', () => pm.response.to.have.status(201));",
                  "pm.collectionVariables.set('request_id', '');"
                ]
              }
            }
          ]
        },
        {
          "name": "8) Solución aplicada",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{agent_id}}\",\n  \"request_id\": \"{{request_id}}\",\n  \"summary\": \"Se renovó el token OAuth y se restableció la sincronización\",\n  \"applied\": true,\n  \"actions_performed\": [\n    \"Generé un token nuevo\",\n    \"Actualicé la configuración\",\n    \"Reinicié el sincronizador\"\n  ],\n  \"verification\": \"Se enviaron 3 correos de prueba y todos llegaron. Sin errores 401 en 20 minutos.\",\n  \"evidence\": \"log: sync ok, 0 errores\"\n}"
            },
            "url": {
              "raw": "{{base_url}}/?c=agentapi&a=solution&Id={{case_id}}",
              "host": ["{{base_url}}"],
              "path": [""],
              "query": [
                { "key": "c", "value": "agentapi" },
                { "key": "a", "value": "solution" },
                { "key": "Id", "value": "{{case_id}}" }
              ]
            },
            "description": "`applied: true` exige `verification`. La respuesta trae `puede_resolver` y `faltantes_para_resolver`."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Responde 201 y habilita la resolución', function () {",
                  "    pm.response.to.have.status(201);",
                  "    pm.expect(pm.response.json().data.puede_resolver).to.be.true;",
                  "});",
                  "pm.collectionVariables.set('request_id', '');"
                ]
              }
            }
          ]
        },
        {
          "name": "9) Cerrar (espera 422: prohibido)",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{agent_id}}\",\n  \"request_id\": \"{{request_id}}\",\n  \"to_status\": \"Cerrado\",\n  \"reason\": \"Intento de cierre directo\"\n}"
            },
            "url": {
              "raw": "{{base_url}}/?c=agentapi&a=changeStatus&Id={{case_id}}",
              "host": ["{{base_url}}"],
              "path": [""],
              "query": [
                { "key": "c", "value": "agentapi" },
                { "key": "a", "value": "changeStatus" },
                { "key": "Id", "value": "{{case_id}}" }
              ]
            },
            "description": "El agente nunca cierra casos. La respuesta trae los destinos que sí puede usar."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Responde 422 TRANSITION_NOT_ALLOWED', function () {",
                  "    pm.response.to.have.status(422);",
                  "    const e = pm.response.json().error;",
                  "    pm.expect(e.code).to.eql('TRANSITION_NOT_ALLOWED');",
                  "    pm.expect(e.details.destinos_permitidos).to.be.an('array');",
                  "});",
                  "pm.collectionVariables.set('request_id', '');"
                ]
              }
            }
          ]
        },
        {
          "name": "10) Resolver (con case_version)",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{agent_id}}\",\n  \"request_id\": \"{{request_id}}\",\n  \"from_status\": \"En progreso\",\n  \"to_status\": \"Resuelto\",\n  \"reason\": \"Token renovado y sincronización verificada\",\n  \"case_version\": {{case_version}}\n}"
            },
            "url": {
              "raw": "{{base_url}}/?c=agentapi&a=changeStatus&Id={{case_id}}",
              "host": ["{{base_url}}"],
              "path": [""],
              "query": [
                { "key": "c", "value": "agentapi" },
                { "key": "a", "value": "changeStatus" },
                { "key": "Id", "value": "{{case_id}}" }
              ]
            },
            "description": "Si `case_version` está desactualizada responde 409 VERSION_CONFLICT: hay que releer el caso. Ejecute antes 'Detalle del caso' para refrescar la variable."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 409) {",
                  "    pm.test('Conflicto de versión detectado', function () {",
                  "        pm.expect(pm.response.json().error.code).to.eql('VERSION_CONFLICT');",
                  "    });",
                  "} else {",
                  "    pm.test('Resuelto', function () {",
                  "        pm.response.to.have.status(200);",
                  "        pm.expect(pm.response.json().data.estado_nuevo).to.eql('Resuelto');",
                  "    });",
                  "}",
                  "pm.collectionVariables.set('request_id', '');"
                ]
              }
            }
          ]
        },
        {
          "name": "11) Solicitar aprobación para cerrar",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{agent_id}}\",\n  \"request_id\": \"{{request_id}}\",\n  \"operation\": \"close\",\n  \"justification\": \"El caso quedó resuelto y verificado; solicito autorización para cerrarlo.\",\n  \"payload\": {\n    \"to_status\": \"Cerrado\",\n    \"reason\": \"Solución verificada\"\n  }\n}"
            },
            "url": {
              "raw": "{{base_url}}/?c=agentapi&a=requestApproval&Id={{case_id}}",
              "host": ["{{base_url}}"],
              "path": [""],
              "query": [
                { "key": "c", "value": "agentapi" },
                { "key": "a", "value": "requestApproval" },
                { "key": "Id", "value": "{{case_id}}" }
              ]
            },
            "description": "Responde 202 y NO ejecuta nada. Resuélvala con `php bin/agent-approval approve --id=N --user-id=1`."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Responde 202 sin ejecutar la operación', function () {",
                  "    pm.response.to.have.status(202);",
                  "    pm.expect(pm.response.json().data.operacion_ejecutada).to.be.false;",
                  "});",
                  "pm.collectionVariables.set('request_id', '');"
                ]
              }
            }
          ]
        },
        {
          "name": "12) Liberar el caso",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{agent_id}}\",\n  \"request_id\": \"{{request_id}}\",\n  \"reason\": \"Trabajo terminado\"\n}"
            },
            "url": {
              "raw": "{{base_url}}/?c=agentapi&a=release&Id={{case_id}}",
              "host": ["{{base_url}}"],
              "path": [""],
              "query": [
                { "key": "c", "value": "agentapi" },
                { "key": "a", "value": "release" },
                { "key": "Id", "value": "{{case_id}}" }
              ]
            },
            "description": "Libere siempre, incluso en los caminos de error: un caso retenido bloquea a los demás hasta que el lease expire."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Liberado', function () {",
                  "    pm.response.to.have.status(200);",
                  "    pm.expect(pm.response.json().data.claim.activo).to.be.false;",
                  "});",
                  "pm.collectionVariables.set('request_id', '');"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "4. Otros caminos",
      "item": [
        {
          "name": "Escalar a un grupo",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{agent_id}}\",\n  \"request_id\": \"{{request_id}}\",\n  \"reason\": \"Requiere acceso al servidor de correo que no tengo autorizado\",\n  \"group_id\": {{group_id}}\n}"
            },
            "url": {
              "raw": "{{base_url}}/?c=agentapi&a=escalate&Id={{case_id}}",
              "host": ["{{base_url}}"],
              "path": [""],
              "query": [
                { "key": "c", "value": "agentapi" },
                { "key": "a", "value": "escalate" },
                { "key": "Id", "value": "{{case_id}}" }
              ]
            },
            "description": "Requiere el bloqueo y el scope `cases:escalate`. Libera el bloqueo automáticamente. Alternativa: `assignee_id` con un técnico de soporte."
          }
        },
        {
          "name": "Solicitar información y dejar en espera",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{agent_id}}\",\n  \"request_id\": \"{{request_id}}\",\n  \"to_status\": \"En espera\",\n  \"reason\": \"Falta la versión del navegador del solicitante\"\n}"
            },
            "url": {
              "raw": "{{base_url}}/?c=agentapi&a=changeStatus&Id={{case_id}}",
              "host": ["{{base_url}}"],
              "path": [""],
              "query": [
                { "key": "c", "value": "agentapi" },
                { "key": "a", "value": "changeStatus" },
                { "key": "Id", "value": "{{case_id}}" }
              ]
            },
            "description": "Al pasar a espera, el servidor aplica el subestado que pausa el ANS, si la mesa tiene uno configurado."
          }
        },
        {
          "name": "Idempotencia: repetir un request_id",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{agent_id}}\",\n  \"request_id\": \"idempotencia-fija-0001\",\n  \"type\": \"internal\",\n  \"message\": \"Este comentario debe registrarse una sola vez.\"\n}"
            },
            "url": {
              "raw": "{{base_url}}/?c=agentapi&a=comment&Id={{case_id}}",
              "host": ["{{base_url}}"],
              "path": [""],
              "query": [
                { "key": "c", "value": "agentapi" },
                { "key": "a", "value": "comment" },
                { "key": "Id", "value": "{{case_id}}" }
              ]
            },
            "description": "Envíe DOS veces. La segunda devuelve el resultado original con `replayed: true` y no vuelve a comentar el caso. Requiere tener el bloqueo."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const d = pm.response.json();",
                  "if (d.replayed) {",
                  "    pm.test('Segundo envío: resultado repetido, sin operar de nuevo', function () {",
                  "        pm.expect(d.replayed).to.be.true;",
                  "    });",
                  "} else {",
                  "    pm.test('Primer envío: operación aplicada', () => pm.expect(d.success).to.be.true);",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "Acción inexistente (espera 404)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/?c=agentapi&a=borrarTodo",
              "host": ["{{base_url}}"],
              "path": [""],
              "query": [
                { "key": "c", "value": "agentapi" },
                { "key": "a", "value": "borrarTodo" }
              ]
            },
            "description": "Una acción no declarada responde 404 JSON, no un redirect."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Responde 404 NOT_FOUND', function () {",
                  "    pm.response.to.have.status(404);",
                  "    pm.expect(pm.response.json().error.code).to.eql('NOT_FOUND');",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    }
  ]
}
