{
  "info": {
    "_postman_id": "d28a0708-34b4-53cc-8a86-6857f8f14ce3",
    "name": "Dialer Core API (/v1)",
    "description": "Superficie /v1 de dd/core (fuente de verdad: lib/dialer/api/v1_router.ex; shapes en API.md).\n\nAuth: Bearer {{api_token}} a nivel colección (token de tenant; importa dialer-public.postman_environment.json y pega tu propio token en `api_token`).\n\nDiseñada para correrse COMPLETA y EN ORDEN (newman lo hace por defecto): la carpeta '03 · golden-flow' siembra las variables {{golden_*}} que consumen las carpetas de dominio. Requests sueltos contra ids no sembrados responden el 404 fail-closed del contrato.\n\nGate anti-drift: .ci/qa/check_postman_drift.py compara las rutas del router contra esta colección en ambas direcciones (rutas faltantes Y requests huérfanos).\n\nCuota por API key (core#1471): cada key dd_ tiene su propio presupuesto de peticiones por minuto en /v1. Toda respuesta contada de un dd_ (no cuentan POST /v1/calls/{call_id}/hangup ni la petición admitida cuando el limitador no puede decidir) lleva X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset; pasado el cupo, 429 `rate_limited` con Retry-After (segundos). El test a nivel colección valida esas cabeceras en CADA request en cuanto aparecen, y que todo 429 con código `rate_limited` traiga Retry-After entero >= 1.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{api_token}}",
        "type": "string"
      }
    ]
  },
  "item": [
    {
      "name": "00 · smoke",
      "description": "Liveness sin auth (fuera de /v1; el gate anti-drift solo compara /v1).",
      "item": [
        {
          "name": "GET /healthz",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/healthz",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "healthz"
              ]
            },
            "description": "Liveness del control plane (sin bearer).",
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"body ok\", function () { pm.expect(pm.response.text()).to.not.be.empty; });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "01 · identity",
      "description": "Tenant autenticado por el bearer (RLS; nunca se acepta tenant_id del cliente).",
      "item": [
        {
          "name": "GET /v1/me",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/me",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me"
              ]
            },
            "description": "Tenant del token. Siembra {{tenant_id}} y {{tenant_name}} para el resto de la carpeta."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"cuota core#1471: la llave dd_ admitida trae X-RateLimit-Limit\", function () { pm.expect(pm.response.headers.get(\"X-RateLimit-Limit\"), \"X-RateLimit-Limit\").to.be.a(\"string\"); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: tenant con id/name/tier\", function () {",
                  "  pm.expect(j.tenant).to.be.an(\"object\");",
                  "  pm.expect(j.tenant.id).to.be.a(\"string\");",
                  "  pm.expect(j.tenant).to.have.property(\"name\");",
                  "  pm.expect(j.tenant).to.have.property(\"tier\");",
                  "});",
                  "// core#1059 (ADR #137): /me dice CUAL es el asiento del que llama. Esta",
                  "// peticion va con la llave dd_ de MAQUINA, que no tiene persona detras.",
                  "pm.test(\"seat: una llave dd_ no tiene asiento propio, y lo dice\", function () {",
                  "  pm.expect(j.seat, \"seat\").to.equal(null);",
                  "  pm.expect(j.seat_source, \"seat_source\").to.equal(\"no_identity\");",
                  "});",
                  "pm.collectionVariables.set(\"tenant_id\", j.tenant.id);",
                  "pm.collectionVariables.set(\"tenant_name\", j.tenant.name || \"localdev\");"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/tenants",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/tenants",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "tenants"
              ]
            },
            "description": "Lista de un elemento: el propio tenant (RLS)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: tenants[] con exactamente el propio tenant\", function () {",
                  "  pm.expect(j.tenants).to.be.an(\"array\").with.lengthOf(1);",
                  "  pm.expect(j.tenants[0].id).to.eql(pm.collectionVariables.get(\"tenant_id\"));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/tenants/:id",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/tenants/{{tenant_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "tenants",
                "{{tenant_id}}"
              ]
            },
            "description": "El propio tenant; cualquier otro id es 404 (nunca 403, sin fuga de existencia)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: tenant.id coincide\", function () {",
                  "  pm.expect(j.tenant.id).to.eql(pm.collectionVariables.get(\"tenant_id\"));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/tenant",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/tenant",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "tenant"
              ]
            },
            "description": "Solo `name` es editable por el tenant (tier/seats/voice_ai son superficie admin). Idempotente: re-escribe el nombre actual.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"{{tenant_name}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: tenant.name persiste\", function () {",
                  "  pm.expect(j.tenant.name).to.eql(pm.collectionVariables.get(\"tenant_name\"));",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "02 · api-keys",
      "description": "Mint/revocación de llaves del tenant. El plaintext se muestra UNA vez y jamás se loguea aquí.",
      "item": [
        {
          "name": "GET /v1/api_keys",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/api_keys",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "api_keys"
              ]
            },
            "description": "Lista (hashes nunca expuestos)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: api_keys[]\", function () { pm.expect(j.api_keys).to.be.an(\"array\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/api_keys",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/api_keys",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "api_keys"
              ]
            },
            "description": "Mint. La respuesta trae `token` en claro UNA sola vez — este test NO lo imprime ni lo guarda.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"label\": \"postman-smoke-{{$timestamp}}\",\n  \"environment\": \"live\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: api_key.id + token presente (no se persiste)\", function () {",
                  "  pm.expect(j.api_key.id).to.be.a(\"string\");",
                  "  pm.expect(j.token).to.be.a(\"string\").and.to.not.be.empty;",
                  "});",
                  "pm.test(\"environment: live (explicito en el body)\", function () { pm.expect(j.api_key.environment).to.eql(\"live\"); });",
                  "pm.collectionVariables.set(\"api_key_id\", j.api_key.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/api_keys/:id",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\"allowed_cidrs\": []}"
            },
            "url": {
              "raw": "{{baseUrl}}/v1/api_keys/{{api_key_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "api_keys",
                "{{api_key_id}}"
              ]
            },
            "description": "Fija la lista de redes (allowed_cidrs) de la llave recién minteada (core#2450). [] la deja sin restricción, así que el smoke no se bloquea a sí mismo."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: api_key con allowed_cidrs y stale\", function () {",
                  "  pm.expect(j.api_key.id).to.eql(pm.collectionVariables.get(\"api_key_id\"));",
                  "  pm.expect(j.api_key.allowed_cidrs).to.be.an(\"array\");",
                  "  pm.expect(j.api_key.stale).to.be.a(\"boolean\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/api_keys/:id/rotate",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\"overlap_seconds\": 60}"
            },
            "url": {
              "raw": "{{baseUrl}}/v1/api_keys/{{api_key_id}}/rotate",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "api_keys",
                "{{api_key_id}}",
                "rotate"
              ]
            },
            "description": "Rota la llave recién minteada (core#2850): emite la nueva y deja la vieja viva 60 s. El `token` nuevo viene en claro UNA vez — este test NO lo imprime ni lo guarda. La vieja caduca sola a los 60 s; el test deja la nueva en `api_key_id` y el DELETE siguiente la revoca."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: api_key nueva + token + rotated con expires_at\", function () {",
                  "  pm.expect(j.api_key.id).to.be.a(\"string\").and.to.not.eql(pm.collectionVariables.get(\"api_key_id\"));",
                  "  pm.expect(j.token).to.be.a(\"string\").and.to.not.be.empty;",
                  "  pm.expect(j.rotated.id).to.eql(pm.collectionVariables.get(\"api_key_id\"));",
                  "  pm.expect(j.rotated.expires_at).to.be.a(\"string\");",
                  "});",
                  "pm.collectionVariables.set(\"api_key_id\", j.api_key.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/api_keys/:id/requests",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/api_keys/{{api_key_id}}/requests",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "api_keys",
                "{{api_key_id}}",
                "requests"
              ]
            },
            "description": "Rastro de peticiones de la clave (core#2852): método, plantilla de ruta, estado, duración y request_id; sin cuerpos ni cabeceras."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: requests[] + next_cursor\", function () { pm.expect(j.requests).to.be.an(\"array\"); pm.expect(j).to.have.property(\"next_cursor\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/api_keys/:id/requests/summary",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/api_keys/{{api_key_id}}/requests/summary",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "api_keys",
                "{{api_key_id}}",
                "requests",
                "summary"
              ]
            },
            "description": "Peticiones, 429 y 5xx por minuto de la clave (core#2852)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: minutes[]\", function () { pm.expect(j.minutes).to.be.an(\"array\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/api_keys/:id",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/api_keys/{{api_key_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "api_keys",
                "{{api_key_id}}"
              ]
            },
            "description": "Revoca (nunca borra) la llave recién minteada."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: api_key revocada (revoked_at)\", function () {",
                  "  pm.expect(j.api_key.id).to.eql(pm.collectionVariables.get(\"api_key_id\"));",
                  "  pm.expect(j.api_key.revoked_at).to.not.be.null;",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "03 · golden-flow",
      "description": "Flujo encadenado tipo golden: crear campaña → importar leads → next-lead → checkin → start → dial manual → stats → stop → checkout. Siembra {{golden_*}} para las carpetas de dominio. Correr EN ORDEN (newman lo hace solo).",
      "item": [
        {
          "name": "1. crear campaña",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns"
              ]
            },
            "description": "Nace en `draft`. La ventana de campaña queda abierta; la gate legal de quiet-hours es aparte (por eso el pre-request elige timezone en ventana).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"postman-golden-{{$timestamp}}\",\n  \"default_timezone\": \"{{golden_tz}}\",\n  \"calling_window_start\": \"00:00:00\",\n  \"calling_window_end\": \"23:59:59\",\n  \"abandonment_threshold\": 0.03,\n  \"caller_ids\": [\n    \"+13125550199\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Timezone en ventana quiet-hours AHORA (mismo truco que tools/demo.sh pick_tz):",
                  "// la gate legal [08:00,21:00) evalúa hora local del lead; elegimos margen [9,20).",
                  "const zones = [",
                  "  { tz: \"America/New_York\", state: \"NY\", city: \"Buffalo\" },",
                  "  { tz: \"America/Chicago\", state: \"TX\", city: \"Dallas\" },",
                  "  { tz: \"America/Denver\", state: \"CO\", city: \"Denver\" },",
                  "  { tz: \"America/Phoenix\", state: \"AZ\", city: \"Phoenix\" },",
                  "  { tz: \"America/Los_Angeles\", state: \"CA\", city: \"Fresno\" },",
                  "  { tz: \"Pacific/Honolulu\", state: \"HI\", city: \"Hilo\" }",
                  "];",
                  "function localHour(tz) {",
                  "  try {",
                  "    return parseInt(new Intl.DateTimeFormat(\"en-US\", { timeZone: tz, hour: \"2-digit\", hour12: false }).format(new Date()), 10) % 24;",
                  "  } catch (e) { return null; }",
                  "}",
                  "const pick = zones.find(z => { const h = localHour(z.tz); return h !== null && h >= 9 && h < 20; }) || zones[1];",
                  "pm.collectionVariables.set(\"golden_tz\", pick.tz);",
                  "pm.collectionVariables.set(\"golden_state\", pick.state);",
                  "pm.collectionVariables.set(\"golden_city\", pick.city);"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: campaign draft con id\", function () {",
                  "  pm.expect(j.campaign.id).to.be.a(\"string\");",
                  "  pm.expect(j.campaign.state).to.eql(\"draft\");",
                  "});",
                  "pm.collectionVariables.set(\"golden_campaign_id\", j.campaign.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "2. importar leads",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/debts/import",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debts",
                "import"
              ]
            },
            "description": "Import bulk idempotente (upsert por external_ref / phone). 3 deudas asignadas a la campaña golden. Cada deuda lleva debtor_name (opcional, cifrado en reposo, core#1529).",
            "body": {
              "mode": "raw",
              "raw": "{{golden_import_body}}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// 3 deudas frescas con refs únicos por corrida; contactos landline (sin gate TCPA-consent).",
                  "const ts = Date.now();",
                  "const tz = pm.collectionVariables.get(\"golden_tz\") || \"America/Chicago\";",
                  "const st = pm.collectionVariables.get(\"golden_state\") || \"TX\";",
                  "const city = pm.collectionVariables.get(\"golden_city\") || \"Dallas\";",
                  "const debts = [];",
                  "for (let i = 0; i < 3; i++) {",
                  "  debts.push({",
                  "    external_ref: `postman-${ts}-${i}`,",
                  "    consumer_ref: `postman-consumer-${ts}-${i}`,",
                  "    debtor_name: `Postman Deudor ${ts}-${i}`,",
                  "    debt_type: \"other\",",
                  "    amount_cents: 125000 + i * 2500,",
                  "    currency: \"USD\",",
                  "    campaign_id: pm.collectionVariables.get(\"golden_campaign_id\"),",
                  "    contacts: [{",
                  "      phone_e164: \"+1312557\" + String((ts + i) % 10000).padStart(4, \"0\"),",
                  "      line_type: \"landline\",",
                  "      timezone: tz,",
                  "      us_state: st,",
                  "      city: city,",
                  "      is_primary: true",
                  "    }]",
                  "  });",
                  "}",
                  "pm.collectionVariables.set(\"golden_import_body\", JSON.stringify({ debts }, null, 2));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: 3/3 importadas, 0 fallidas\", function () {",
                  "  pm.expect(j.imported).to.eql(3);",
                  "  pm.expect(j.failed).to.be.an(\"array\").with.lengthOf(0);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "3. next-lead (preview)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{golden_campaign_id}}/next-lead",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{golden_campaign_id}}",
                "next-lead"
              ]
            },
            "description": "El SIGUIENTE lead marcable, sin marcarlo. Siembra {{golden_debt_id}} y {{golden_contact_phone}}."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: lead no nulo con debt_id + contact_phone\", function () {",
                  "  pm.expect(j.lead).to.be.an(\"object\").and.to.not.be.null;",
                  "  pm.expect(j.lead.debt_id).to.be.a(\"string\");",
                  "  pm.expect(j.lead.contact_phone).to.be.a(\"string\");",
                  "});",
                  "pm.test(\"debtor_name: opcional, texto o null (core#1529)\", function () {",
                  "  pm.expect(j.lead).to.have.property(\"debtor_name\");",
                  "  if (j.lead.debtor_name !== null) { pm.expect(j.lead.debtor_name).to.be.a(\"string\").and.not.empty; }",
                  "});",
                  "if (j.lead) {",
                  "  pm.collectionVariables.set(\"golden_debt_id\", j.lead.debt_id);",
                  "  pm.collectionVariables.set(\"golden_contact_phone\", j.lead.contact_phone);",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "4. roster de agentes",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/agents",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents"
              ]
            },
            "description": "Captura el seat 1000 del seed del harness (Dialer.Release.seed()); si no está, usa el primero del roster."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: agents[] con al menos un seat (corre el seed si falla)\", function () {",
                  "  pm.expect(j.agents).to.be.an(\"array\").and.to.have.length.above(0);",
                  "});",
                  "const seat = (j.agents || []).find(a => a.sip_extension === \"1000\") || (j.agents || [])[0];",
                  "if (seat) {",
                  "  pm.collectionVariables.set(\"golden_agent_id\", seat.id);",
                  "  pm.collectionVariables.set(\"golden_agent_ext\", seat.sip_extension || \"1000\");",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "5. checkin del agente",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/agents/checkin",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents",
                "checkin"
              ]
            },
            "description": "Seat online + bootstrap softphone (cred SIP efímera vía kamailio RPC; el password jamás se loguea aquí).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"sip_extension\": \"{{golden_agent_ext}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: agent presente (sip solo si hay SBC)\", function () {",
                  "  pm.expect(j.agent).to.be.an(\"object\");",
                  "  pm.expect(j.agent.id).to.be.a(\"string\");",
                  "});",
                  "// core#1076 (MT-CTI-16): sip.turn sólo si la celda tiene TURN; si viene, forma RTCIceServer",
                  "// con la credencial efímera de coturn y la MISMA caducidad que la contraseña SIP.",
                  "pm.test(\"shape: sip.turn (si lo hay) = RTCIceServer efímero con el horizonte SIP\", function () {",
                  "  if (!j.sip || !j.sip.turn) { return; }",
                  "  const t = j.sip.turn;",
                  "  pm.expect(t.urls).to.be.an(\"array\").that.is.not.empty;",
                  "  t.urls.forEach(function (u) { pm.expect(u).to.match(/^turns?:./); });",
                  "  pm.expect(t.credential).to.be.a(\"string\").and.not.empty;",
                  "  const exp = Math.floor(Date.parse(j.sip.expires_at) / 1000);",
                  "  pm.expect(t.username).to.equal(exp + \":\" + j.agent.id);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "6. start campaña",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{golden_campaign_id}}/start",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{golden_campaign_id}}",
                "start"
              ]
            },
            "description": "Persiste running + arranca el runner (gateway `default` → SIPp mock carrier del harness).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"tick_ms\": 1000,\n  \"batch_size\": 2,\n  \"gateway\": \"default\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: campaign running\", function () {",
                  "  pm.expect(j.campaign.state).to.eql(\"running\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "7. dial manual",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{golden_campaign_id}}/dial",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{golden_campaign_id}}",
                "dial"
              ]
            },
            "description": "Dial iniciado por humano (preview/manual). La campaña de esta carpeta es `predictive` porque el cuerpo de `1. crear campaña` NO manda `campaign_type` y el default del schema es `predictive` — NO porque el API lo impida: `campaign_type` sí es campo aceptado en POST /v1/campaigns. Añadirlo a ese cuerpo cambiaría el perfil de compliance de la campaña (incluido si aplica el gate ArtificialVoice) y esta rama dejaría de valer. Con el cuerpo actual lo esperado contra este stack es 422 `not_human_initiated`; en una campaña manual/preview sería 200 {dialing|blocked}. Ambos se aceptan y se valida la shape de cada rama.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"debt_id\": \"{{golden_debt_id}}\",\n  \"agent_id\": \"{{golden_agent_id}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200 (manual/preview) o 422 (predictive: auto-marca)\", function () {",
                  "  pm.expect([200, 422]).to.include(pm.response.code);",
                  "});",
                  "const j = pm.response.json();",
                  "if (pm.response.code === 200) {",
                  "  pm.test(\"shape 200: status dialing|blocked\", function () {",
                  "    pm.expect([\"dialing\", \"blocked\"]).to.include(j.status);",
                  "  });",
                  "  if (j.status === \"dialing\") { pm.collectionVariables.set(\"golden_call_id\", j.call_id); }",
                  "} else {",
                  "  pm.test(\"shape 422: error.code unprocessable\", function () {",
                  "    pm.expect(j.error.code).to.eql(\"unprocessable\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "7b. dial manual de un número tecleado sin cuenta (dialpad)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/dial/manual",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "dial",
                "manual"
              ]
            },
            "description": "Dialpad del widget (core#1455): el número TECLEADO se resuelve contra los teléfonos de las cuentas del tenant. Un E.164 que ninguna cuenta tiene es 422 `no_account` y NO se marca nada (fail-closed: sin cuenta no hay deuda sobre la que decidir Reg F ni consentimiento). El número sale de un prerequest aleatorio fuera de las cuentas de la corrida; la aserción anti-catch-all exige que el 422 sea el del endpoint real, no un `no such route`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"number\": \"{{golden_typed_unknown}}\",\n  \"agent_id\": \"{{golden_agent_id}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// E.164 aleatorio en un prefijo que el import de esta colección no usa (+1312557xxxx): nunca coincide con una cuenta.",
                  "const n = \"+1999\" + String(Math.floor(Math.random() * 1e7)).padStart(7, \"0\");",
                  "pm.collectionVariables.set(\"golden_typed_unknown\", n);"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 422 (sin cuenta, nada se marca)\", function () { pm.response.to.have.status(422); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: error.code no_account, del endpoint real (no catch-all)\", function () {",
                  "  pm.expect(j.error.code).to.eql(\"no_account\");",
                  "  pm.expect(j.error.message).to.not.eql(\"no such route\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "7c. dial manual de un número tecleado de una cuenta (dialpad)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/dial/manual",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "dial",
                "manual"
              ]
            },
            "description": "Dialpad (core#1455), número de una cuenta del tenant (`{{golden_contact_phone}}`, sembrado por `3. next-lead`): se resuelve a ESA cuenta y entra por el mismo camino que el clic de preview. Como en `7. dial manual`, la campaña de esta carpeta es `predictive`: lo esperado es 422 `unprocessable` (`not_human_initiated`); en una campaña manual/preview sería 200 {dialing|blocked}. Lo que NUNCA puede salir es `no_account`/`ambiguous_account`/`invalid_number`: el número SÍ es de una cuenta.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"number\": \"{{golden_contact_phone}}\",\n  \"agent_id\": \"{{golden_agent_id}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200 (manual/preview) o 422 (predictive: no es humano-iniciada)\", function () {",
                  "  pm.expect([200, 422]).to.include(pm.response.code);",
                  "});",
                  "const j = pm.response.json();",
                  "if (pm.response.code === 200) {",
                  "  pm.test(\"shape 200: status dialing|blocked\", function () {",
                  "    pm.expect([\"dialing\", \"blocked\"]).to.include(j.status);",
                  "  });",
                  "} else {",
                  "  pm.test(\"422 de la campaña (no de la resolución): la cuenta SÍ se resolvió\", function () {",
                  "    pm.expect(j.error.code).to.not.be.oneOf([\"no_account\", \"ambiguous_account\", \"invalid_number\"]);",
                  "    pm.expect(j.error.code).to.eql(\"unprocessable\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "8. stats de campaña",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{golden_campaign_id}}/stats",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{golden_campaign_id}}",
                "stats"
              ]
            },
            "description": "Contadores vivos del runner + pacing."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: campaign_id + flag running\", function () {",
                  "  pm.expect(j.campaign_id).to.eql(pm.collectionVariables.get(\"golden_campaign_id\"));",
                  "  pm.expect(j).to.have.property(\"running\");",
                  "  pm.expect(j).to.have.property(\"stats\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "8b. compliance-preflight de campaña (dry-run)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{golden_campaign_id}}/compliance-preflight",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{golden_campaign_id}}",
                "compliance-preflight"
              ]
            },
            "description": "Dry-run del motor de compliance sobre los leads marcables de la campaña (core#924, MT-UX-11): un agregado por estado (reglas, elegibles, suprimidos por regla y motivo), sin marcar nada y sin devolver leads. Honesto sobre el gate estatal configurable: con `state_matrix_unknown_state_fails_strict` apagada (default) `unknown_state_policy` y `tenant_gates.state_matrix` dicen `federal_fallback`, nunca «protegido». Desde core#1181 trae además `list_health`: cada señal (consentimiento, estado resoluble, DNC, RND) contada sin cortocircuito sobre las mismas deudas, con `no_verificable` contado como bloqueo, la edad de cada snapshot DNC y el estado del proveedor RND."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: campaign_id + posture + totals + states\", function () {",
                  "  pm.expect(j.campaign_id).to.eql(pm.collectionVariables.get(\"golden_campaign_id\"));",
                  "  pm.expect(j.unknown_state_policy).to.be.oneOf([\"strict\", \"federal_fallback\"]);",
                  "  pm.expect(j.tenant_gates).to.have.property(\"state_matrix\");",
                  "  pm.expect(j.tenant_gates.state_matrix).to.be.oneOf([\"active\", \"federal_fallback\"]);",
                  "  pm.expect(j.totals).to.have.all.keys(\"leads\", \"evaluated\", \"eligible\", \"suppressed\", \"unknown_state\");",
                  "  pm.expect(j.truncated).to.be.a(\"boolean\");",
                  "  pm.expect(j.states).to.be.an(\"object\");",
                  "});",
                  "pm.test(\"honesto: la postura del gate estatal y la bandera cuentan lo mismo\", function () {",
                  "  const strict = j.unknown_state_policy === \"strict\";",
                  "  pm.expect(j.tenant_gates.state_matrix).to.eql(strict ? \"active\" : \"federal_fallback\");",
                  "});",
                  "pm.test(\"contadores cuadran: eligible + suppressed == evaluated, por estado y en total\", function () {",
                  "  pm.expect(j.totals.eligible + j.totals.suppressed).to.eql(j.totals.evaluated);",
                  "  for (const [state, b] of Object.entries(j.states)) {",
                  "    pm.expect(b.eligible + b.suppressed, state).to.eql(b.leads);",
                  "  }",
                  "});",
                  "pm.test(\"list_health (core#1181): cuatro señales; blocked incluye no_verificable\", function () {",
                  "  const h = j.list_health;",
                  "  pm.expect(h.signals).to.have.all.keys(\"consent\", \"state\", \"dnc\", \"rnd\");",
                  "  for (const [name, s] of Object.entries(h.signals)) {",
                  "    pm.expect(s.status, name).to.be.oneOf([\"ok\", \"blocking\", \"no_verificable\"]);",
                  "    pm.expect(s.blocked, name).to.be.at.least(s.no_verificable);",
                  "    pm.expect(s.blocked, name).to.be.at.most(j.totals.evaluated);",
                  "    if (s.no_verificable > 0) { pm.expect(s.status, name).to.eql(\"no_verificable\"); }",
                  "    if (s.status === \"ok\") { pm.expect(s.blocked, name).to.eql(0); }",
                  "  }",
                  "  pm.expect(h.dnc_snapshots).to.have.all.keys(\"federal\", \"state\", \"internal_global\");",
                  "  for (const snap of Object.values(h.dnc_snapshots)) {",
                  "    pm.expect(snap.status).to.be.oneOf([\"provisioned\", \"stale\", \"absent\"]);",
                  "  }",
                  "  pm.expect(h.dnc_state_registries).to.be.an(\"object\");",
                  "  pm.expect(h.rnd_provider.status).to.be.oneOf([\"declared\", \"stub\", \"no_verificable\"]);",
                  "});",
                  "pm.test(\"list_health (core#1181): una premisa no verificable nunca sale sana\", function () {",
                  "  const h = j.list_health;",
                  "  if (h.rnd_provider.status === \"no_verificable\") { pm.expect(h.signals.rnd.status).to.eql(\"no_verificable\"); }",
                  "  const dncUnverifiable = Object.values(h.dnc_snapshots).some((s) => s.status !== \"provisioned\");",
                  "  if (dncUnverifiable) { pm.expect(h.signals.dnc.status).to.eql(\"no_verificable\"); }",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "9. stop campaña",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{golden_campaign_id}}/stop",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{golden_campaign_id}}",
                "stop"
              ]
            },
            "description": "Persiste completed + detiene el runner (in-flight termina solo)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: campaign completed\", function () {",
                  "  pm.expect(j.campaign.state).to.eql(\"completed\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "10. checkout del agente",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/agents/checkout",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents",
                "checkout"
              ]
            },
            "description": "Seat offline (revoca la cred SIP). 409 si el seat sigue en llamada in-flight — se acepta.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"sip_extension\": \"{{golden_agent_ext}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200 (libre) o 409 (seat aún en llamada)\", function () {",
                  "  pm.expect([200, 409]).to.include(pm.response.code);",
                  "});",
                  "if (pm.response.code === 200) {",
                  "  const j = pm.response.json();",
                  "  pm.test(\"shape 200: agent presente\", function () { pm.expect(j.agent).to.be.an(\"object\"); });",
                  "}"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "04 · campaigns",
      "description": "CRUD + ciclo de vida completo + rewind sobre una campaña propia de esta carpeta.",
      "item": [
        {
          "name": "POST /v1/campaigns",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns"
              ]
            },
            "description": "Alta mínima (nace draft, defaults del schema).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"postman-crud-{{$timestamp}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: campaign draft\", function () {",
                  "  pm.expect(j.campaign.id).to.be.a(\"string\");",
                  "  pm.expect(j.campaign.state).to.eql(\"draft\");",
                  "});",
                  "pm.collectionVariables.set(\"campaign_id\", j.campaign.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/campaigns",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: campaigns[]\", function () {",
                  "  pm.expect(j.campaigns).to.be.an(\"array\").and.to.have.length.above(0);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/campaigns/:id",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{campaign_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{campaign_id}}"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: campaign.id coincide\", function () {",
                  "  pm.expect(j.campaign.id).to.eql(pm.collectionVariables.get(\"campaign_id\"));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/campaigns/:id",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{campaign_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{campaign_id}}"
              ]
            },
            "description": "Actualiza config (nunca state).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"record_calls\": false\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: record_calls actualizado\", function () {",
                  "  pm.expect(j.campaign.record_calls).to.eql(false);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/campaigns/:id — ventana semanal",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{campaign_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{campaign_id}}"
              ]
            },
            "description": "Ventana por día (core#1192, ADR #163): sólo ESTRECHA la ventana única y quiet hours; un día que no aparece queda cerrado. Se guarda normalizada a HH:MM:SS y un PATCH la reemplaza entera.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"calling_window_weekly\": {\n    \"mon\": {\n      \"start\": \"09:00\",\n      \"end\": \"17:00\"\n    },\n    \"sat\": {\n      \"start\": \"10:00\",\n      \"end\": \"14:00\"\n    }\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: ventana semanal normalizada a HH:MM:SS\", function () {",
                  "  pm.expect(j.campaign.calling_window_weekly).to.eql({",
                  "    mon: { start: \"09:00:00\", end: \"17:00:00\" },",
                  "    sat: { start: \"10:00:00\", end: \"14:00:00\" }",
                  "  });",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/campaigns/:id — ventana semanal hasta 24:00 (422)",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{campaign_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{campaign_id}}"
              ]
            },
            "description": "Una ventana que amplía más allá del día (24:00, o un inicio después del fin) o mal formada es 422 unprocessable y no se guarda: el fin del día se escribe 23:59:59.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"calling_window_weekly\": {\n    \"mon\": {\n      \"start\": \"09:00\",\n      \"end\": \"24:00\"\n    }\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 422\", function () { pm.response.to.have.status(422); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: unprocessable nombra el campo\", function () {",
                  "  pm.expect(j.error.code).to.eql(\"unprocessable\");",
                  "  pm.expect(j.error.message).to.include(\"calling_window_weekly\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/campaigns/:id — quita la ventana semanal",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{campaign_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{campaign_id}}"
              ]
            },
            "description": "null quita la capa semanal: la campaña vuelve a la ventana única de siempre.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"calling_window_weekly\": null\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: sin ventana semanal\", function () {",
                  "  pm.expect(j.campaign.calling_window_weekly).to.eql(null);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/campaigns/:id/survey-results",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{campaign_id}}/survey-results",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{campaign_id}}",
                "survey-results"
              ]
            },
            "description": "MT-UX-07 (core#916): distribución de teclas DTMF de las respuestas de encuesta/IVR de la campaña sobre [from, to] (30 días UTC por defecto), tenant-scoped (la campaña de otro tenant ⇒ 404). Sobre una campaña recién creada la distribución es vacía y honesta (total 0), no un error. `?format=csv` devuelve el mismo artefacto en CSV."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: campaign_id, ventana, total y distribution\", function () {",
                  "  pm.expect(j.campaign_id).to.eql(pm.collectionVariables.get(\"campaign_id\"));",
                  "  pm.expect(j.from).to.be.a(\"string\");",
                  "  pm.expect(j.to).to.be.a(\"string\");",
                  "  pm.expect(j.total).to.be.a(\"number\");",
                  "  pm.expect(j.distribution).to.be.an(\"array\");",
                  "});",
                  "pm.test(\"campaña recién creada: distribución vacía, no error\", function () {",
                  "  pm.expect(j.total).to.eql(0);",
                  "  pm.expect(j.distribution).to.eql([]);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/campaigns/:id/abandonment",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{campaign_id}}/abandonment",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{campaign_id}}",
                "abandonment"
              ]
            },
            "description": "core#1324 (ADR #178): tasa de abandono durable de la campaña por tramo de 30 días (§64.1200(a)(7), TSR §310.4(b)(4)), plegada del CDR append-only; los registros que piden las dos normas. Denominador: contestadas sin veredicto AMD `machine`; numerador por motivo (`no_agent`, `ai_stream_lost` y, desde core#1383, `agent_bridge_failed`: había asiento reservado y el puente falló; la suma y la tasa no cambian); `handoff_failed` aparte, fuera de la tasa. Tenant-scoped (la campaña de otro tenant ⇒ 404). Sobre una campaña recién creada no hay tramo: `anchored_at: null` y `periods: []`, no un error. `?format=csv` devuelve el mismo informe en CSV con `X-Artifact-SHA256`."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: campaign_id, tramo de 30 días, techo del 3 % y predicado\", function () {",
                  "  pm.expect(j.campaign_id).to.eql(pm.collectionVariables.get(\"campaign_id\"));",
                  "  pm.expect(j.measured_at).to.be.a(\"string\");",
                  "  pm.expect(j.period_days).to.eql(30);",
                  "  pm.expect(j.ceiling_percent).to.eql(3);",
                  "  // Contrato ADITIVO: los motivos nuevos se anaden AL FINAL (core#1383 anadio",
                  "  // agent_bridge_failed), asi que el prefijo es fijo, el vocabulario es cerrado",
                  "  // y ningun motivo contado se reporta tambien aparte.",
                  "  pm.expect(j.predicate.counts.slice(0, 2)).to.eql([\"no_agent\", \"ai_stream_lost\"]);",
                  "  j.predicate.counts.forEach(function (r) {",
                  "    pm.expect([\"no_agent\", \"ai_stream_lost\", \"agent_bridge_failed\", \"handoff_failed\", \"late_bridge\"]).to.include(r);",
                  "    pm.expect(j.predicate.reported_apart).to.not.include(r);",
                  "  });",
                  "  pm.expect(new Set(j.predicate.counts).size).to.eql(j.predicate.counts.length);",
                  "  // core#1426 anade late_bridge AL FINAL de reported_apart: prefijo fijo y",
                  "  // vocabulario cerrado, asi que la aserción vale con el core de antes y el de despues.",
                  "  pm.expect(j.predicate.reported_apart[0]).to.eql(\"handoff_failed\");",
                  "  j.predicate.reported_apart.forEach(function (r) {",
                  "    pm.expect([\"handoff_failed\", \"late_bridge\"]).to.include(r);",
                  "  });",
                  "  pm.expect(new Set(j.predicate.reported_apart).size).to.eql(j.predicate.reported_apart.length);",
                  "  pm.expect(j.periods).to.be.an(\"array\");",
                  "});",
                  "pm.test(\"campaña recién creada: sin ancla ni tramos, no error\", function () {",
                  "  pm.expect(j.anchored_at).to.eql(null);",
                  "  pm.expect(j.periods).to.eql([]);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/campaigns/:id/start",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{campaign_id}}/start",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{campaign_id}}",
                "start"
              ]
            },
            "description": "Tick de 60 s: campaña sin leads, el runner no tiene nada que marcar.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"tick_ms\": 60000,\n  \"batch_size\": 1,\n  \"gateway\": \"default\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: running\", function () { pm.expect(j.campaign.state).to.eql(\"running\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/campaigns/:id/pause",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{campaign_id}}/pause",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{campaign_id}}",
                "pause"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: paused\", function () { pm.expect(j.campaign.state).to.eql(\"paused\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/campaigns/:id/resume",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{campaign_id}}/resume",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{campaign_id}}",
                "resume"
              ]
            },
            "description": "Resume ES start (paused → running): una semántica, dos ortografías."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: running otra vez\", function () { pm.expect(j.campaign.state).to.eql(\"running\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/campaigns/:id/rewind/preview",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{campaign_id}}/rewind/preview?max_attempts=1&amd_verdicts=machine,human",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{campaign_id}}",
                "rewind",
                "preview"
              ],
              "query": [
                {
                  "key": "max_attempts",
                  "value": "1"
                },
                {
                  "key": "amd_verdicts",
                  "value": "machine,human"
                }
              ]
            },
            "description": "Dry-run con filtros en query (forma dashboard). Nunca escribe audit."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: matching/excluded_by_compliance/truncated\", function () {",
                  "  pm.expect(j.matching).to.be.a(\"number\");",
                  "  pm.expect(j).to.have.property(\"excluded_by_compliance\");",
                  "  pm.expect(j).to.have.property(\"truncated\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/campaigns/:id/rewind/preview",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{campaign_id}}/rewind/preview",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{campaign_id}}",
                "rewind",
                "preview"
              ]
            },
            "description": "El mismo dry-run con filtros en el body (shape del handoff B1).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"max_attempts\": 1,\n  \"amd_verdicts\": [\"machine\", \"human\"]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: matching numérico\", function () { pm.expect(j.matching).to.be.a(\"number\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/campaigns/:id/rewind",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{campaign_id}}/rewind",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{campaign_id}}",
                "rewind"
              ]
            },
            "description": "Re-encola las deudas que matchean (campaña fresca: 0). CDR intacto (append-only).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"max_attempts\": 1,\n  \"amd_verdicts\": [\"machine\", \"human\"],\n  \"keep_scheduled_callbacks\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: requeued + callbacks_cancelled\", function () {",
                  "  pm.expect(j.requeued).to.be.a(\"number\");",
                  "  pm.expect(j.callbacks_cancelled).to.be.a(\"number\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/rewind-segments",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/rewind-segments",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "rewind-segments"
              ]
            },
            "description": "Guarda un segmento: un conjunto NOMBRADO de filtros del rewind (no cuentas). Los filtros pasan por el mismo validador que el rewind.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Postman · sin contestar, 3+ intentos\",\n  \"filters\": {\n    \"dispositions\": [\n      \"no_answer\",\n      \"voicemail\"\n    ],\n    \"max_attempts\": 3,\n    \"last_attempt_older_than_days\": 7\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: rewind_segment\", function () {",
                  "  pm.expect(j.rewind_segment.id).to.be.a(\"string\");",
                  "  pm.expect(j.rewind_segment.filters.max_attempts).to.eql(3);",
                  "  pm.expect(j.rewind_segment.retired_at).to.eql(null);",
                  "});",
                  "pm.collectionVariables.set(\"rewind_segment_id\", j.rewind_segment.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/rewind-segments (filtro inválido)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/rewind-segments",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "rewind-segments"
              ]
            },
            "description": "Un filtro que el rewind rechazaría se rechaza al guardar: 422 (el mismo validador).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Postman · inválido\",\n  \"filters\": {\n    \"max_attempts\": 0\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 422\", function () { pm.response.to.have.status(422); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/rewind-segments",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/rewind-segments",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "rewind-segments"
              ]
            },
            "description": "Los segmentos vigentes del tenant; los retirados no salen."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"el segmento guardado está en la lista\", function () {",
                  "  pm.expect(j.rewind_segments.map(s => s.id)).to.include(pm.collectionVariables.get(\"rewind_segment_id\"));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/rewind-segments/:id",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/rewind-segments/{{rewind_segment_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "rewind-segments",
                "{{rewind_segment_id}}"
              ]
            },
            "description": "Renombra y/o SUSTITUYE los filtros (no se mezclan).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Postman · sin contestar (editado)\",\n  \"filters\": {\n    \"max_attempts\": 2\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: filtros sustituidos\", function () {",
                  "  pm.expect(j.rewind_segment.filters).to.eql({ max_attempts: 2 });",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/campaigns/:id/rewind/preview (segment_id)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{campaign_id}}/rewind/preview",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{campaign_id}}",
                "rewind",
                "preview"
              ]
            },
            "description": "El dry-run con `segment_id` en lugar de filtros sueltos: el servidor lo resuelve y lo evalúa AHORA.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"segment_id\": \"{{rewind_segment_id}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: matching numérico\", function () { pm.expect(j.matching).to.be.a(\"number\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/rewind-segments/:id/retire",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/rewind-segments/{{rewind_segment_id}}/retire",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "rewind-segments",
                "{{rewind_segment_id}}",
                "retire"
              ]
            },
            "description": "Retirada lógica, terminal: sale de la lista y ya no se puede usar ni editar."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: retirado\", function () { pm.expect(j.rewind_segment.retired_at).to.be.a(\"string\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/campaigns/:id/stop",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{campaign_id}}/stop",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{campaign_id}}",
                "stop"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: completed\", function () { pm.expect(j.campaign.state).to.eql(\"completed\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/campaigns/:id/archive",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{campaign_id}}/archive",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{campaign_id}}",
                "archive"
              ]
            },
            "description": "Archivo terminal."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: archived\", function () { pm.expect(j.campaign.state).to.eql(\"archived\"); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "05 · debts",
      "description": "Deuda + contactos + CDR por deuda + Litigation Defense Packet. Usa {{golden_debt_id}} del golden-flow. Cierra con la tarjeta del deudor (GET /v1/consumers/:id).",
      "item": [
        {
          "name": "POST /v1/debts/import — multipart CSV",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/debts/import",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debts",
                "import"
              ]
            },
            "description": "El fichero CSV entra por multipart sin tocar disco. mapping es target→header; ambas filas atraviesan el mismo import JSON: una se importa y la otra falla por índice sin eco del teléfono. La columna `name` va mapeada a debtor_name (core#1529): la fila valida lo guarda cifrado y la rechazada no lo devuelve.",
            "body": {
              "mode": "formdata",
              "formdata": [
                {
                  "key": "mapping",
                  "value": "{\"external_ref\":\"account\",\"debtor_name\":\"name\",\"consumer_ref\":\"consumer\",\"contact.phone_e164\":\"phone\",\"contact.line_type\":\"line_type\",\"contact.timezone\":\"timezone\",\"contact.is_primary\":\"primary\"}",
                  "type": "text"
                },
                {
                  "key": "separator",
                  "value": "comma",
                  "type": "text"
                },
                {
                  "key": "file",
                  "type": "file",
                  "src": "postman/fixtures/debts-import.csv",
                  "contentType": "text/csv"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"paridad por fila: una importada y una rechazada\", function () {",
                  "  pm.expect(j.imported).to.eql(1);",
                  "  pm.expect(j.failed).to.be.an(\"array\").with.lengthOf(1);",
                  "  pm.expect(j.failed[0].index).to.eql(1);",
                  "  pm.expect(j.failed[0].error).to.include(\"phone_e164\");",
                  "});",
                  "pm.test(\"PII rechazada no vuelve en la respuesta\", function () {",
                  "  pm.expect(pm.response.text()).to.not.include(\"not-a-phone\");",
                  "  pm.expect(pm.response.text()).to.not.include(\"+13125550177\");",
                  "});",
                  "pm.test(\"el nombre del deudor rechazado no vuelve en la respuesta (core#1529)\", function () {\n  pm.expect(pm.response.text()).to.not.include(\"Nombre Rechazado\");\n});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/debts",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/debts?limit=50",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debts"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "50"
                }
              ]
            },
            "description": "Listado + búsqueda de cuentas (core#847, MT-CTI-12 FASE 1), keyset-paginado sobre la vista `debts_wide` (limit por defecto 50, máx 200). Filtros opcionales: campaign_id, state, min_amount_cents, max_amount_cents, dialable_by, phone, consumer_ref, external_ref, cursor. Es superficie de BARRIDO: el teléfono viaja SIEMPRE enmascarado y account_number no está en la proyección."
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: debts[] + next_cursor\", function () {",
                  "  pm.expect(j.debts).to.be.an(\"array\");",
                  "  pm.expect(j).to.have.property(\"next_cursor\");",
                  "});",
                  "pm.test(\"el listado NO publica E.164 en claro ni account_number\", function () {",
                  "  (j.debts || []).forEach(function (d) {",
                  "    pm.expect(d).to.not.have.property(\"account_number\");",
                  "    pm.expect(d).to.not.have.property(\"debtor_name\");",
                  "    pm.expect(d).to.have.property(\"primary_phone_masked\");",
                  "    if (d.primary_phone_masked) { pm.expect(d.primary_phone_masked).to.match(/^\\*\\*\\*/); }",
                  "  });",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/debts/search — telefono por cuerpo",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/debts/search",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debts",
                "search"
              ]
            },
            "description": "El telefono se busca dentro del cuerpo para que no viaje en access logs. La respuesta conserva la proyeccion enmascarada de GET /v1/debts.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"phone\": \"{{golden_contact_phone}}\",\n  \"limit\": 5\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "const phone = pm.collectionVariables.get(\"golden_contact_phone\");",
                  "pm.test(\"shape: una deuda golden + next_cursor nulo\", function () {",
                  "  pm.expect(j.debts).to.be.an(\"array\").with.lengthOf(1);",
                  "  pm.expect(j.debts[0].id).to.eql(pm.collectionVariables.get(\"golden_debt_id\"));",
                  "  pm.expect(j.next_cursor).to.eql(null);",
                  "});",
                  "pm.test(\"proyeccion enmascarada, sin PII en claro\", function () {",
                  "  const d = j.debts[0];",
                  "  pm.expect(d.primary_phone_masked).to.eql(\"***\" + phone.slice(-4));",
                  "  pm.expect(d).to.not.have.property(\"account_number\");",
                  "  pm.expect(d).to.not.have.property(\"debtor_name\");",
                  "  pm.expect(pm.response.text()).to.not.include(phone);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/debts/search — telefono no E.164",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/debts/search",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debts",
                "search"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"phone\": \"3125550100\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "pm.test(\"shape: bad_request nombra E.164\", function () {",
                  "  const e = pm.response.json().error;",
                  "  pm.expect(e.code).to.eql(\"bad_request\");",
                  "  pm.expect(e.message).to.include(\"E.164\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/debts/search — sin bearer",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/debts/search",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debts",
                "search"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 401\", function () { pm.response.to.have.status(401); });",
                  "pm.test(\"shape: unauthorized sin pagina\", function () {",
                  "  const j = pm.response.json();",
                  "  pm.expect(j.error.code).to.eql(\"unauthorized\");",
                  "  pm.expect(j).to.not.have.property(\"debts\");",
                  "  pm.expect(j).to.not.have.property(\"next_cursor\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/debts?phone=banana — el telefono no viaja en la URL: 400 phone_not_in_query",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/debts?phone=banana",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debts"
              ],
              "query": [
                {
                  "key": "phone",
                  "value": "banana"
                }
              ]
            },
            "description": "core#1745: un teléfono en la query string llega a los access logs, así que el GET lo rechaza siempre (sea E.164 o basura) con 400 phone_not_in_query y apunta a POST /v1/debts/search, que lo recibe en el cuerpo."
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"400 phone_not_in_query\", function () {",
                  "  pm.expect(pm.response.code).to.eql(400);",
                  "  pm.expect(pm.response.json().error.code).to.eql(\"phone_not_in_query\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/debts/:id",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/debts/{{golden_debt_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debts",
                "{{golden_debt_id}}"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: debt.id + contacts[]\", function () {",
                  "  pm.expect(j.debt.id).to.eql(pm.collectionVariables.get(\"golden_debt_id\"));",
                  "  pm.expect(j.debt.contacts).to.be.an(\"array\");",
                  "});",
                  "pm.test(\"debtor_name: opcional, texto o null (core#1529)\", function () {\n  pm.expect(j.debt).to.have.property(\"debtor_name\");\n  if (j.debt.debtor_name !== null) { pm.expect(j.debt.debtor_name).to.match(/^Postman Deudor /); }\n});",
                  "pm.test(\"siembra golden_consumer_ref para GET /v1/consumers/:id\", function () {\n  pm.expect(j.debt.consumer_ref).to.be.a(\"string\").and.not.empty;\n  pm.collectionVariables.set(\"golden_consumer_ref\", j.debt.consumer_ref);\n});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/debts/:id/call_attempts",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/debts/{{golden_debt_id}}/call_attempts?limit=50",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debts",
                "{{golden_debt_id}}",
                "call_attempts"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "50"
                }
              ]
            },
            "description": "CDR durable por deuda, keyset-paginado (limit por defecto 100, máx 1000)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: call_attempts[] + next_cursor\", function () {",
                  "  pm.expect(j.call_attempts).to.be.an(\"array\");",
                  "  pm.expect(j).to.have.property(\"next_cursor\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/debts/:id/defense-packet/archives (archivado apagado -> 404)",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/debts/{{golden_debt_id}}/defense-packet/archives",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debts",
                "{{golden_debt_id}}",
                "defense-packet",
                "archives"
              ]
            },
            "description": "Con DEFENSE_ARCHIVE_ENABLED apagado, el contrato responde 404 como una ruta inexistente y no archiva datos."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/debts/:id/defense-packet/archives (archivado apagado -> 404)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/debts/{{golden_debt_id}}/defense-packet/archives",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debts",
                "{{golden_debt_id}}",
                "defense-packet",
                "archives"
              ]
            },
            "description": "Con DEFENSE_ARCHIVE_ENABLED apagado, el contrato responde 404 como una ruta inexistente."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/debts/:id/defense-packet/archives/:aid/attestation (archivado apagado -> 404)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/debts/{{golden_debt_id}}/defense-packet/archives/{{golden_archive_id}}/attestation",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debts",
                "{{golden_debt_id}}",
                "defense-packet",
                "archives",
                "{{golden_archive_id}}",
                "attestation"
              ]
            },
            "description": "Con DEFENSE_ARCHIVE_ENABLED apagado, el contrato responde 404 como una ruta inexistente."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/debts/:id/defense-packet",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/debts/{{golden_debt_id}}/defense-packet",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debts",
                "{{golden_debt_id}}",
                "defense-packet"
              ]
            },
            "description": "Bundle de evidencia sellado (sha256 en integrity.digest). Variante ZIP: ?format=zip."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: integrity.digest sha256\", function () {",
                  "  pm.expect(j.integrity).to.be.an(\"object\");",
                  "  pm.expect(j.integrity.digest).to.be.a(\"string\").and.to.have.length.above(16);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/consumers/:id",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/consumers/{{golden_consumer_ref}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "consumers",
                "{{golden_consumer_ref}}"
              ]
            },
            "description": "Tarjeta del deudor (core#1460): deudas, DNC y cease-and-desist, contador Reg F y proxima ventana, derivada de las fuentes append-only. La identidad es el consumer_ref de la deuda golden ({{golden_consumer_ref}}, sembrado en GET /v1/debts/:id). Sin numeros de telefono: DNC va por conteos y la deuda con telefono enmascarado. Cada deuda lleva `debtor_name` (opcional, cifrado en reposo, core#1529)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: consumer.id == consumer_ref de la deuda golden, identity consumer_ref\", function () {",
                  "  pm.expect(j.consumer.id).to.eql(pm.collectionVariables.get(\"golden_consumer_ref\"));",
                  "  pm.expect(j.consumer.identity).to.eql(\"consumer_ref\");",
                  "  pm.expect(j.consumer.generated_at).to.be.a(\"string\");",
                  "});",
                  "pm.test(\"debts incluye la deuda golden con reg_f y window\", function () {",
                  "  const d = j.consumer.debts.find(function (x) { return x.id === pm.collectionVariables.get(\"golden_debt_id\"); });",
                  "  pm.expect(d, \"deuda golden en la tarjeta\").to.be.an(\"object\");",
                  "  pm.expect(d.reg_f.cap).to.be.a(\"number\");",
                  "  pm.expect(d.reg_f.attempts_in_window).to.be.a(\"number\");",
                  "  pm.expect([\"permitted_now\", \"scheduled\", \"suppressed\", \"not_dialable\", \"undecidable\"]).to.include(d.window.status);",
                  "});",
                  "pm.test(\"next_window, DNC y C&D con su forma; los gates evaluados se nombran\", function () {",
                  "  pm.expect([\"permitted_now\", \"scheduled\", \"suppressed\", \"not_dialable\", \"undecidable\"]).to.include(j.consumer.next_window.status);",
                  "  pm.expect(j.consumer).to.have.property(\"negotiation_bounds\"); if (j.consumer.negotiation_bounds !== null) { pm.expect(j.consumer.negotiation_bounds).to.have.all.keys(\"max_settlement_pct\", \"min_payment_cents\", \"max_plan_months\"); }",
                  "  pm.expect(j.consumer.cease_and_desist.on_file).to.be.a(\"boolean\");",
                  "  pm.expect(j.consumer.dnc.lists).to.have.all.keys(\"federal\", \"state\", \"internal_tenant\", \"internal_global\");",
                  "  pm.expect(j.consumer.evaluated_gates).to.include(\"regf.7in7.v1\");",
                  "  pm.expect(j.consumer.not_evaluated_gates).to.be.an(\"array\").that.is.not.empty;",
                  "});",
                  "pm.test(\"sin PII: el telefono golden no viaja en claro\", function () {",
                  "  pm.expect(pm.response.text()).to.not.include(pm.collectionVariables.get(\"golden_contact_phone\"));",
                  "});",
                  "pm.test(\"la deuda de la tarjeta lleva debtor_name (texto o null, core#1529)\", function () {\n  const d = j.consumer.debts.find(function (x) { return x.id === pm.collectionVariables.get(\"golden_debt_id\"); });\n  pm.expect(d).to.have.property(\"debtor_name\");\n  if (d.debtor_name !== null) { pm.expect(d.debtor_name).to.match(/^Postman Deudor /); }\n});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/consumers/:id — consumer_ref sin deudas (404)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/consumers/{{ghost_consumer_ref}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "consumers",
                "{{ghost_consumer_ref}}"
              ]
            },
            "description": "consumer_ref inexistente via VARIABLE (nunca literal): 404 determinista del handler, no del catch-all. Un consumidor que solo existe en otro tenant da el mismo 404."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.variables.set(\"ghost_consumer_ref\", \"postman-ghost-consumer-\" + Date.now());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "const j = pm.response.json();",
                  "pm.test(\"404 del handler (not_found), no del catch-all\", function () {",
                  "  pm.expect(j.error.code).to.eql(\"not_found\");",
                  "  pm.expect(j.error.message).to.not.include(\"no such route\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/consumers/:id — consumer_ref de mas de 128 caracteres (400)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/consumers/{{long_consumer_ref}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "consumers",
                "{{long_consumer_ref}}"
              ]
            },
            "description": "El consumer_ref admite 1 a 128 caracteres, no en blanco y sin bytes NUL (el mismo tope de GET /v1/debts?consumer_ref=). 129 caracteres: 400 bad_request del handler, no 404 ni 500."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.variables.set(\"long_consumer_ref\", \"x\".repeat(129));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "const j = pm.response.json();",
                  "pm.test(\"400 bad_request del handler, no del catch-all\", function () {",
                  "  pm.expect(j.error.code).to.eql(\"bad_request\");",
                  "  pm.expect(j.error.message).to.include(\"128\");",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "06 · call-attempts",
      "description": "CDR filtrado/paginado + disposition tipada (corrección append-only sobre el original).",
      "item": [
        {
          "name": "GET /v1/call_attempts",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/call_attempts?limit=50",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "call_attempts"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "50"
                }
              ]
            },
            "description": "Captura un attempt ORIGINAL (corrects_id null) para la disposition si el stack ya marcó."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: call_attempts[] + next_cursor\", function () {",
                  "  pm.expect(j.call_attempts).to.be.an(\"array\");",
                  "  pm.expect(j).to.have.property(\"next_cursor\");",
                  "});",
                  "pm.test(\"cada fila lleva disclosure con un status del contrato (core#1587)\", function () {",
                  "  const states = [\"attested\", \"declared_not_delivered\", \"unattested\", \"not_applicable\", \"undecidable\"];",
                  "  (j.call_attempts || []).forEach(function (a) {",
                  "    pm.expect(a.disclosure).to.be.an(\"object\");",
                  "    pm.expect(states).to.include(a.disclosure.status);",
                  "    pm.expect(a.disclosure).to.have.all.keys(\"status\", \"event_id\", \"at\", \"revised\", \"script\", \"script_match\");",
                  "    pm.expect([\"matched\", \"other_version\", \"unscripted\", \"not_applicable\"]).to.include(a.disclosure.script_match);",
                  "  });",
                  "});",
                  "const orig = (j.call_attempts || []).find(a => !a.corrects_id);",
                  "if (orig) { pm.collectionVariables.set(\"attempt_id\", orig.id); }"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/reports/cdr",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/reports/cdr?limit=50",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "reports",
                "cdr"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "50"
                }
              ]
            },
            "description": "Los mismos call_attempts que /v1/call_attempts (mismos filtros y cursor) como protojson canónico de dialer.v1.CallDetailRecord: camelCase, enums por nombre, defaults omitidos."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: callDetailRecords[] (camelCase, sin snake_case)\", function () {",
                  "  pm.expect(j.callDetailRecords).to.be.an(\"array\");",
                  "  (j.callDetailRecords || []).forEach(function (r) {",
                  "    pm.expect(r).to.have.property(\"callId\");",
                  "    pm.expect(r).to.have.property(\"tenantId\");",
                  "    pm.expect(Object.keys(r).some(function (k) { return k.indexOf(\"_\") >= 0; })).to.eql(false);",
                  "  });",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/call_attempts/:id/disposition",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/call_attempts/{{attempt_id}}/disposition",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "call_attempts",
                "{{attempt_id}}",
                "disposition"
              ]
            },
            "description": "Outcome tipado anclado al attempt original. 201 con CDR real; 404 si la corrida aún no tiene attempts ({{attempt_id}} vacío).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"disposition\": \"wrong_number\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201 (con CDR) o 404 (sin attempts aún)\", function () {",
                  "  pm.expect([201, 404]).to.include(pm.response.code);",
                  "});",
                  "if (pm.response.code === 201) {",
                  "  const j = pm.response.json();",
                  "  pm.test(\"shape 201: call_attempt corrección + promise null\", function () {",
                  "    pm.expect(j.call_attempt).to.be.an(\"object\");",
                  "    pm.expect(j.call_attempt.corrects_id).to.not.be.null;",
                  "  });",
                  "  pm.collectionVariables.set(\"disposition_correction_id\", j.call_attempt.id);",
                  "  pm.test(\"cierre sin disclosure: la respuesta no inventa el campo\", function () {",
                  "    pm.expect(j).to.not.have.property(\"disclosure\");",
                  "  });",
                  "}",
                  "if (pm.response.code === 404) {",
                  "  pm.test(\"404 del endpoint real (no catch-all 'no such route')\", function () {",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}"
                ]
              }
            },
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// attempt_id queda vacío cuando la corrida aún no generó call attempts (CDR vacío).",
                  "// Con la variable vacía la URL tendría un segmento vacío y caería en el",
                  "// catch-all del router ('no such route') SIN ejercitar la ruta real:",
                  "// sembramos un UUID aleatorio para que el 404 sea el del endpoint.",
                  "if (!pm.collectionVariables.get(\"attempt_id\")) {",
                  "  pm.collectionVariables.set(\"attempt_id\", pm.variables.replaceIn(\"{{$guid}}\"));",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/call_attempts/:id/disclosure — sin outcome (400)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/call_attempts/{{attempt_id}}/disclosure",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "call_attempts",
                "{{attempt_id}}",
                "disclosure"
              ]
            },
            "description": "Nunca se asume `outcome`: sin él, 400 `invalid`, no un «delivered» por defecto. Con un attempt real el 400 es seguro; si la corrida aún no tiene attempts ({{attempt_id}} sembrado al azar) el handler contesta 404 antes de mirar el cuerpo.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{golden_agent_id}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400 (o 404 si la corrida aún no tiene attempts)\", function () {",
                  "  pm.expect([400, 404]).to.include(pm.response.code);",
                  "});",
                  "if (pm.response.code === 400) {",
                  "  pm.test(\"400 invalid del changeset: sin valor por defecto de outcome\", function () {",
                  "    pm.expect(pm.response.json().error.code).to.eql(\"invalid\");",
                  "  });",
                  "}",
                  "if (pm.response.code === 404) {",
                  "  pm.test(\"404 del endpoint real (no catch-all 'no such route')\", function () {",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/call_attempts/:id/disposition — disclosure anidado inválido (400)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/call_attempts/{{attempt_id}}/disposition",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "call_attempts",
                "{{attempt_id}}",
                "disposition"
              ]
            },
            "description": "El `disclosure` anidado sigue las reglas del endpoint propio: `not_delivered` sin `reason` es 400 y NO se escribe ni la corrección ni la atestación. 404 si la corrida aún no tiene attempts ({{attempt_id}} sembrado al azar).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{golden_agent_id}}\",\n  \"disposition\": \"wrong_number\",\n  \"disclosure\": {\"outcome\": \"not_delivered\"}\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.collectionVariables.get(\"attempt_id\")) {",
                  "  pm.collectionVariables.set(\"attempt_id\", pm.variables.replaceIn(\"{{$guid}}\"));",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400 (o 404 sin attempts aún)\", function () {",
                  "  pm.expect([400, 404]).to.include(pm.response.code);",
                  "});",
                  "if (pm.response.code === 400) {",
                  "  pm.test(\"400 invalid del changeset: not_delivered sin reason\", function () {",
                  "    pm.expect(pm.response.json().error.code).to.eql(\"invalid\");",
                  "  });",
                  "}",
                  "pm.test(\"la respuesta de error no trae call_attempt ni disclosure\", function () {",
                  "  const j = pm.response.json();",
                  "  pm.expect(j).to.not.have.property(\"call_attempt\");",
                  "  pm.expect(j).to.not.have.property(\"disclosure\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/call_attempts/:id/disclosure",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/call_attempts/{{attempt_id}}/disclosure",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "call_attempts",
                "{{attempt_id}}",
                "disclosure"
              ]
            },
            "description": "Atestación del aviso de cobranza (core#1587, ADR #211): lo que el agente DECLARA, no una prueba de que se dijo. 201 con el evento sellado, o 404 si la corrida aún no tiene attempts ({{attempt_id}} sembrado al azar), o 409 `already_attested` si la corrida se repite sobre el mismo attempt (la segunda «primera» atestación se rectifica con supersedes_id). Una llave dd_ nombra al agente en el cuerpo (`agent_id`); nunca se asume `outcome`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{golden_agent_id}}\",\n  \"outcome\": \"delivered\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// attempt_id queda vacío cuando la corrida aún no generó call attempts (CDR vacío):",
                  "// con la variable vacía la URL caería en el catch-all del router SIN ejercitar la ruta real.",
                  "if (!pm.collectionVariables.get(\"attempt_id\")) {",
                  "  pm.collectionVariables.set(\"attempt_id\", pm.variables.replaceIn(\"{{$guid}}\"));",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201, 404 (sin attempts aún) o 409 (ya atestado)\", function () {",
                  "  pm.expect([201, 404, 409]).to.include(pm.response.code);",
                  "});",
                  "if (pm.response.code === 201) {",
                  "  const d = pm.response.json().disclosure;",
                  "  pm.collectionVariables.set(\"disclosure_event_id\", d.id);",
                  "  pm.test(\"shape 201: disclosure con outcome delivered, el attempt y el agente\", function () {",
                  "    pm.expect(d).to.be.an(\"object\");",
                  "    pm.expect(d.outcome).to.eql(\"delivered\");",
                  "    pm.expect(d.call_attempt_id).to.eql(pm.collectionVariables.get(\"attempt_id\"));",
                  "    pm.expect(d.agent_id).to.eql(pm.collectionVariables.get(\"golden_agent_id\"));",
                  "  });",
                  "}",
                  "if (pm.response.code === 404) {",
                  "  pm.test(\"404 del endpoint real (no catch-all 'no such route')\", function () {",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}",
                  "if (pm.response.code === 409) {",
                  "  pm.test(\"409 already_attested\", function () {",
                  "    pm.expect(pm.response.json().error.code).to.eql(\"already_attested\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/call_attempts/:id/disclosure — rectificación not_delivered",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/call_attempts/{{attempt_id}}/disclosure",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "call_attempts",
                "{{attempt_id}}",
                "disclosure"
              ]
            },
            "description": "Rectifica la declaración anterior con otra fila append-only: not_delivered exige motivo y supersedes_id. En una corrida con attempt real y la creación anterior aceptada, esta request debe crear el segundo evento con 201.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{golden_agent_id}}\",\n  \"outcome\": \"not_delivered\",\n  \"reason\": \"consumer_hung_up\",\n  \"supersedes_id\": \"{{disclosure_event_id}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.collectionVariables.get(\"disclosure_event_id\")) {",
                  "  pm.collectionVariables.set(\"disclosure_event_id\", pm.variables.replaceIn(\"{{$guid}}\"));",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201 en el camino sembrado; 404/409/422 sin primera atestación utilizable\", function () {",
                  "  pm.expect([201, 404, 409, 422]).to.include(pm.response.code);",
                  "});",
                  "if (pm.response.code === 201) {",
                  "  const d = pm.response.json().disclosure;",
                  "  pm.test(\"rectificación append-only enlazada\", function () {",
                  "    pm.expect(d.outcome).to.eql(\"not_delivered\");",
                  "    pm.expect(d.reason).to.eql(\"consumer_hung_up\");",
                  "    pm.expect(d.supersedes_id).to.eql(pm.collectionVariables.get(\"disclosure_event_id\"));",
                  "    pm.expect(d.id).to.not.eql(d.supersedes_id);",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/call_attempts/:id/disclosure — campo desconocido (400)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/call_attempts/{{attempt_id}}/disclosure",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "call_attempts",
                "{{attempt_id}}",
                "disclosure"
              ]
            },
            "description": "El cuerpo es estricto: un campo que el endpoint no declara es 400 `bad_request` que lo nombra, antes de mirar el attempt, así que el 400 sale con cualquier {{attempt_id}}.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{golden_agent_id}}\",\n  \"outcome\": \"delivered\",\n  \"bogus_field\": 1\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// attempt_id queda vacío cuando la corrida aún no generó call attempts (CDR vacío):",
                  "// con la variable vacía la URL caería en el catch-all del router SIN ejercitar la ruta real.",
                  "if (!pm.collectionVariables.get(\"attempt_id\")) {",
                  "  pm.collectionVariables.set(\"attempt_id\", pm.variables.replaceIn(\"{{$guid}}\"));",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "const j = pm.response.json();",
                  "pm.test(\"400 bad_request del handler que nombra el campo\", function () {",
                  "  pm.expect(j.error.code).to.eql(\"bad_request\");",
                  "  pm.expect(j.error.message).to.include(\"bogus_field\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/call_attempts/:id/disclosure — supersedes_id no UUID (400)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/call_attempts/{{attempt_id}}/disclosure",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "call_attempts",
                "{{attempt_id}}",
                "disclosure"
              ]
            },
            "description": "`supersedes_id` debe ser un UUID: 400 `bad_request` del handler, antes de mirar el attempt.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{golden_agent_id}}\",\n  \"outcome\": \"not_delivered\",\n  \"reason\": \"consumer_hung_up\",\n  \"supersedes_id\": \"no-es-un-uuid\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// attempt_id queda vacío cuando la corrida aún no generó call attempts (CDR vacío):",
                  "// con la variable vacía la URL caería en el catch-all del router SIN ejercitar la ruta real.",
                  "if (!pm.collectionVariables.get(\"attempt_id\")) {",
                  "  pm.collectionVariables.set(\"attempt_id\", pm.variables.replaceIn(\"{{$guid}}\"));",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "const j = pm.response.json();",
                  "pm.test(\"400 bad_request: supersedes_id must be a UUID\", function () {",
                  "  pm.expect(j.error.code).to.eql(\"bad_request\");",
                  "  pm.expect(j.error.message).to.include(\"supersedes_id\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/call_attempts/:id/disclosure — attempt inexistente (404)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/call_attempts/{{disclosure_missing_attempt_id}}/disclosure",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "call_attempts",
                "{{disclosure_missing_attempt_id}}",
                "disclosure"
              ]
            },
            "description": "Un attempt que no existe para el tenant es 404 `not_found` del handler, nunca una atestación huérfana.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{golden_agent_id}}\",\n  \"outcome\": \"delivered\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set(\"disclosure_missing_attempt_id\", pm.variables.replaceIn(\"{{$guid}}\"));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "const j = pm.response.json();",
                  "pm.test(\"404 del handler (not_found), no del catch-all\", function () {",
                  "  pm.expect(j.error.code).to.eql(\"not_found\");",
                  "  pm.expect(j.error.message).to.not.include(\"no such route\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/call_attempts/:id/disclosure — llamada ajena (404)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/call_attempts/{{disclosure_foreign_attempt_id}}/disclosure",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "call_attempts",
                "{{disclosure_foreign_attempt_id}}",
                "disclosure"
              ]
            },
            "description": "Un attempt de OTRO tenant no se distingue de uno inexistente: 404 `not_found` del handler, sin filtrar su existencia y sin escribir nada. El caso con un segundo tenant real (sus filas ni se listan ni se atestan) lo prueba tests/e2e_agent/test_disclosure_attestation_spa.py.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{golden_agent_id}}\",\n  \"outcome\": \"delivered\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set(\"disclosure_foreign_attempt_id\", pm.variables.replaceIn(\"{{$guid}}\"));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "const j = pm.response.json();",
                  "pm.test(\"404 del handler (not_found), no del catch-all\", function () {",
                  "  pm.expect(j.error.code).to.eql(\"not_found\");",
                  "  pm.expect(j.error.message).to.not.include(\"no such route\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/call_attempts/:id/disclosure — sobre una corrección (404)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/call_attempts/{{disposition_correction_id}}/disclosure",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "call_attempts",
                "{{disposition_correction_id}}",
                "disclosure"
              ]
            },
            "description": "`{id}` debe ser un attempt ORIGINAL: la corrección que creó la disposition de arriba (`corrects_id` no nulo) es 404 `not_found` y no escribe nada. Sin corrección sembrada (la corrida no tuvo attempts) se siembra un UUID al azar y el 404 es el mismo.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{golden_agent_id}}\",\n  \"outcome\": \"delivered\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.collectionVariables.get(\"disposition_correction_id\")) {",
                  "  pm.collectionVariables.set(\"disposition_correction_id\", pm.variables.replaceIn(\"{{$guid}}\"));",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "pm.test(\"404 del handler (not_found), no del catch-all\", function () {",
                  "  const j = pm.response.json();",
                  "  pm.expect(j.error.code).to.eql(\"not_found\");",
                  "  pm.expect(j.error.message).to.not.include(\"no such route\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/call_attempts/:id/analysis (llave dd_ -> 403)",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/call_attempts/{{ghost_uuid}}/analysis",
            "description": "Analisis post-llamada de un intento (core#1972). Lo leen el agente del intento y un supervisor o admin; una llave dd_ es un servicio y es 403 role_forbidden antes de buscar el intento."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"error.code role_forbidden\", function () { pm.expect(pm.response.json().error.code).to.eql(\"role_forbidden\"); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "07 · callbacks",
      "description": "Citas de re-marcado acordadas. Usa la deuda/el teléfono del golden-flow (teléfono desconocido → 422).",
      "item": [
        {
          "name": "POST /v1/callbacks (para complete)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/callbacks",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "callbacks"
              ]
            },
            "description": "Redial acordado con el consumidor (regla de aviso mínimo: 10 min; agendamos +30 min).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"debt_id\": \"{{golden_debt_id}}\",\n  \"contact_phone\": \"{{golden_contact_phone}}\",\n  \"scheduled_at\": \"{{callback_when}}\",\n  \"note\": \"postman: consumer pidió el martes\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set(\"callback_when\", new Date(Date.now() + 30 * 60 * 1000).toISOString());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: callback pending\", function () {",
                  "  pm.expect(j.callback.id).to.be.a(\"string\");",
                  "  pm.expect(j.callback.status).to.eql(\"pending\");",
                  "});",
                  "pm.collectionVariables.set(\"callback_id_complete\", j.callback.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/callbacks",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/callbacks",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "callbacks"
              ]
            },
            "description": "Vista cola (status pending por defecto)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: callbacks[] con el recién creado\", function () {",
                  "  pm.expect(j.callbacks).to.be.an(\"array\").and.to.have.length.above(0);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/callbacks?due=now",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/callbacks?due=now",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "callbacks"
              ],
              "query": [
                {
                  "key": "due",
                  "value": "now"
                }
              ]
            },
            "description": "Feed de vencidos para el runner (pending Y scheduled_at <= now): el de +30 min NO debe salir."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: callbacks[] (feed due)\", function () { pm.expect(j.callbacks).to.be.an(\"array\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/callbacks/:id/complete",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/callbacks/{{callback_id_complete}}/complete",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "callbacks",
                "{{callback_id_complete}}",
                "complete"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: done\", function () { pm.expect(j.callback.status).to.eql(\"done\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/callbacks (para cancel)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/callbacks",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "callbacks"
              ]
            },
            "description": "Redial acordado con el consumidor (regla de aviso mínimo: 10 min; agendamos +45 min).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"debt_id\": \"{{golden_debt_id}}\",\n  \"contact_phone\": \"{{golden_contact_phone}}\",\n  \"scheduled_at\": \"{{callback_when}}\",\n  \"note\": \"postman: se cancelará\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set(\"callback_when\", new Date(Date.now() + 45 * 60 * 1000).toISOString());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: callback pending\", function () {",
                  "  pm.expect(j.callback.id).to.be.a(\"string\");",
                  "  pm.expect(j.callback.status).to.eql(\"pending\");",
                  "});",
                  "pm.collectionVariables.set(\"callback_id_cancel\", j.callback.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/callbacks/:id/cancel",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/callbacks/{{callback_id_cancel}}/cancel",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "callbacks",
                "{{callback_id_cancel}}",
                "cancel"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: cancelled\", function () { pm.expect(j.callback.status).to.eql(\"cancelled\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/callbacks (para miss)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/callbacks",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "callbacks"
              ]
            },
            "description": "Redial acordado con el consumidor (regla de aviso mínimo: 10 min; agendamos +60 min).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"debt_id\": \"{{golden_debt_id}}\",\n  \"contact_phone\": \"{{golden_contact_phone}}\",\n  \"scheduled_at\": \"{{callback_when}}\",\n  \"note\": \"postman: se marcará missed\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set(\"callback_when\", new Date(Date.now() + 60 * 60 * 1000).toISOString());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: callback pending\", function () {",
                  "  pm.expect(j.callback.id).to.be.a(\"string\");",
                  "  pm.expect(j.callback.status).to.eql(\"pending\");",
                  "});",
                  "pm.collectionVariables.set(\"callback_id_miss\", j.callback.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/callbacks/:id/miss",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/callbacks/{{callback_id_miss}}/miss",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "callbacks",
                "{{callback_id_miss}}",
                "miss"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: missed\", function () { pm.expect(j.callback.status).to.eql(\"missed\"); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "08 · promises",
      "description": "Pipeline PTP. Las promesas nacen vía POST /call_attempts/:id/disposition (promise_to_pay), no por POST directo.",
      "item": [
        {
          "name": "GET /v1/promises",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/promises?status=pending&limit=5",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "promises"
              ],
              "query": [
                {
                  "key": "status",
                  "value": "pending"
                },
                {
                  "key": "limit",
                  "value": "5"
                }
              ]
            },
            "description": "Captura una promesa pending si el pipeline tiene (p. ej. tras demo-check)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: promises[]\", function () { pm.expect(j.promises).to.be.an(\"array\"); });",
                  "if (j.promises.length > 0) { pm.collectionVariables.set(\"promise_id\", j.promises[0].id); }"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/promises/:id",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/promises/{{promise_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "promises",
                "{{promise_id}}"
              ]
            },
            "description": "Transición pending → kept. 404 (real del endpoint, id aleatorio sembrado en pre-request) si esta corrida no tiene promesas pending.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"status\": \"kept\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200 (había pending) o 404 (pipeline vacío)\", function () {",
                  "  pm.expect([200, 404]).to.include(pm.response.code);",
                  "});",
                  "if (pm.response.code === 200) {",
                  "  const j = pm.response.json();",
                  "  pm.test(\"shape 200: promise kept\", function () { pm.expect(j.promise.status).to.eql(\"kept\"); });",
                  "}",
                  "if (pm.response.code === 404) {",
                  "  pm.test(\"404 del endpoint real (no catch-all 'no such route')\", function () {",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}"
                ]
              }
            },
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// promise_id queda vacío cuando el pipeline no tiene promesas pending en esta corrida.",
                  "// Con la variable vacía la URL tendría un segmento vacío y caería en el",
                  "// catch-all del router ('no such route') SIN ejercitar la ruta real:",
                  "// sembramos un UUID aleatorio para que el 404 sea el del endpoint.",
                  "if (!pm.collectionVariables.get(\"promise_id\")) {",
                  "  pm.collectionVariables.set(\"promise_id\", pm.variables.replaceIn(\"{{$guid}}\"));",
                  "}"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "09 · agents",
      "description": "Roster + presencia + checkin/checkout de un seat propio (extensión única por corrida).",
      "item": [
        {
          "name": "GET /v1/agents",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/agents",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: agents[]\", function () { pm.expect(j.agents).to.be.an(\"array\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/agents",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/agents",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents"
              ]
            },
            "description": "Alta de seat (name + email requeridos; sip_extension única por tenant → se genera por corrida).\n\nTope del tier (core#1381, ADR #183 de core): si el tenant ya tiene los `max_seats` asientos activos de su tier vivo, la respuesta es `422` con `error.code = seat_limit_reached` y `max_seats`, `tier` y `active_seats` en `error`; no se crea fila ni se factura nada por encima. `enterprise` no tiene tope y los asientos `inactive` no cuentan. Un tier que el catálogo no resuelve contesta `422 unpriceable_tier`. El tenant sembrado del harness va en `dialer_core`, cuyo `max_seats` es NULL, así que esta carpeta nunca lo alcanza: el 422 queda documentado en el ejemplo guardado, no se ejecuta aquí.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Postman Seat\",\n  \"email\": \"postman+{{$timestamp}}@example.test\",\n  \"role\": \"agent\",\n  \"sip_extension\": \"{{postman_agent_ext}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const ext = \"9\" + String(Date.now()).slice(-6);",
                  "pm.collectionVariables.set(\"postman_agent_ext\", ext);"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: agent.id + extensión generada\", function () {",
                  "  pm.expect(j.agent.id).to.be.a(\"string\");",
                  "  pm.expect(j.agent.sip_extension).to.eql(pm.collectionVariables.get(\"postman_agent_ext\"));",
                  "});",
                  "pm.collectionVariables.set(\"postman_agent_id\", j.agent.id);"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "422 seat_limit_reached (tope del tier alcanzado)",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/v1/agents",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "agents"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"name\": \"Postman Seat\",\n  \"email\": \"postman+1751600000@example.test\",\n  \"role\": \"agent\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"error\": {\n    \"code\": \"seat_limit_reached\",\n    \"message\": \"tier starter allows 5 active seats and the tenant already has them\",\n    \"max_seats\": 5,\n    \"tier\": \"starter\",\n    \"active_seats\": 5\n  }\n}"
            }
          ]
        },
        {
          "name": "GET /v1/agents/:id",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/agents/{{postman_agent_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents",
                "{{postman_agent_id}}"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: agent.id coincide\", function () {",
                  "  pm.expect(j.agent.id).to.eql(pm.collectionVariables.get(\"postman_agent_id\"));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/agents/checkin",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/agents/checkin",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents",
                "checkin"
              ]
            },
            "description": "Seat online. Con SBC configurado además provisiona cred SIP efímera (jamás se imprime).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"sip_extension\": \"{{postman_agent_ext}}\",\n  \"device_mode\": \"external\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: agent online\", function () { pm.expect(j.agent).to.be.an(\"object\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/agents/:id/pause",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/agents/{{postman_agent_id}}/pause",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents",
                "{{postman_agent_id}}",
                "pause"
              ]
            },
            "description": "Saca el asiento de la rotación con un motivo del conjunto cerrado (MT-ROAD-R11 FASE 2a, macro-tasks#524). Tenant scope; un ddw_ es 403 por la allowlist.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"reason\": \"lunch\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: agent.presence == paused (leída del asiento)\", function () {",
                  "  pm.expect(j.agent.id).to.eql(pm.collectionVariables.get(\"postman_agent_id\"));",
                  "  pm.expect(j.agent.presence).to.eql(\"paused\");",
                  "  pm.expect(j.agent.pause_reason).to.eql(\"lunch\");",
                  "  pm.expect(j.agent).to.have.property(\"state_since\");",
                  "  pm.expect(j.agent.pause_armed).to.eql(false);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/agents/:id/pause (motivo fuera del conjunto -> 400)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/agents/{{postman_agent_id}}/pause",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents",
                "{{postman_agent_id}}",
                "pause"
              ]
            },
            "description": "Fail-closed: un motivo fuera de {manual, break, lunch, training, meeting} es 400 y el asiento no se toca.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"reason\": \"coffee\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "pm.test(\"shape: error nombra el conjunto\", function () {",
                  "  pm.expect(pm.response.json().error.message).to.include(\"reason must be one of\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/agents/:id/resume",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/agents/{{postman_agent_id}}/resume",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents",
                "{{postman_agent_id}}",
                "resume"
              ]
            },
            "description": "Devuelve el asiento en pausa a la rotación, al final de la cola de reserva."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: agent.presence == available\", function () {",
                  "  pm.expect(j.agent.presence).to.eql(\"available\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/agents/:id/wrap-up (sin ACW en curso -> 409)",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/agents/{{postman_agent_id}}/wrap-up",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents",
                "{{postman_agent_id}}",
                "wrap-up"
              ]
            },
            "description": "Cierra el trabajo posterior a la llamada. El asiento del golden-flow está available y el tope del tenant es el default (0: sin ACW), así que no hay ACW que cerrar: 409 fail-closed. El camino 200 exige una llamada contestada y colgada con tope > 0, que newman no fabrica; lo cubre test/db/agent_wrap_up_api_test.exs de core."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 409\", function () { pm.response.to.have.status(409); });",
                  "pm.test(\"shape: conflict, no catch-all\", function () {",
                  "  const j = pm.response.json();",
                  "  pm.expect(j.error.code).to.eql(\"conflict\");",
                  "  pm.expect(j.error.message).to.include(\"after-call work\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/agents/:id/status paused (fachada de pause)",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/agents/{{postman_agent_id}}/status",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents",
                "{{postman_agent_id}}",
                "status"
              ]
            },
            "description": "Fachada de la FSM de asiento (core#1459): status=paused se traduce al verbo `pause` de AgentFSM, no escribe estado por su cuenta. Mismo resultado que POST /v1/agents/:id/pause. Tenant scope; un ddw_ es 403 por la allowlist.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"status\": \"paused\",\n  \"reason\": \"lunch\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: agent.presence == paused (leída del asiento)\", function () {",
                  "  pm.expect(j.agent.id).to.eql(pm.collectionVariables.get(\"postman_agent_id\"));",
                  "  pm.expect(j.agent.presence).to.eql(\"paused\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/agents/:id/status paused otra vez (transición no admitida -> 409)",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/agents/{{postman_agent_id}}/status",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents",
                "{{postman_agent_id}}",
                "status"
              ]
            },
            "description": "El asiento ya está paused: la FSM niega pause sobre paused y la fachada contesta 409 con el estado actual en `error.current_state`. Fail-closed: no hay escritura directa de estado.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"status\": \"paused\",\n  \"reason\": \"lunch\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 409\", function () { pm.response.to.have.status(409); });",
                  "pm.test(\"shape: conflict con current_state, no catch-all\", function () {",
                  "  const j = pm.response.json();",
                  "  pm.expect(j.error.code).to.eql(\"conflict\");",
                  "  pm.expect(j.error.current_state).to.eql(\"paused\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/agents/:id/status (status fuera del conjunto -> 400)",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/agents/{{postman_agent_id}}/status",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents",
                "{{postman_agent_id}}",
                "status"
              ]
            },
            "description": "status debe ser available, paused u offline; cualquier otro valor es 400 y no toca la FSM.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"status\": \"busy\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "pm.test(\"shape: bad_request, no catch-all\", function () {",
                  "  const j = pm.response.json();",
                  "  pm.expect(j.error.code).to.eql(\"bad_request\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/agents/:id (id inexistente -> 404)",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/agents/{{ghost_uuid}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents",
                "{{ghost_uuid}}"
              ]
            },
            "description": "Edita o libera un asiento (core#2527). Un id que no es de este tenant es 404 not_found, y NO el 404 del enrutador (la ruta existe).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"x\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "pm.test(\"es el error del recurso, no del enrutador\", function () { const j = pm.response.json(); pm.expect(j.error.code).to.eql(\"not_found\"); pm.expect(j.error.message).to.not.include(\"no such route\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/agents/:id (clave desconocida -> 400)",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/agents/{{postman_agent_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents",
                "{{postman_agent_id}}"
              ]
            },
            "description": "El cuerpo es cerrado: una clave desconocida es 400 que la nombra y no escribe nada (core#2527).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"nombre_mal\": \"x\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "pm.test(\"es el error del recurso, no del enrutador\", function () { const j = pm.response.json(); pm.expect(j.error.code).to.eql(\"bad_request\"); pm.expect(j.error.message).to.not.include(\"no such route\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/agents/:id/status available (fachada de unpause)",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/agents/{{postman_agent_id}}/status",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents",
                "{{postman_agent_id}}",
                "status"
              ]
            },
            "description": "status=available sobre un asiento paused se traduce a `unpause`; deja el asiento available para el checkout que sigue en la carpeta.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"status\": \"available\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: agent.presence == available (leída del asiento)\", function () {",
                  "  pm.expect(j.agent.id).to.eql(pm.collectionVariables.get(\"postman_agent_id\"));",
                  "  pm.expect(j.agent.presence).to.eql(\"available\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/agent/queue (clave de tenant sin asiento -> 403)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/agent/queue",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agent",
                "queue"
              ]
            },
            "description": "La cola es del asiento del token: una clave dd_ de tenant no tiene asiento, así que 403 fail-closed y sin parámetro que nombre a otro agente. El camino 200 exige un token ddu_ con asiento, que newman no fabrica; lo cubre test/db/agent_status_queue_api_test.exs de core."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"shape: forbidden, no catch-all\", function () {",
                  "  const j = pm.response.json();",
                  "  pm.expect(j.error.code).to.eql(\"forbidden\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/agent/preview/next (clave de tenant sin asiento -> 403)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/agent/preview/next",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agent",
                "preview",
                "next"
              ]
            },
            "description": "Ofrece y reserva la siguiente cuenta de una campaña de vista previa al asiento del token. El asiento es el del token: una clave dd_ de tenant no tiene asiento, así que 403 fail-closed y sin campo que nombre a otro agente. El camino 200 exige un token ddu_ con asiento, que newman no fabrica; lo cubre test/db/agent_preview_verbs_test.exs de core (core#2814, ADR #347).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"campaign_id\": \"{{campaign_id}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"shape: forbidden, no catch-all\", function () {",
                  "  const j = pm.response.json();",
                  "  pm.expect(j.error.code).to.eql(\"forbidden\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/agent/preview/dial (clave de tenant sin asiento -> 403)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/agent/preview/dial",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agent",
                "preview",
                "dial"
              ]
            },
            "description": "Marca la cuenta ofrecida (offer_id, vista previa) o elegida (debt_id, manual) por el mismo originate con todos los gates. El asiento es el del token: una clave dd_ de tenant no tiene asiento, así que 403 fail-closed y sin campo que nombre a otro agente. El camino 200 exige un token ddu_ con asiento, que newman no fabrica; lo cubre test/db/agent_preview_verbs_test.exs de core (core#2814, ADR #347).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"campaign_id\": \"{{campaign_id}}\",\n  \"debt_id\": \"{{golden_debt_id}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"shape: forbidden, no catch-all\", function () {",
                  "  const j = pm.response.json();",
                  "  pm.expect(j.error.code).to.eql(\"forbidden\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/agent/preview/skip (clave de tenant sin asiento -> 403)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/agent/preview/skip",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agent",
                "preview",
                "skip"
              ]
            },
            "description": "Salta la cuenta ofrecida; deja un hecho auditable. El asiento es el del token: una clave dd_ de tenant no tiene asiento, así que 403 fail-closed y sin campo que nombre a otro agente. El camino 200 exige un token ddu_ con asiento, que newman no fabrica; lo cubre test/db/agent_preview_verbs_test.exs de core (core#2814, ADR #347).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"campaign_id\": \"{{campaign_id}}\",\n  \"offer_id\": \"{{golden_debt_id}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"shape: forbidden, no catch-all\", function () {",
                  "  const j = pm.response.json();",
                  "  pm.expect(j.error.code).to.eql(\"forbidden\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/agents/checkout",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/agents/checkout",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents",
                "checkout"
              ]
            },
            "description": "Seat offline (revoca la cred SIP en el SBC).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"sip_extension\": \"{{postman_agent_ext}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: agent presente\", function () { pm.expect(j.agent).to.be.an(\"object\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/agents/occupancy?from&to",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/agents/occupancy?from={{occupancy_from}}&to={{occupancy_to}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents",
                "occupancy"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "{{occupancy_from}}"
                },
                {
                  "key": "to",
                  "value": "{{occupancy_to}}"
                }
              ]
            },
            "description": "Tiempo DERIVADO por estado y asiento en una ventana explicita `entered_at in [from, to)`: `Dialer.Agents.Occupancy.by_agent/4` tal cual, sin join con el roster (un asiento sin evidencia en la ventana esta AUSENTE, nunca aparece como cero fabricado) y con la ratio `occupancy` que calcula core bajo una definicion nombrada (`v1`: `on_call + wrap_up` sobre el tiempo contado sin `paused`, core#1077); el cliente no divide. El journal de intervalos (`agent_session_journal_enabled`) nace apagado, asi que el asiento de esta carpeta puede no aparecer: lo determinista es la forma y los invariantes de cada entrada que si aparezca."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Ventana RELATIVA al reloj (nunca un literal de calendario): la ultima hora",
                  "// hasta un minuto en el futuro, para que `entered_at` de esta corrida caiga",
                  "// dentro de [from, to) si el journal de asiento esta encendido.",
                  "const to = new Date(Date.now() + 60 * 1000);",
                  "const from = new Date(to.getTime() - 60 * 60 * 1000);",
                  "pm.collectionVariables.set(\"occupancy_from\", from.toISOString());",
                  "pm.collectionVariables.set(\"occupancy_to\", to.toISOString());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: ventana devuelta tal cual se pidio\", function () {",
                  "  pm.expect(Date.parse(j.window.from)).to.eql(Date.parse(pm.collectionVariables.get(\"occupancy_from\")));",
                  "  pm.expect(Date.parse(j.window.to)).to.eql(Date.parse(pm.collectionVariables.get(\"occupancy_to\")));",
                  "});",
                  "pm.test(\"shape: agents es un mapa por UUID con la derivacion medida y la ratio v1 de core\", function () {",
                  "  pm.expect(j.agents).to.be.an(\"object\");",
                  "  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;",
                  "  Object.keys(j.agents).forEach(function (id) {",
                  "    pm.expect(id).to.match(UUID);",
                  "    const r = j.agents[id];",
                  "    pm.expect(r).to.have.all.keys(\"by_state\", \"intervals\", \"occupied_ms\", \"counted_ms\", \"excluded\", \"overlapping_intervals\", \"occupancy\");",
                  "    pm.expect(r).to.not.have.property(\"occupancy_percent\");",
                  "    const o = r.occupancy;",
                  "    pm.expect(o).to.have.all.keys(\"definition\", \"numerator_ms\", \"denominator_ms\", \"percent\", \"reason\", \"complete\");",
                  "    pm.expect(o.definition).to.eql(\"v1\");",
                  "    const s = function (k) { return r.by_state[k] || 0; };",
                  "    pm.expect(o.numerator_ms).to.eql(s(\"on_call\") + s(\"wrap_up\"));",
                  "    pm.expect(o.denominator_ms).to.eql(r.counted_ms - s(\"paused\"));",
                  "    pm.expect(o.complete).to.eql(r.excluded.open.intervals === 0 && r.excluded.synthetic.intervals === 0);",
                  "    if (r.overlapping_intervals > 0) { pm.expect(o.reason).to.eql(\"overlapping_intervals\"); }",
                  "    else if (o.denominator_ms === 0) { pm.expect(o.reason).to.eql(\"no_staffed_time\"); }",
                  "    else { pm.expect(o.reason).to.eql(null); pm.expect(o.percent).to.be.within(0, 100); pm.expect(o.percent).to.be.closeTo(o.numerator_ms * 100 / o.denominator_ms, 0.051); }",
                  "    pm.expect(o.percent === null).to.eql(o.reason !== null);",
                  "    const sum = Object.values(r.by_state).reduce(function (a, b) { return a + b; }, 0);",
                  "    pm.expect(r.counted_ms).to.eql(sum);",
                  "    pm.expect(r.occupied_ms).to.eql(r.by_state.on_call || 0);",
                  "    pm.expect(r.excluded).to.have.all.keys(\"synthetic\", \"open\");",
                  "    pm.expect(r.excluded.synthetic).to.have.all.keys(\"intervals\", \"ms\");",
                  "    pm.expect(r.excluded.open).to.have.all.keys(\"intervals\");",
                  "  });",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/agents/handle-time?from&to",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/agents/handle-time?from={{handle_time_from}}&to={{handle_time_to}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents",
                "handle-time"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "{{handle_time_from}}"
                },
                {
                  "key": "to",
                  "value": "{{handle_time_to}}"
                }
              ]
            },
            "description": "Tiempo de atencion por agente de una ventana explicita de INICIO de llamada `[from, to)`, acreditando a cada agente SOLO sus tramos de una llamada transferida (core#2403, ADR #297): `Dialer.Calls.TransferChain.handle_time/4` tal cual, derivado del rastro `call_control_actions` y del reloj del CDR. `aht_ms` es `control_ms / calls` y `null` (nunca un cero fabricado) si el agente no sostuvo ninguna llamada. Lo determinista es la forma y los invariantes de cada entrada; la cadena por llamada sale en `transfer_chain` de `GET /v1/calls/{call_id}`."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Ventana RELATIVA al reloj, nunca un literal de calendario.",
                  "const to = new Date(Date.now() + 60 * 1000);",
                  "const from = new Date(to.getTime() - 60 * 60 * 1000);",
                  "pm.collectionVariables.set(\"handle_time_from\", from.toISOString());",
                  "pm.collectionVariables.set(\"handle_time_to\", to.toISOString());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: definicion v1 y ventana\", function () {",
                  "  pm.expect(j.definition).to.eql(\"v1\");",
                  "  pm.expect(Date.parse(j.from)).to.eql(Date.parse(pm.collectionVariables.get(\"handle_time_from\")));",
                  "  pm.expect(Date.parse(j.to)).to.eql(Date.parse(pm.collectionVariables.get(\"handle_time_to\")));",
                  "  pm.expect(j.excluded).to.have.all.keys(\"open_legs\");",
                  "});",
                  "pm.test(\"shape: cada agente trae sus tramos medidos y el AHT sale de ellos\", function () {",
                  "  pm.expect(j.agents).to.be.an(\"array\");",
                  "  j.agents.forEach(function (r) {",
                  "    pm.expect(r).to.have.all.keys(\"agent_id\", \"calls\", \"control_ms\", \"consult_ms\", \"occupied_ms\", \"aht_ms\");",
                  "    pm.expect(r.occupied_ms).to.eql(r.control_ms + r.consult_ms);",
                  "    if (r.calls === 0) { pm.expect(r.aht_ms).to.eql(null); }",
                  "    else { pm.expect(r.aht_ms).to.eql(Math.floor(r.control_ms / r.calls)); }",
                  "  });",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/agents/occupancy?agent_id={{ghost_uuid}} (filtro sin evidencia -> mapa vacio)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/agents/occupancy?from={{occupancy_from}}&to={{occupancy_to}}&agent_id={{ghost_uuid}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents",
                "occupancy"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "{{occupancy_from}}"
                },
                {
                  "key": "to",
                  "value": "{{occupancy_to}}"
                },
                {
                  "key": "agent_id",
                  "value": "{{ghost_uuid}}"
                }
              ]
            },
            "description": "El filtro opcional `agent_id` acota dentro del tenant del bearer. Un asiento sin evidencia en la ventana NO aparece: la respuesta es 200 con `agents: {}`, nunca un 404 ni una fila a cero."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"asiento sin evidencia: mapa vacio, no cero fabricado\", function () {",
                  "  pm.expect(j.agents).to.eql({});",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/agents/occupancy?to (sin from -> 400)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/agents/occupancy?to={{occupancy_to}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents",
                "occupancy"
              ],
              "query": [
                {
                  "key": "to",
                  "value": "{{occupancy_to}}"
                }
              ]
            },
            "description": "`from` y `to` son obligatorios (la ventana es explicita a proposito: no hay ventana por defecto que fabricar). Sin `from` es 400 nombrando el parametro."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "const j = pm.response.json();",
                  "pm.test(\"400 que nombra el parametro que falta\", function () {",
                  "  pm.expect(j.error.code).to.eql(\"bad_request\");",
                  "  pm.expect(j.error.message).to.eql(\"from is required\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/agents/occupancy?from=to (ventana no creciente -> 400)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/agents/occupancy?from={{occupancy_to}}&to={{occupancy_to}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents",
                "occupancy"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "{{occupancy_to}}"
                },
                {
                  "key": "to",
                  "value": "{{occupancy_to}}"
                }
              ]
            },
            "description": "La ventana es semiabierta y debe crecer: `from == to` es 400 `from must be before to`."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "const j = pm.response.json();",
                  "pm.test(\"400 de ventana no creciente\", function () {",
                  "  pm.expect(j.error.code).to.eql(\"bad_request\");",
                  "  pm.expect(j.error.message).to.eql(\"from must be before to\");",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "10 · supervision",
      "description": "Audio del supervisor sobre una llamada del CDR del tenant. Determinista solo con una llamada viva ({{golden_call_id}} se siembra si el dial manual devolvió `dialing`); sin ella el contrato responde 404. Desde core#930 (ADR #116 de core) `supervisor_ext` debe ser un agente con rol `supervisor|admin`: la carpeta siembra su propio seat supervisor ({{postman_supervisor_ext}}) y cierra con la request negativa — la extensión del seat `agent` de la carpeta 09 ⇒ exactamente 403 `role_forbidden`. El gate se demuestra, no se tolera.",
      "item": [
        {
          "name": "POST /v1/agents (seat con rol supervisor para esta carpeta)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/agents",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents"
              ]
            },
            "description": "Siembra el seat que las 4 requests de supervisión nombran en `supervisor_ext`. El seed de core (agente 1000) nace con rol `agent` y core#930 lo rechaza con 403; el rol se fija aquí, en la propia colección, sin tocar el seed.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Postman Supervisor\",\n  \"email\": \"postman-supervisor+{{$timestamp}}@example.test\",\n  \"role\": \"supervisor\",\n  \"sip_extension\": \"{{postman_supervisor_ext}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Prefijo 8: no colisiona con el seat agent de la carpeta 09 (prefijo 9) ni con el 1000 del seed.",
                  "const ext = \"8\" + String(Date.now()).slice(-6);",
                  "pm.collectionVariables.set(\"postman_supervisor_ext\", ext);"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: agent.role == supervisor (lo que exige el gate de core#930)\", function () {",
                  "  pm.expect(j.agent.id).to.be.a(\"string\");",
                  "  pm.expect(j.agent.role).to.eql(\"supervisor\");",
                  "  pm.expect(j.agent.sip_extension).to.eql(pm.collectionVariables.get(\"postman_supervisor_ext\"));",
                  "});",
                  "pm.collectionVariables.set(\"postman_supervisor_id\", j.agent.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/supervision/calls/:call_id/listen",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/supervision/calls/{{golden_call_id}}/listen",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "supervision",
                "calls",
                "{{golden_call_id}}",
                "listen"
              ]
            },
            "description": "Supervisión `listen` (evidence-first: audit row ANTES del comando ESL). Con llamada viva → 200; sin call_id/llamada → 404; switch rechaza → 502; audit/ESL caído → 503. `supervisor_ext` es el seat supervisor sembrado arriba: con rol `agent` sería 403 `role_forbidden` (core#930).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"supervisor_ext\": \"{{postman_supervisor_ext}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200|404|502|503 (según haya llamada viva)\", function () {",
                  "  pm.expect([200, 404, 502, 503]).to.include(pm.response.code);",
                  "});",
                  "if (pm.response.code === 200) {",
                  "  const j = pm.response.json();",
                  "  pm.test(\"shape 200: supervision presente\", function () { pm.expect(j.supervision).to.be.an(\"object\"); });",
                  "}",
                  "if (pm.response.code === 404) {",
                  "  pm.test(\"404 del endpoint real (no catch-all 'no such route')\", function () {",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/supervision/calls/:call_id/whisper",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/supervision/calls/{{golden_call_id}}/whisper",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "supervision",
                "calls",
                "{{golden_call_id}}",
                "whisper"
              ]
            },
            "description": "Supervisión `whisper` (evidence-first: audit row ANTES del comando ESL). Con llamada viva → 200; sin call_id/llamada → 404; switch rechaza → 502; audit/ESL caído → 503. `supervisor_ext` es el seat supervisor sembrado arriba: con rol `agent` sería 403 `role_forbidden` (core#930).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"supervisor_ext\": \"{{postman_supervisor_ext}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200|404|502|503 (según haya llamada viva)\", function () {",
                  "  pm.expect([200, 404, 502, 503]).to.include(pm.response.code);",
                  "});",
                  "if (pm.response.code === 200) {",
                  "  const j = pm.response.json();",
                  "  pm.test(\"shape 200: supervision presente\", function () { pm.expect(j.supervision).to.be.an(\"object\"); });",
                  "}",
                  "if (pm.response.code === 404) {",
                  "  pm.test(\"404 del endpoint real (no catch-all 'no such route')\", function () {",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/supervision/calls/:call_id/barge",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/supervision/calls/{{golden_call_id}}/barge",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "supervision",
                "calls",
                "{{golden_call_id}}",
                "barge"
              ]
            },
            "description": "Supervisión `barge` (evidence-first: audit row ANTES del comando ESL). Con llamada viva → 200; sin call_id/llamada → 404; switch rechaza → 502; audit/ESL caído → 503. `supervisor_ext` es el seat supervisor sembrado arriba: con rol `agent` sería 403 `role_forbidden` (core#930).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"supervisor_ext\": \"{{postman_supervisor_ext}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200|404|502|503 (según haya llamada viva)\", function () {",
                  "  pm.expect([200, 404, 502, 503]).to.include(pm.response.code);",
                  "});",
                  "if (pm.response.code === 200) {",
                  "  const j = pm.response.json();",
                  "  pm.test(\"shape 200: supervision presente\", function () { pm.expect(j.supervision).to.be.an(\"object\"); });",
                  "}",
                  "if (pm.response.code === 404) {",
                  "  pm.test(\"404 del endpoint real (no catch-all 'no such route')\", function () {",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/supervision/calls/:call_id/takeover",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/supervision/calls/{{golden_call_id}}/takeover",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "supervision",
                "calls",
                "{{golden_call_id}}",
                "takeover"
              ]
            },
            "description": "Supervisión `takeover` (evidence-first: audit row ANTES del comando ESL). Con llamada viva → 200; sin call_id/llamada → 404; switch rechaza → 502; audit/ESL caído → 503. `supervisor_ext` es el seat supervisor sembrado arriba: con rol `agent` sería 403 `role_forbidden` (core#930).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"supervisor_ext\": \"{{postman_supervisor_ext}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200|404|502|503 (según haya llamada viva)\", function () {",
                  "  pm.expect([200, 404, 502, 503]).to.include(pm.response.code);",
                  "});",
                  "if (pm.response.code === 200) {",
                  "  const j = pm.response.json();",
                  "  pm.test(\"shape 200: supervision presente\", function () { pm.expect(j.supervision).to.be.an(\"object\"); });",
                  "}",
                  "if (pm.response.code === 404) {",
                  "  pm.test(\"404 del endpoint real (no catch-all 'no such route')\", function () {",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/supervision/calls/:call_id/listen (ext con rol agent -> 403 role_forbidden)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/supervision/calls/{{golden_call_id}}/listen",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "supervision",
                "calls",
                "{{golden_call_id}}",
                "listen"
              ]
            },
            "description": "Negativa del gate de rol (core#930, ADR #116 de core): `supervisor_ext` es el seat de la carpeta 09, creado con `role: agent`. Exactamente 403 y `error.code == role_forbidden`, haya o no llamada viva: el WHO va antes que el lookup de la llamada (sin oráculo 404-vs-403). La evidencia queda como `supervision_actions.result = refused`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"supervisor_ext\": \"{{postman_agent_ext}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403 exacto (gate de rol antes que la llamada)\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"error.code == role_forbidden\", function () {",
                  "  const e = pm.response.json().error;",
                  "  pm.expect(e.code).to.eql(\"role_forbidden\");",
                  "  pm.expect(e.message).to.include(\"supervisor or admin\");",
                  "});"
                ]
              }
            }
          ]
        }
      ],
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "// golden_call_id queda vacío cuando el dial del golden-flow tomó la rama 422 (la campaña del golden-flow no manda campaign_type ⇒ predictive; el API sí acepta el campo).",
              "// Con la variable vacía la URL tendría un segmento vacío y caería en el",
              "// catch-all del router ('no such route') SIN ejercitar la ruta real:",
              "// sembramos un UUID aleatorio para que el 404 sea el del endpoint.",
              "if (!pm.collectionVariables.get(\"golden_call_id\")) {",
              "  pm.collectionVariables.set(\"golden_call_id\", pm.variables.replaceIn(\"{{$guid}}\"));",
              "}"
            ]
          }
        }
      ]
    },
    {
      "name": "11 · stats",
      "description": "Contadores operativos + evidencia durable de bloqueos + metering voice-AI.",
      "item": [
        {
          "name": "GET /v1/stats/summary",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/stats/summary",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "stats",
                "summary"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: contadores del día\", function () {",
                  "  pm.expect(j.calls_today).to.be.a(\"number\");",
                  "  pm.expect(j.agents_online).to.be.a(\"number\");",
                  "  pm.expect(j).to.have.property(\"campaigns_running\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/stats/contact-windows",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/stats/contact-windows?from={{cw_from}}&to={{cw_to}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "stats",
                "contact-windows"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "{{cw_from}}"
                },
                {
                  "key": "to",
                  "value": "{{cw_to}}"
                }
              ]
            },
            "description": "Tasa de contacto por hora LOCAL del consumidor (core#1525): 24 horas siempre presentes, cubo `unknown` aparte, `best_window` nulo con su razón cuando no hay intentos suficientes, y teléfonos por persona."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Ventana de 7 días que termina AHORA (tope 90, por defecto 30).",
                  "const to = new Date();",
                  "const from = new Date(to.getTime() - 7 * 24 * 3600 * 1000);",
                  "pm.collectionVariables.set(\"cw_from\", from.toISOString());",
                  "pm.collectionVariables.set(\"cw_to\", to.toISOString());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: 24 horas locales y el cubo unknown aparte\", function () {",
                  "  pm.expect(j.basis).to.eql(\"consumer_local_hour\");",
                  "  pm.expect(j.hours).to.have.lengthOf(24);",
                  "  pm.expect(j.hours[0].hour).to.eql(0);",
                  "  pm.expect(j.hours[23].hour).to.eql(23);",
                  "  pm.expect(j.unknown.attempts).to.be.a(\"number\");",
                  "  pm.expect(j.min_attempts).to.be.a(\"number\");",
                  "});",
                  "pm.test(\"best_window honesto: o ventana de dos horas o nulo con razón\", function () {",
                  "  if (j.best_window === null) {",
                  "    pm.expect(j.best_window_reason).to.be.oneOf([\"insufficient_attempts\", \"no_resolved_attempts\"]);",
                  "  } else {",
                  "    pm.expect(j.best_window.end_hour - j.best_window.start_hour).to.eql(2);",
                  "    pm.expect(j.best_window_reason).to.eql(null);",
                  "  }",
                  "});",
                  "pm.test(\"shape: teléfonos por persona\", function () {",
                  "  pm.expect(j.phones_per_person.persons).to.be.a(\"number\");",
                  "  pm.expect(j.phones_per_person.phones).to.be.a(\"number\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/stats/contact-windows (rango > 90 días → 422)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/stats/contact-windows?from={{cw_wide_from}}&to={{cw_wide_to}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "stats",
                "contact-windows"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "{{cw_wide_from}}"
                },
                {
                  "key": "to",
                  "value": "{{cw_wide_to}}"
                }
              ]
            },
            "description": "Una ventana de más de 90 días se rechaza con 422: el informe se calcula en lectura sobre el CDR y su coste lo acota el rango (core#1525)."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const to = new Date();",
                  "const from = new Date(to.getTime() - 91 * 24 * 3600 * 1000);",
                  "pm.collectionVariables.set(\"cw_wide_from\", from.toISOString());",
                  "pm.collectionVariables.set(\"cw_wide_to\", to.toISOString());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 422\", function () { pm.response.to.have.status(422); });",
                  "pm.test(\"el error nombra el tope\", function () {",
                  "  const j = pm.response.json();",
                  "  pm.expect(j.error.code).to.eql(\"unprocessable\");",
                  "  pm.expect(j.error.message).to.include(\"90 days\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/stats/promises",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/stats/promises",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "stats",
                "promises"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: pipeline PTP (open)\", function () { pm.expect(j).to.have.property(\"open\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/stats/penetration",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/stats/penetration?campaign_ids={{golden_campaign_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "stats",
                "penetration"
              ],
              "query": [
                {
                  "key": "campaign_ids",
                  "value": "{{golden_campaign_id}}"
                }
              ]
            },
            "description": "Penetración a nivel persona de la campaña golden."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: totals con unique_accounts\", function () {",
                  "  pm.expect(j.totals).to.be.an(\"object\");",
                  "  pm.expect(j.totals.unique_accounts).to.be.a(\"number\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/stats/penetration (granularity=day)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/stats/penetration?campaign_ids={{golden_campaign_id}}&granularity=day&tz=America/Mexico_City&from={{pen_day_from}}&to={{pen_day_to}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "stats",
                "penetration"
              ],
              "query": [
                {
                  "key": "campaign_ids",
                  "value": "{{golden_campaign_id}}"
                },
                {
                  "key": "granularity",
                  "value": "day"
                },
                {
                  "key": "tz",
                  "value": "America/Mexico_City"
                },
                {
                  "key": "from",
                  "value": "{{pen_day_from}}"
                },
                {
                  "key": "to",
                  "value": "{{pen_day_to}}"
                }
              ]
            },
            "description": "Desglose por día local (core#1182): filas densas día × campaña cortadas en `tz`. `totals` y `by_campaign` siguen siendo los del rango."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Ventana de 3 días que termina AHORA: granularity=day exige from y to (tope 31 días).",
                  "const to = new Date();",
                  "const from = new Date(to.getTime() - 3 * 24 * 3600 * 1000);",
                  "pm.collectionVariables.set(\"pen_day_from\", from.toISOString());",
                  "pm.collectionVariables.set(\"pen_day_to\", to.toISOString());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: by_day denso con día y campaña, totals intactos\", function () {",
                  "  pm.expect(j.granularity).to.eql(\"day\");",
                  "  pm.expect(j.tz).to.eql(\"America/Mexico_City\");",
                  "  pm.expect(j.totals.unique_accounts).to.be.a(\"number\");",
                  "  pm.expect(j.by_day).to.be.an(\"array\");",
                  "  // 3 días exactos tocan 3 o 4 días locales, por cada campaña con universo.",
                  "  const campaigns = j.by_campaign.length;",
                  "  pm.expect(campaigns, \"campañas con universo\").to.be.above(0);",
                  "  pm.expect(j.by_day.length).to.be.oneOf([3 * campaigns, 4 * campaigns]);",
                  "  j.by_day.forEach(function (r) {",
                  "    pm.expect(r.day).to.match(/^\\d{4}-\\d{2}-\\d{2}$/);",
                  "    pm.expect(r.campaign_id).to.be.a(\"string\");",
                  "    pm.expect(r.attempted).to.be.a(\"number\");",
                  "    pm.expect(r.effective_contact_pct).to.be.a(\"number\");",
                  "  });",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/stats/penetration (granularity=day sin tz → 422)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/stats/penetration?campaign_ids={{golden_campaign_id}}&granularity=day&from={{pen_day_from}}&to={{pen_day_to}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "stats",
                "penetration"
              ],
              "query": [
                {
                  "key": "campaign_ids",
                  "value": "{{golden_campaign_id}}"
                },
                {
                  "key": "granularity",
                  "value": "day"
                },
                {
                  "key": "from",
                  "value": "{{pen_day_from}}"
                },
                {
                  "key": "to",
                  "value": "{{pen_day_to}}"
                }
              ]
            },
            "description": "Sin `tz`, el desglose por día se rechaza con 422: nunca se corta en UTC en silencio (core#1182)."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Ventana de 3 días que termina AHORA: granularity=day exige from y to (tope 31 días).",
                  "const to = new Date();",
                  "const from = new Date(to.getTime() - 3 * 24 * 3600 * 1000);",
                  "pm.collectionVariables.set(\"pen_day_from\", from.toISOString());",
                  "pm.collectionVariables.set(\"pen_day_to\", to.toISOString());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 422\", function () { pm.response.to.have.status(422); });",
                  "pm.test(\"el error nombra tz\", function () {",
                  "  const j = pm.response.json();",
                  "  pm.expect(j.error.code).to.eql(\"unprocessable\");",
                  "  pm.expect(j.error.message).to.include(\"tz\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/stats/abandonment",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/stats/abandonment",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "stats",
                "abandonment"
              ]
            },
            "description": "core#2617 (hija B de core#2338): el mismo informe de abandono durable que GET /v1/campaigns/:id/abandonment, de TODAS las campañas del tenant en una sola respuesta (la consola preguntaba una vez por campaña). Sale de una instantánea en memoria derivada del CDR append-only: `as_of` es el instante en que se calcularon las cifras y una instantánea más vieja que el `max_age` del servidor (60 s por defecto) nunca se sirve, se calcula en vivo. Un `AbandonmentReport` por campaña, ordenado por nombre. La ruta por campaña sigue en vivo y es el registro; esta es la vista de conjunto."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: as_of + campaigns[] con el informe por campaña\", function () {",
                  "  pm.expect(j.as_of).to.be.a(\"string\");",
                  "  pm.expect(Number.isNaN(Date.parse(j.as_of))).to.eql(false);",
                  "  pm.expect(j.campaigns).to.be.an(\"array\");",
                  "  j.campaigns.forEach(function (c) {",
                  "    pm.expect(c.campaign_id).to.be.a(\"string\");",
                  "    pm.expect(c.period_days).to.eql(30);",
                  "    pm.expect(c.ceiling_percent).to.eql(3);",
                  "    pm.expect(c.periods).to.be.an(\"array\");",
                  "  });",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/stats/blocked",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/stats/blocked?campaign_id={{golden_campaign_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "stats",
                "blocked"
              ],
              "query": [
                {
                  "key": "campaign_id",
                  "value": "{{golden_campaign_id}}"
                }
              ]
            },
            "description": "Rollup durable de dials REHUSADOS por gate (gate_blocks append-only, ADR #8)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: total + by_gate\", function () {",
                  "  pm.expect(j.total).to.be.a(\"number\");",
                  "  pm.expect(j.by_gate).to.be.an(\"object\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/stats/capacity",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/stats/capacity",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "stats",
                "capacity"
              ]
            },
            "description": "Techos de capacidad admin (max_cps, max_concurrent_channels; null = sin techo) + uso en vivo leido del capacity gate. limits: null = los techos no se pudieron leer y nunca se cachearon (la condicion fail-closed del gate), reportado con honestidad — nunca fabricado como \"ilimitado\". (ADR #12)"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: techos + uso en vivo\", function () {",
                  "  pm.expect(j).to.have.property(\"limits\");",
                  "  pm.expect(j.channels_in_use).to.be.a(\"number\");",
                  "  pm.expect(j.dials_last_second).to.be.a(\"number\");",
                  "  if (j.limits !== null) {",
                  "    pm.expect(j.limits).to.have.property(\"max_cps\");",
                  "    pm.expect(j.limits).to.have.property(\"max_concurrent_channels\");",
                  "  }",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/stats/ai-usage",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/stats/ai-usage",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "stats",
                "ai-usage"
              ]
            },
            "description": "Metering de minutos voice-AI (feed ai_usage exactly-once). CSV vía ?format=csv."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: ventana from/to\", function () {",
                  "  pm.expect(j).to.have.property(\"from\");",
                  "  pm.expect(j).to.have.property(\"to\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/stats/sms-usage",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/stats/sms-usage",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "stats",
                "sms-usage"
              ]
            },
            "description": "Metering de segmentos SMS (ledger unificado billable_events, filas sms_segment con snapshot de precio por región). CSV vía ?format=csv."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: ventana from/to + totales del ledger\", function () {",
                  "  pm.expect(j).to.have.property(\"from\");",
                  "  pm.expect(j).to.have.property(\"to\");",
                  "  pm.expect(j).to.have.property(\"messages\");",
                  "  pm.expect(j).to.have.property(\"segments\");",
                  "  pm.expect(j).to.have.property(\"amount_usd\");",
                  "  pm.expect(j.daily).to.be.an(\"array\");",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "12 · billing",
      "description": "La lectura \"qué facturo\": seats por tier + overage de DIDs + uso voice-AI.",
      "item": [
        {
          "name": "GET /v1/billing/account",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/billing/account",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "billing",
                "account"
              ]
            },
            "description": "Saldo, techo en vigor, consumo contra el techo y modo de enforcement, en un solo objeto. `balance` y `cap` son nulables de forma independiente: un tenant con billing apagado responde 200 con `billing_enabled: false` y dinero nulo (no 404), y `cap` es nulo además siempre que el carril esté inerte (plan no activo, sin wallet, sin funding, o sin grant todavía)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: identidad y modo\", function () {",
                  "    pm.expect(j.tenant_id).to.be.a(\"string\");",
                  "    pm.expect(j.currency).to.be.a(\"string\");",
                  "    pm.expect(j.billing_enabled).to.be.a(\"boolean\");",
                  "    pm.expect(j.alert_thresholds).to.be.an(\"array\");",
                  "    // enforcement_mode es nulable por contrato: el router lo sirve como",
                  "    // `plan && plan.enforcement_mode`, asi que un tenant SIN plan responde",
                  "    // 200 con billing_enabled:false y este campo en null.",
                  "    if (j.enforcement_mode !== null) {",
                  "        pm.expect(j.enforcement_mode).to.be.a(\"string\");",
                  "    }",
                  "});",
                  "// balance y cap son nulables por contrato (billing apagado / carril inerte):",
                  "// se afirma la forma SOLO cuando vienen, para no convertir un 200 valido en rojo.",
                  "pm.test(\"shape: balance nulo o montos en string\", function () {",
                  "    if (j.balance === null) return;",
                  "    pm.expect(j.balance.available_usd).to.be.a(\"string\");",
                  "    pm.expect(j.balance.cash_usd).to.be.a(\"string\");",
                  "});",
                  "pm.test(\"shape: cap nulo o techo con consumo\", function () {",
                  "    if (j.cap === null) return;",
                  "    pm.expect(j.cap.limit_usd).to.be.a(\"string\");",
                  "    pm.expect(j.cap.spent_usd).to.be.a(\"string\");",
                  "    // used_pct es nulable DENTRO de un cap presente: `used_pct/2` es un",
                  "    // `if limit > 0` sin `else`, o sea null cuando no hay techo positivo.",
                  "    if (j.cap.used_pct !== null) {",
                  "        pm.expect(j.cap.used_pct).to.be.a(\"string\");",
                  "    }",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/billing/cost",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/billing/cost?group_by=campaign&from={{billing_cost_from}}&to={{billing_cost_to}}",
            "description": "Coste bruto del ledger por campaña durante los dos días UTC más recientes. La ventana usa sólo días que el contrato permite leer directamente, incluso si todavía no están cerrados en el rollup."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const to = new Date();",
                  "const from = new Date(to);",
                  "from.setUTCDate(from.getUTCDate() - 1);",
                  "from.setUTCHours(0, 0, 0, 0);",
                  "pm.collectionVariables.set(\"billing_cost_from\", from.toISOString());",
                  "pm.collectionVariables.set(\"billing_cost_to\", to.toISOString());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: dimensión, ventana y fuentes\", function () {",
                  "  pm.expect(j.group_by).to.eql(\"campaign\");",
                  "  pm.expect(j.from).to.eql(pm.collectionVariables.get(\"billing_cost_from\"));",
                  "  pm.expect(j.to).to.eql(pm.collectionVariables.get(\"billing_cost_to\"));",
                  "  pm.expect(j.ledger_days.from).to.match(/^\\d{4}-\\d{2}-\\d{2}$/);",
                  "  pm.expect(j.ledger_days.to).to.match(/^\\d{4}-\\d{2}-\\d{2}$/);",
                  "  pm.expect(j.sources.rollup).to.be.a(\"number\");",
                  "  pm.expect(j.sources.direct).to.be.a(\"number\");",
                  "  pm.expect(j.sources.empty).to.be.a(\"number\");",
                  "});",
                  "pm.test(\"shape: grupos y totales monetarios\", function () {",
                  "  pm.expect(j.groups).to.be.an(\"array\");",
                  "  pm.expect(j.gross_total).to.be.an(\"array\");",
                  "  j.groups.forEach(function (g) {",
                  "    pm.expect(g.unattributed).to.eql(g.key === null);",
                  "    pm.expect(g.currency).to.be.a(\"string\");",
                  "    pm.expect(g.amount).to.be.a(\"string\");",
                  "    pm.expect(Number.isInteger(g.events)).to.eql(true);",
                  "  });",
                  "  j.gross_total.forEach(function (t) { pm.expect(t.amount).to.be.a(\"string\"); pm.expect(Number.isInteger(t.events)).to.eql(true); });",
                  "});",
                  "pm.test(\"shape: reconciliación explícita\", function () {",
                  "  pm.expect(j.reconciliation.basis).to.eql(\"ledger_gross\");",
                  "  pm.expect(j.reconciliation.groups_sum).to.be.an(\"array\");",
                  "  pm.expect(j.reconciliation.not_allocated).to.eql([\"included_allowance\", \"volume_tier\", \"discount\", \"monthly_minimum\"]);",
                  "  pm.expect(j.reconciliation.invoice.status).to.be.oneOf([\"available\", \"unavailable\"]);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/billing/cost — sin bearer",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/billing/cost?group_by=campaign&from={{billing_cost_from}}&to={{billing_cost_to}}",
            "auth": {
              "type": "noauth"
            },
            "description": "Control negativo de autenticación sobre la misma ventana válida y con la flag activa: sin bearer la ruta responde el envelope 401, no datos de coste."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 401\", function () { pm.response.to.have.status(401); });",
                  "pm.test(\"shape: unauthorized sin datos\", function () {",
                  "  const j = pm.response.json();",
                  "  pm.expect(j.error.code).to.eql(\"unauthorized\");",
                  "  pm.expect(j).to.not.have.property(\"groups\");",
                  "  pm.expect(j).to.not.have.property(\"gross_total\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/billing/ledger",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/billing/ledger?limit=2",
            "description": "El libro de saldo del tenant, asiento a asiento y de más nuevo a más viejo, paginado por cursor (core#2573). Cada asiento trae su tipo, los deltas con signo y las claves que lo unen a su origen; sin texto libre."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: página de asientos\", function () {",
                  "  pm.expect(j.entries).to.be.an(\"array\");",
                  "  pm.expect(j.entries.length).to.be.at.most(2);",
                  "  pm.expect(j.next_cursor === null || typeof j.next_cursor === \"string\").to.eql(true);",
                  "});",
                  "pm.test(\"shape: cada asiento, con dinero en string y sin texto libre\", function () {",
                  "  j.entries.forEach(function (e) {",
                  "    pm.expect(e.id).to.be.a(\"string\");",
                  "    pm.expect(e.entry_type).to.be.a(\"string\");",
                  "    pm.expect(e.ref_uuid).to.be.a(\"string\");",
                  "    pm.expect(e.cash_delta).to.be.a(\"string\");",
                  "    pm.expect(e.grant_delta).to.be.a(\"string\");",
                  "    pm.expect(e.deposit_delta).to.be.a(\"string\");",
                  "    pm.expect(e.occurred_on).to.match(/^\\d{4}-\\d{2}-\\d{2}$/);",
                  "    pm.expect(e).to.not.have.property(\"note\");",
                  "  });",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/billing/ledger — sin bearer",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/billing/ledger?limit=2",
            "auth": {
              "type": "noauth"
            },
            "description": "Control negativo de autenticación sobre la misma petición válida y con la flag activa: sin bearer la ruta responde el envelope 401, no asientos."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 401\", function () { pm.response.to.have.status(401); });",
                  "pm.test(\"shape: unauthorized sin datos\", function () {",
                  "  const j = pm.response.json();",
                  "  pm.expect(j.error.code).to.eql(\"unauthorized\");",
                  "  pm.expect(j).to.not.have.property(\"entries\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/billing/alerts",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/billing/alerts",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "billing",
                "alerts"
              ]
            },
            "description": "Los cruces de umbral registrados en el epoch de facturación vigente, más nuevos primero. Cada fila se sirve TAL CUAL se grabó: `spent_usd`, `limit_usd` y `enforcement_mode` son el snapshot AL cruce, no un recálculo contra el saldo de hoy. `period_key` es el mismo epoch que `cap.period_key` de `GET /v1/billing/account`, y es nulo cuando el carril no tiene epoch — en cuyo caso `alerts` viene vacío. Sin plan responde 200 con lista vacía, nunca 404."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: identidad y epoch\", function () {",
                  "    pm.expect(j.tenant_id).to.be.a(\"string\");",
                  "    pm.expect(j.alerts).to.be.an(\"array\");",
                  "    // period_key es nulable por contrato: null cuando el carril no",
                  "    // tiene epoch (sin plan activo, sin wallet, sin funding, o sin",
                  "    // grant todavia). Se afirma la forma SOLO cuando viene, para no",
                  "    // convertir un 200 valido en rojo.",
                  "    if (j.period_key !== null) {",
                  "        pm.expect(j.period_key).to.be.a(\"string\");",
                  "    }",
                  "});",
                  "// La lista puede estar vacia legitimamente: un epoch recien abierto",
                  "// re-arma los umbrales, asi que \"nada cruzado AUN\" no es un fallo.",
                  "pm.test(\"shape: cada cruce trae su snapshot entero\", function () {",
                  "    j.alerts.forEach(function (a) {",
                  "        // El contrato dice integer y enum, no number y string: con los",
                  "        // asertos laxos pasaban 90.5 y \"bogus_scheme\" (it#185).",
                  "        pm.expect(Number.isInteger(a.threshold_pct), \"threshold_pct entero\").to.be.true;",
                  "        pm.expect(a.scheme).to.be.oneOf([\"subscription\", \"prepaid\", \"postpaid\"]);",
                  "        pm.expect(a.enforcement_mode).to.be.oneOf([\"graduated\", \"hard_block\"]);",
                  "        pm.expect(a.spent_usd).to.be.a(\"string\");",
                  "        pm.expect(a.limit_usd).to.be.a(\"string\");",
                  "        pm.expect(a.crossed_at).to.be.a(\"string\");",
                  "    });",
                  "});",
                  "pm.test(\"orden: mas nuevos primero\", function () {",
                  "    // Date.parse trunca a MILISEGUNDOS y crossed_at es utc_datetime_usec:",
                  "    // dos cruces del mismo ms en orden invertido pasaban el aserto (it#185).",
                  "    // Se compara el par (instante, fraccion), con la fraccion a 6 digitos.",
                  "    const t = j.alerts.map(function (a) {",
                  "        const m = /^(.+?)(?:\\.(\\d+))?Z$/.exec(a.crossed_at);",
                  "        const base = m ? m[1] : String(a.crossed_at).replace(/Z$/, \"\");",
                  "        const frac = (m && m[2] ? m[2] : \"\").padEnd(6, \"0\").slice(0, 6);",
                  "        return { s: Date.parse(base + \"Z\"), f: Number(frac) };",
                  "    });",
                  "    for (let i = 1; i < t.length; i++) {",
                  "        const prev = t[i - 1], cur = t[i];",
                  "        const ordenado = prev.s > cur.s || (prev.s === cur.s && prev.f >= cur.f);",
                  "        pm.expect(ordenado, \"cruce \" + (i - 1) + \" no es mas nuevo que \" + i).to.be.true;",
                  "    }",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/billing/summary",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/billing/summary",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "billing",
                "summary"
              ]
            },
            "description": "Line items facturables del período. CSV vía ?format=csv.\n\nLas líneas de libro diario (`house_minutes`, `inbound_minutes`, `did_rent`, `sms`) suman días UTC enteros aunque `from`/`to` sean horarios; desde core#1383 `period.day_ledger` (`from_day`, `to_day`, `kinds`) dice qué días sumaron, junto al periodo pedido.\n\nCada línea lleva `fiscal_bucket` (`saas | telecom | pass_through | tax`) desde macro-tasks#448 MT-D: cómo se clasifica ese dinero fiscalmente. Antes se retiraba de la respuesta, porque el encuadre era una propuesta y publicarlo habría afirmado una postura que nadie había tomado (ADR #73 decisión 6); el founder la confirmó y el filtro se quitó."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: line_items[]\", function () { pm.expect(j.line_items).to.be.an(\"array\"); });",
                  "// core#1383: las lineas de libro diario (house/inbound minutes, did_rent, sms)",
                  "// suman DIAS UTC enteros aunque from/to sean horarios, y period.day_ledger dice",
                  "// que dias sumaron. Sin from/to la ventana es de 30 dias con from a media",
                  "// jornada: from_day es el dia de from y to_day el del ultimo instante antes de to.",
                  "// Condicional porque esta coleccion corre contra el core de main, que no lo",
                  "// trae hasta que core#1383 mergee; cuando esta presente se comprueba entero.",
                  "pm.test(\"period.day_ledger: el tramo de dias que sumaron las lineas de libro diario\", function () {",
                  "  if (j.period.day_ledger === undefined) { return; }",
                  "  const dl = j.period.day_ledger;",
                  "  pm.expect(dl.from_day).to.eql(j.period.from.slice(0, 10));",
                  "  const lastInstant = new Date(Date.parse(j.period.to) - 1);",
                  "  pm.expect(dl.to_day).to.eql(lastInstant.toISOString().slice(0, 10));",
                  "  pm.expect(dl.kinds).to.eql([\"house_minutes\", \"inbound_minutes\", \"did_rent\", \"sms\"]);",
                  "  const kinds = j.line_items.map(function (i) { return i.kind; });",
                  "  dl.kinds.forEach(function (kind) { pm.expect(kinds).to.include(kind); });",
                  "});",
                  "// macro-tasks#448 MT-D: el cubo fiscal ES publico. Se afirma sobre TODAS",
                  "// las lineas y contra el vocabulario cerrado, no sobre la primera ni",
                  "// contra `undefined`: un filtro que volviera solo para algunas lineas",
                  "// pasaria un test que mirase una sola.",
                  "pm.test(\"cada linea publica su fiscal_bucket, del vocabulario cerrado\", function () {",
                  "  const buckets = [\"saas\", \"telecom\", \"pass_through\", \"tax\"];",
                  "  pm.expect(j.line_items).to.not.be.empty;",
                  "  j.line_items.forEach(function (item) {",
                  "    pm.expect(buckets, \"kind=\" + item.kind).to.include(item.fiscal_bucket);",
                  "  });",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/billing/invoice",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/billing/invoice",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "billing",
                "invoice"
              ]
            },
            "description": "La factura. La ventana la fija el SERVIDOR — sin query params responde el mes UTC en curso; el único mando es ?period=YYYY-MM y un from/to en el query-string es 400 (core#465, MT-BIL-05 MT.4). CSV vía ?format=csv.\n\nOJO con el origen de las líneas, que NO siempre es el mismo (core#473, MT-BIL-05 MT.5d): si el periodo pedido tiene una factura EMITIDA, se sirven sus filas CONGELADAS tal cual quedaron al cierre; sólo cuando no la hay se cae al read-model vivo, que es el que comparte line items con summary. Es decir, para un mes cerrado esta ruta y /v1/billing/summary pueden diferir a propósito: la factura es el documento, el summary es la lectura de hoy."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: line_items[]\", function () { pm.expect(j.line_items).to.be.an(\"array\"); });",
                  "pm.test(\"shape: period resuelto por el servidor\", function () { pm.expect(j.period).to.have.property(\"from\"); pm.expect(j.period).to.have.property(\"to\"); });",
                  "// macro-tasks#448 MT-D: el cubo fiscal ES publico tambien aqui — la rama",
                  "// VIVA de esta ruta pasa por el MISMO BillingSummary.for_api/1 que",
                  "// /billing/summary (v1_router.ex), y ADR #73 decision 6 la nombra a mano.",
                  "//",
                  "// Se discrimina por `source` y NO se afirma a ciegas: cuando hay documento",
                  "// EMITIDO esta ruta sirve Render.frozen_invoice/1, cuyas lineas llevan",
                  "// `line_type` y NO llevan `fiscal_bucket` — el documento congelado no se",
                  "// retro-clasifica (ADR #73 decision 5). Afirmarlo alli seria exigirle a la",
                  "// factura emitida un campo que su contrato no promete.",
                  "pm.test(\"la rama VIVA publica su fiscal_bucket; la CONGELADA no lo promete\", function () {",
                  "  if (j.source === \"frozen_invoice\") {",
                  "    pm.expect(j.line_items[0] || {}, \"documento congelado\").to.have.property(\"line_type\");",
                  "    return;",
                  "  }",
                  "  const buckets = [\"saas\", \"telecom\", \"pass_through\", \"tax\"];",
                  "  pm.expect(j.line_items).to.not.be.empty;",
                  "  j.line_items.forEach(function (item) {",
                  "    pm.expect(buckets, \"kind=\" + item.kind).to.include(item.fiscal_bucket);",
                  "  });",
                  "})"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/billing/tier",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/billing/tier",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "billing",
                "tier"
              ]
            },
            "description": "El tier en vigor y el cambio de tier PENDIENTE, si lo hay (core#998, MT-BIL-08.4, ADR #130). `tier` es lo que el tenant tiene AHORA: los derechos de producto leen el tier vivo, así que un upgrade los concede al instante. `pending_change` es null cuando no hay nada pedido — eso es una respuesta, no un hueco, y por eso no es 404 (mismo criterio que /v1/billing/account). Cuando lo hay, dice a qué tier factura el mes en curso y desde qué día entra el precio nuevo, porque un cliente que pulsa «mejorar plan» y no ve cambiar su factura concluye que no funcionó."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: identidad y tier vivo\", function () {",
                  "    pm.expect(j.tenant_id).to.be.a(\"string\");",
                  "    pm.expect(j.tier).to.be.a(\"string\");",
                  "});",
                  "// `pending_change` es nulable por contrato: se afirma la forma SOLO cuando",
                  "// viene, para no convertir en rojo un 200 valido de un tenant sin nada pedido.",
                  "pm.test(\"shape: pendiente nulo, o tier + fecha de efecto\", function () {",
                  "    if (j.pending_change === null) return;",
                  "    const p = j.pending_change;",
                  "    pm.expect(p.requested_tier).to.be.a(\"string\");",
                  "    pm.expect(p.billing_tier).to.be.a(\"string\");",
                  "    // Dinero como STRING: un numeric serializado a float pierde centavos.",
                  "    pm.expect(p.billing_price_usd).to.be.a(\"string\");",
                  "    pm.expect(p.effective_from).to.match(/^\\d{4}-\\d{2}-\\d{2}$/);",
                  "    pm.expect(p.period_end).to.match(/^\\d{4}-\\d{2}-\\d{2}$/);",
                  "    // El precio nuevo entra el dia SIGUIENTE al ultimo del periodo.",
                  "    const dayAfter = new Date(p.period_end + \"T00:00:00Z\");",
                  "    dayAfter.setUTCDate(dayAfter.getUTCDate() + 1);",
                  "    pm.expect(p.effective_from).to.eql(dayAfter.toISOString().slice(0, 10));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/billing/tiers",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/billing/tiers",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "billing",
                "tiers"
              ]
            },
            "description": "El catálogo VENDIBLE con su precio, leído ANTES de subir (core#1093, ADR #142): cada tier en venta con su precio de catálogo por asiento y mes, su moneda y sus derechos; `current` marca el tier vivo y `upgrade` es la misma regla que aplica POST /v1/billing/tier/upgrade, evaluada sin escribir. Existe para que la confirmación de una subida (dashboard#316) muestre un precio servido por core y no uno escrito en el front. Lo leen la llave dd_ del tenant y un login account_admin; un login member es 403 role_forbidden y un ddw_ 403 forbidden. Lectura pura: no muta el tenant compartido del golden-flow."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: identidad, tier vivo y lista\", function () {",
                  "    pm.expect(j.tenant_id).to.be.a(\"string\");",
                  "    pm.expect(j.tier).to.be.a(\"string\");",
                  "    pm.expect(j.tiers).to.be.an(\"array\").that.is.not.empty;",
                  "    // Nulable por contrato: null solo si el tier vivo no esta en el catalogo.",
                  "    if (j.current_price_usd !== null) pm.expect(j.current_price_usd).to.be.a(\"string\");",
                  "});",
                  "pm.test(\"cada tier: precio string, derechos y flags booleanos\", function () {",
                  "    j.tiers.forEach(function (t) {",
                  "        pm.expect(t.tier, \"tier\").to.be.a(\"string\");",
                  "        // Dinero como STRING: un numeric serializado a float pierde centavos.",
                  "        pm.expect(t.monthly_price_usd, t.tier).to.be.a(\"string\");",
                  "        pm.expect(t.currency, t.tier).to.eql(\"USD\");",
                  "        pm.expect(t.entitlements, t.tier).to.be.an(\"object\");",
                  "        pm.expect(t.current, t.tier).to.be.a(\"boolean\");",
                  "        pm.expect(t.upgrade, t.tier).to.be.a(\"boolean\");",
                  "        // El tier vivo nunca es una subida de si mismo.",
                  "        if (t.current) pm.expect(t.upgrade, t.tier).to.eql(false);",
                  "    });",
                  "});",
                  "pm.test(\"ordenado por precio, de menor a mayor\", function () {",
                  "    const prices = j.tiers.map(function (t) { return parseFloat(t.monthly_price_usd); });",
                  "    for (let k = 1; k < prices.length; k++) pm.expect(prices[k]).to.be.at.least(prices[k - 1]);",
                  "});",
                  "pm.test(\"a lo sumo un tier current, y es el vivo\", function () {",
                  "    const cur = j.tiers.filter(function (t) { return t.current; });",
                  "    pm.expect(cur.length).to.be.at.most(1);",
                  "    if (cur.length === 1) pm.expect(cur[0].tier).to.eql(j.tier);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/billing/tier/upgrade — tier inexistente: 422 determinista",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"tier\": \"{{tierUpgradeUnknown}}\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/v1/billing/tier/upgrade",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "billing",
                "tier",
                "upgrade"
              ]
            },
            "description": "Autoservicio de upgrade de tier (core#998, MT-BIL-08.4, ADR #130): los derechos entran al instante y el precio en el siguiente cierre de periodo. NO emite cargo de ajuste prorrateado — decisión del founder en macro-tasks#158 (comentario 5563628225, 2026-09-07): el exceso intra-mes se cobra por las líneas de overage que ya existen.\n\nSe ejerce con un tier INEXISTENTE a propósito. newman corre la colección entera EN ORDEN sobre el tenant que siembra el golden-flow: una subida REAL le movería el tier —y con él sus cupos de asientos, minutos y DIDs— bajo todos los requests que vienen después, así que el resultado de esta colección pasaría a depender del orden de lectura. El 422 `unknown_tier` es determinista y no muta nada, y la aserción anti-catch-all comprueba que la ruta EXISTE: un path no registrado cae en el `match _` del router y contesta 404 `no such route`, no un 422 con código de dominio."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Un tier que NO puede existir en el vocabulario de `tenants.tier` ni en el",
                  "// catalogo. El sufijo aleatorio impide que una siembra futura lo haga valido",
                  "// por accidente y convierta este 422 en un 200 que SI mueve el tier del tenant",
                  "// compartido — que es justo el modo en que este request dejaria de ser inocuo.",
                  "pm.collectionVariables.set(",
                  "    \"tierUpgradeUnknown\",",
                  "    \"__not_a_tier__\" + Math.random().toString(36).slice(2, 10)",
                  ");"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 422\", function () { pm.response.to.have.status(422); });",
                  "const j = pm.response.json();",
                  "// ANTI-CATCH-ALL: la ruta tiene que EXISTIR. Un path no registrado cae en el",
                  "// `match _` del router y contesta 404 `no such route`; que esto sea un 422 con",
                  "// codigo de DOMINIO es lo que prueba que el verbo esta montado de verdad.",
                  "pm.test(\"la ruta existe: 422 de dominio, no catch-all\", function () {",
                  "    pm.expect(j.error.code).to.eql(\"unknown_tier\");",
                  "    pm.expect(JSON.stringify(j)).to.not.include(\"no such route\");",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "13 · reports",
      "description": "Artefactos de compliance empaquetados (cadena de custodia x-artifact-sha256 en la variante CSV).",
      "item": [
        {
          "name": "GET /v1/reports/violations-prevented",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/reports/violations-prevented",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "reports",
                "violations-prevented"
              ]
            },
            "description": "Violaciones PREVENIDAS (conteo real sobre gate_blocks, nunca estimado). Unidad de todo conteo: totals.unit = deduplicated_gate_block_days (dias-con-bloqueo deduplicados, NO llamadas bloqueadas); el CSV la declara en las filas unit/unit_description del preambulo. CSV vía ?format=csv."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: period presente\", function () { pm.expect(j.period).to.be.an(\"object\"); });",
                  "pm.test(\"unidad declarada: dias-con-bloqueo deduplicados, no llamadas\", function () { pm.expect(j.totals.unit).to.eql(\"deduplicated_gate_block_days\"); pm.expect(j.totals.unit_description).to.include(\"NOT one blocked call\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/reports/attempts",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/reports/attempts?from=2026-09-01T00:00:00Z&to=2026-09-17T00:00:00Z&format=csv",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "reports",
                "attempts"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "2026-09-01T00:00:00Z"
                },
                {
                  "key": "to",
                  "value": "2026-09-17T00:00:00Z"
                },
                {
                  "key": "format",
                  "value": "csv"
                }
              ]
            },
            "description": "Export CSV de intentos por rango (≤ 31 días, `from`/`to` obligatorios): una fila por intento con su disposición FINAL y `source` (`platform|cms_import`), para que el 7-en-7 de un CMS externo no diverja del nuestro. Reanudación con `?after=<export_cursor>`."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", () => pm.response.to.have.status(200));",
                  "pm.test(\"content-type text/csv\", () => pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"text/csv\"));",
                  "pm.test(\"x-artifact-sha256: sha256 hex del artefacto\", () => pm.expect(pm.response.headers.get(\"X-Artifact-SHA256\")).to.match(/^[0-9a-f]{64}$/));",
                  "pm.test(\"cabecera: las 10 columnas del contrato\", () => {",
                  "  const header = pm.response.text().split(/\\r?\\n/)[0];",
                  "  pm.expect(header).to.eql(\"debt_key,attempt_at,channel,source,outcome,agent_id,call_id,export_cursor,disclosure_status,disclosure_script_match\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/report-catalog",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/report-catalog",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "report-catalog"
              ]
            },
            "description": "Vocabulario CERRADO del constructor de reportes (core#1964): dimensiones, metricas, detail_columns y limites. Solo describe; no toca datos y responde aunque report_builder_enabled este apagada."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: fact, version, dimensions, metrics, detail_columns, limits\", function () {",
                  "  pm.expect(j.fact).to.eql(\"call_attempts\");",
                  "  pm.expect(j.version).to.be.a(\"number\");",
                  "  pm.expect(j.dimensions).to.be.an(\"array\").that.is.not.empty;",
                  "  pm.expect(j.metrics.map(function (m) { return m.key; })).to.include(\"attempts\");",
                  "  pm.expect(j.detail_columns).to.be.an(\"array\");",
                  "  pm.expect(j.limits).to.include({ max_window_days: 31, max_groups: 1000, max_dimensions: 4, max_filter_values: 100, max_detail_rows: 1000 });",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/reports/run",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/reports/run",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "reports",
                "run"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"from\": \"{{report_run_from}}\",\n  \"to\": \"{{report_run_to}}\",\n  \"metrics\": [\"attempts\"]\n}"
            },
            "description": "Un agregado del constructor sobre una ventana de 1 dia: totales de intentos. Bandera report_builder_enabled: encendida (default) 200 con filas; apagada 503 report_builder_disabled."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const now = Date.now();",
                  "pm.collectionVariables.set(\"report_run_from\", new Date(now - 24 * 3600 * 1000).toISOString());",
                  "pm.collectionVariables.set(\"report_run_to\", new Date(now).toISOString());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200 o 503\", function () { pm.expect(pm.response.code, pm.response.text()).to.be.oneOf([200, 503]); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape coherente con la bandera\", function () {",
                  "  if (pm.response.code === 200) {",
                  "    pm.expect(j.fact).to.eql(\"call_attempts\");",
                  "    pm.expect(j.rows).to.be.an(\"array\");",
                  "    pm.expect(j.row_count).to.eql(j.rows.length);",
                  "    pm.expect(j.columns.map(function (c) { return c.key; })).to.include(\"attempts\");",
                  "  } else {",
                  "    pm.expect(j.error.code).to.eql(\"report_builder_disabled\");",
                  "  }",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/reports/run campo fuera del vocabulario (tenant_id)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/reports/run",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "reports",
                "run"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"from\": \"2026-10-01T00:00:00Z\",\n  \"to\": \"2026-10-02T00:00:00Z\",\n  \"metrics\": [\"attempts\"],\n  \"tenant_id\": \"{{ghost_uuid}}\"\n}"
            },
            "description": "El tenant es el del principal: un tenant_id en el cuerpo es 422 unknown_field. Con el constructor apagado para el tenant, 503 report_builder_disabled (la bandera se evalua antes que la validacion)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 422 o 503\", function () { pm.expect(pm.response.code, pm.response.text()).to.be.oneOf([422, 503]); });",
                  "pm.test(\"error.code coherente con la bandera\", function () { pm.expect(pm.response.json().error.code).to.eql(pm.response.code === 422 ? \"unknown_field\" : \"report_builder_disabled\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/report-definitions",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/report-definitions",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "report-definitions"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"{{report_definition_name}}\",\n  \"dimensions\": [\"campaign_id\"],\n  \"metrics\": [\"attempts\", \"contacts\"]\n}"
            },
            "description": "Guarda una consulta sobre el catalogo cerrado (nunca su resultado). El nombre es unico por tenant; fact y catalog_version los sella el servidor; con llave dd_ created_by es null."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set(\"report_definition_name\", \"postman \" + pm.variables.replaceIn(\"{{$guid}}\"));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(201); });",
                  "const d = pm.response.json().report_definition;",
                  "pm.test(\"shape: definicion guardada\", function () {",
                  "  pm.expect(d.id).to.be.a(\"string\");",
                  "  pm.expect(d.name).to.eql(pm.collectionVariables.get(\"report_definition_name\"));",
                  "  pm.expect(d.fact).to.eql(\"call_attempts\");",
                  "  pm.expect(d.metrics).to.eql([\"attempts\", \"contacts\"]);",
                  "  pm.expect(d.created_by).to.eql(null);",
                  "});",
                  "pm.collectionVariables.set(\"report_definition_id\", d.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/report-definitions nombre repetido (409)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/report-definitions",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "report-definitions"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"{{report_definition_name}}\",\n  \"metrics\": [\"attempts\"]\n}"
            },
            "description": "El nombre es unico por tenant: repetirlo es 409 name_taken."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 409\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(409); });",
                  "pm.test(\"error.code name_taken\", function () { pm.expect(pm.response.json().error.code).to.eql(\"name_taken\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/report-definitions tenant_id en el cuerpo (422)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/report-definitions",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "report-definitions"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"postman fuera del catalogo\",\n  \"metrics\": [\"attempts\"],\n  \"tenant_id\": \"{{ghost_uuid}}\"\n}"
            },
            "description": "El tenant y el autor son los del principal: un tenant_id en el cuerpo es 422 unknown_field."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 422\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(422); });",
                  "pm.test(\"error.code unknown_field\", function () { pm.expect(pm.response.json().error.code).to.eql(\"unknown_field\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/report-definitions",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/report-definitions",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "report-definitions"
              ]
            },
            "description": "Las definiciones guardadas del tenant, por nombre. No depende de report_builder_enabled (solo ejecutar depende)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(200); });",
                  "const l = pm.response.json().report_definitions;",
                  "pm.test(\"shape: lista con la definicion recien guardada\", function () {",
                  "  pm.expect(l).to.be.an(\"array\");",
                  "  pm.expect(l.map(function (d) { return d.id; })).to.include(pm.collectionVariables.get(\"report_definition_id\"));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/report-definitions/{id}",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/report-definitions/{{report_definition_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "report-definitions",
                "{{report_definition_id}}"
              ]
            },
            "description": "Una definicion guardada del tenant."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(200); });",
                  "const d = pm.response.json().report_definition;",
                  "pm.test(\"shape: la misma definicion\", function () {",
                  "  pm.expect(d.id).to.eql(pm.collectionVariables.get(\"report_definition_id\"));",
                  "  pm.expect(d.dimensions).to.eql([\"campaign_id\"]);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/report-definitions/{id}",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/report-definitions/{{report_definition_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "report-definitions",
                "{{report_definition_id}}"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"{{report_definition_name}}\",\n  \"dimensions\": [],\n  \"metrics\": [\"attempts\"]\n}"
            },
            "description": "Reemplazo completo de name, dimensions, metrics y filters, validado como la creacion; id, created_by y created_at no cambian."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(200); });",
                  "const d = pm.response.json().report_definition;",
                  "pm.test(\"shape: reemplazada, id intacto\", function () {",
                  "  pm.expect(d.id).to.eql(pm.collectionVariables.get(\"report_definition_id\"));",
                  "  pm.expect(d.dimensions).to.eql([]);",
                  "  pm.expect(d.metrics).to.eql([\"attempts\"]);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/report-definitions/{id}",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/report-definitions/{{report_definition_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "report-definitions",
                "{{report_definition_id}}"
              ]
            },
            "description": "Borra la definicion guardada: 204 sin cuerpo."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 204\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(204); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/report-definitions/{id} tras borrar (404)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/report-definitions/{{report_definition_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "report-definitions",
                "{{report_definition_id}}"
              ]
            },
            "description": "Una definicion borrada (o de otro tenant) es 404 not_found."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(404); });",
                  "pm.test(\"error.code not_found\", function () { pm.expect(pm.response.json().error.code).to.eql(\"not_found\"); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "14 · dids",
      "description": "Registro de números propios del tenant + salud computada del CDR. e164 inmutable; DELETE retira, nunca borra.",
      "item": [
        {
          "name": "POST /v1/dids",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/dids",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "dids"
              ]
            },
            "description": "Alta (NPA derivado del NANP; número aleatorio por corrida para evitar el 409 por duplicado).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"e164\": \"{{postman_did_e164}}\",\n  \"us_state\": \"IL\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const nxx = 200 + Math.floor(Math.random() * 800);",
                  "const line = String(Math.floor(Math.random() * 10000)).padStart(4, \"0\");",
                  "pm.collectionVariables.set(\"postman_did_e164\", \"+1312\" + nxx + line);"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: did activo con npa derivado\", function () {",
                  "  pm.expect(j.did.id).to.be.a(\"string\");",
                  "  pm.expect(j.did.npa).to.eql(\"312\");",
                  "  pm.expect(j.did.status).to.eql(\"active\");",
                  "});",
                  "pm.collectionVariables.set(\"did_id\", j.did.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/dids",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/dids",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "dids"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: dids[]\", function () { pm.expect(j.dids).to.be.an(\"array\").and.to.have.length.above(0); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/dids/:id",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/dids/{{did_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "dids",
                "{{did_id}}"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: did.id coincide\", function () {",
                  "  pm.expect(j.did.id).to.eql(pm.collectionVariables.get(\"did_id\"));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/dids/:id",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/dids/{{did_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "dids",
                "{{did_id}}"
              ]
            },
            "description": "Metadata/status (e164 inmutable tras el alta; attestation se acepta e ignora, core#2415).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"attestation\": \"A\",\n  \"labels\": [\n    \"postman\"\n  ],\n  \"notes\": \"postman smoke\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "// core#2415 (F-03): la atestación la fija sólo el admin (POST /v1/admin/tenants/:id/dids/:did_id/verify); el PATCH del tenant acepta la clave y la IGNORA, así que el DID sigue en su valor de alta («unknown») aunque el cuerpo diga «A».",
                  "pm.test(\"shape: attestation ignored (unknown)\", function () { pm.expect(j.did.attestation).to.eql(\"unknown\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/dids/:id/health",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/dids/{{did_id}}/health",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "dids",
                "{{did_id}}",
                "health"
              ]
            },
            "description": "Salud computada del CDR (dials, answer rate, short-call rate, serie diaria)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: objeto de salud\", function () { pm.expect(j).to.be.an(\"object\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/dids/:id/launch-checklist",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/dids/{{did_id}}/launch-checklist",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "dids",
                "{{did_id}}",
                "launch-checklist"
              ]
            },
            "description": "Checklist de lanzamiento: cada paso declara su fuente (derived = registro/CDR, manual = confirmación del operador)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: cuatro pasos, cada uno con su fuente\", function () {",
                  "  pm.expect(j.steps.map(function (s) { return s.step + \":\" + s.source; })).to.eql([\"registration:manual\", \"cnam:manual\", \"attestation:derived\", \"warmup:derived\"]);",
                  "  pm.expect(j.complete).to.eql(false);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/dids/:id/launch-checklist/:step/confirmations",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/dids/{{did_id}}/launch-checklist/{{did_launch_step}}/confirmations",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "dids",
                "{{did_id}}",
                "launch-checklist",
                "{{did_launch_step}}",
                "confirmations"
              ]
            },
            "description": "Confirma un paso MANUAL (cnam); el último registro manda y ninguno se edita ni se borra.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"confirmed\": true,\n  \"note\": \"CNAM fijado en el carrier\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set(\"did_launch_step\", \"cnam\");"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: cnam manual y hecho\", function () {",
                  "  const s = j.steps.find(function (x) { return x.step === \"cnam\"; });",
                  "  pm.expect(s.source).to.eql(\"manual\");",
                  "  pm.expect(s.state).to.eql(\"done\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/dids/:id/launch-checklist/:step/confirmations (paso derivado)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/dids/{{did_id}}/launch-checklist/{{did_launch_step}}/confirmations",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "dids",
                "{{did_id}}",
                "launch-checklist",
                "{{did_launch_step}}",
                "confirmations"
              ]
            },
            "description": "Un paso derivado no se confirma a mano: 422 del endpoint real (no catch-all).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"confirmed\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set(\"did_launch_step\", \"attestation\");"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 422\", function () { pm.response.to.have.status(422); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: unprocessable del endpoint real\", function () {",
                  "  pm.expect(j.error.code).to.eql(\"unprocessable\");",
                  "  pm.expect(j.error.message).to.not.include(\"no such route\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/dids/:id/carrier-tickets",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/dids/{{did_id}}/carrier-tickets",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "dids",
                "{{did_id}}",
                "carrier-tickets"
              ]
            },
            "description": "Abre un ticket con el carrier sobre el número (hilo append-only).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"carrier\": \"Carrier de prueba\",\n  \"subject\": \"Etiquetado como spam\",\n  \"reference\": \"T-1\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: ticket abierto con su evento inicial\", function () {",
                  "  pm.expect(j.ticket.id).to.be.a(\"string\");",
                  "  pm.expect(j.ticket.status).to.eql(\"open\");",
                  "  pm.expect(j.ticket.events.length).to.eql(1);",
                  "});",
                  "pm.collectionVariables.set(\"did_ticket_id\", j.ticket.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/dids/:id/carrier-tickets/:ticket_id/events",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/dids/{{did_id}}/carrier-tickets/{{did_ticket_id}}/events",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "dids",
                "{{did_id}}",
                "carrier-tickets",
                "{{did_ticket_id}}",
                "events"
              ]
            },
            "description": "Añade una nota y mueve el estado del ticket; el hilo conserva todos los eventos.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"status\": \"resolved\",\n  \"note\": \"Re-etiquetado por el carrier\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: resuelto con dos eventos en el hilo\", function () {",
                  "  pm.expect(j.ticket.status).to.eql(\"resolved\");",
                  "  pm.expect(j.ticket.events.length).to.eql(2);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/dids/:id/carrier-tickets",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/dids/{{did_id}}/carrier-tickets",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "dids",
                "{{did_id}}",
                "carrier-tickets"
              ]
            },
            "description": "Tickets del número, el más reciente primero, cada uno con su hilo."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: el ticket abierto está resuelto\", function () {",
                  "  const t = j.tickets.find(function (x) { return x.id === pm.collectionVariables.get(\"did_ticket_id\"); });",
                  "  pm.expect(t.status).to.eql(\"resolved\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/dids/:id",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/dids/{{did_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "dids",
                "{{did_id}}"
              ]
            },
            "description": "Retira (el registro nunca pierde filas: la proveniencia from_number del CDR debe seguir resolviendo)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: retired\", function () { pm.expect(j.did.status).to.eql(\"retired\"); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "15 · carriers",
      "description": "Trunks SIP de la sede (account_id/tenant_id JAMÁS del body; scope empresa = compartido).",
      "item": [
        {
          "name": "POST /v1/carriers",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/carriers",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "carriers"
              ]
            },
            "description": "Alta BYOC per-sede (scope por defecto).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"kind\": \"byoc\",\n  \"name\": \"postman-trunk-{{$timestamp}}\",\n  \"sip_proxy\": \"sip:pstn.example.test:5060\",\n  \"source_ips\": [\n    \"{{postman_trunk_ip}}\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// El IP del troncal se deriva por corrida. La unicidad de source_ips NO es",
                  "// un detalle: es la guarda anti-hijack cross-tenant de MT-SEC-04, y es GLOBAL",
                  "// entre cuentas. Con un literal fijo, la segunda corrida contra el mismo stack",
                  "// moria en 400 'source_ips already assigned to another trunk' (docs#67).",
                  "// Espacio: 11.0.0.0/8, ~16,6 M de combinaciones, fuera de las clases que rechaza",
                  "// core#2424 (SourceIpClass): CGNAT, privadas y documentacion responden 422.",
                  "const rnd = (n) => Math.floor(Math.random() * n);",
                  "pm.variables.set('postman_trunk_ip', '11.' + rnd(256) + '.' + rnd(256) + '.' + (1 + rnd(254)));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: carrier byoc scope sede\", function () {",
                  "  pm.expect(j.carrier.id).to.be.a(\"string\");",
                  "  pm.expect(j.carrier.kind).to.eql(\"byoc\");",
                  "});",
                  "pm.collectionVariables.set(\"carrier_id\", j.carrier.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/carriers",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/carriers",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "carriers"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: carriers[]\", function () { pm.expect(j.carriers).to.be.an(\"array\").and.to.have.length.above(0); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/carriers/:id",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/carriers/{{carrier_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "carriers",
                "{{carrier_id}}"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: carrier.id coincide\", function () {",
                  "  pm.expect(j.carrier.id).to.eql(pm.collectionVariables.get(\"carrier_id\"));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/carriers/:id/health",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/carriers/{{carrier_id}}/health",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "carriers",
                "{{carrier_id}}",
                "health"
              ]
            },
            "description": "La troncal BYOC recién creada aún no tiene muestras: el contrato publica unknown/no_data, no un up optimista."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: salud sin muestras\", function () {",
                  "  pm.expect(j.carrier_id).to.eql(pm.collectionVariables.get(\"carrier_id\"));",
                  "  pm.expect(j.state).to.eql(\"unknown\");",
                  "  pm.expect(j.reason).to.eql(\"no_data\");",
                  "  pm.expect(j.since).to.eql(null);",
                  "  pm.expect(j.observed_at).to.eql(null);",
                  "  pm.expect(j.fresh).to.eql(false);",
                  "  pm.expect(j.signals).to.eql(null);",
                  "  pm.expect(j).to.not.have.property(\"sip_proxy\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/carriers/:id/health/events",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/carriers/{{carrier_id}}/health/events",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "carriers",
                "{{carrier_id}}",
                "health",
                "events"
              ]
            },
            "description": "Historial keyset de una troncal nueva: todavía no hay transiciones ni cursor siguiente."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: events[] y next_cursor nulo\", function () {",
                  "  pm.expect(j.events).to.eql([]);",
                  "  pm.expect(j.next_cursor).to.eql(null);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/carriers/:id/health (404 id desconocido)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/carriers/{{unknown_carrier_id}}/health",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "carriers",
                "{{unknown_carrier_id}}",
                "health"
              ]
            },
            "description": "Caso documentado: un UUID desconocido devuelve el envelope not_found del endpoint, no el catch-all."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.variables.set(\"unknown_carrier_id\", \"00000000-0000-7000-8000-000000000000\");"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "const e = pm.response.json().error;",
                  "pm.test(\"envelope not_found del endpoint\", function () {",
                  "  pm.expect(e.code).to.eql(\"not_found\");",
                  "  pm.expect(e.message).to.not.eql(\"no such route\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/carriers/:id",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/carriers/{{carrier_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "carriers",
                "{{carrier_id}}"
              ]
            },
            "description": "Lista blanca de destinos de la troncal BYOC (core#1281, ADR #172 de core): se reemplaza entera. La peticion siguiente la quita, para que la troncal de la corrida no restrinja nada de lo que venga despues en la coleccion.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"allowed_destinations\": [\n    \"+13125550100\",\n    \"+13125550101\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: la troncal devuelve su lista tal cual\", function () {",
                  "  pm.expect(j.carrier.id).to.eql(pm.collectionVariables.get(\"carrier_id\"));",
                  "  pm.expect(j.carrier.allowed_destinations).to.eql([\"+13125550100\", \"+13125550101\"]);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/carriers/:id (quita la lista)",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/carriers/{{carrier_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "carriers",
                "{{carrier_id}}"
              ]
            },
            "description": "null quita la lista (core#1281): la troncal vuelve a no restringir destinos.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"allowed_destinations\": null\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: sin lista = []\", function () {",
                  "  pm.expect(j.carrier.allowed_destinations).to.eql([]);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/carriers/:id/provision",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/carriers/{{carrier_id}}/provision",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "carriers",
                "{{carrier_id}}",
                "provision"
              ]
            },
            "description": "Empuja el trunk al SIP edge. `sbc_disabled` (sin KAMAILIO_RPC_URL) es 200 con flag explícito, no error."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: provisioned|sbc_disabled\", function () {",
                  "  pm.expect([\"provisioned\", \"sbc_disabled\"]).to.include(j.status);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/carriers/:id/revoke",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/carriers/{{carrier_id}}/revoke",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "carriers",
                "{{carrier_id}}",
                "revoke"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: provisioned|sbc_disabled\", function () {",
                  "  pm.expect([\"provisioned\", \"sbc_disabled\"]).to.include(j.status);",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "16 · inbound-routes",
      "description": "Rutas DID entrantes (un número suena en UN solo lugar: e164 activo globalmente único → 409).",
      "item": [
        {
          "name": "POST /v1/inbound-routes",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-routes",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-routes"
              ]
            },
            "description": "Alta con destino cola (dest_type agent requiere dest_ref; ivr requiere un menú vigente en dest_ref desde core#1319 y se prueba en la carpeta 37, donde nace el menú). Número aleatorio por corrida.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"e164\": \"{{postman_inbound_e164}}\",\n  \"dest_type\": \"queue\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const nxx = 200 + Math.floor(Math.random() * 800);",
                  "const line = String(Math.floor(Math.random() * 10000)).padStart(4, \"0\");",
                  "const e164 = \"+1773\" + nxx + line;",
                  "pm.collectionVariables.set(\"postman_inbound_e164\", e164);",
                  "// core#2414/#2429: una ruta activa exige un DID activo del tenant; se da de alta antes por la API.",
                  "pm.sendRequest({",
                  "  url: pm.variables.get(\"baseUrl\") + \"/v1/dids\",",
                  "  method: \"POST\",",
                  "  header: { \"Content-Type\": \"application/json\", \"Authorization\": \"Bearer \" + pm.variables.get(\"api_token\") },",
                  "  body: { mode: \"raw\", raw: JSON.stringify({ e164: e164, us_state: \"IL\" }) }",
                  "}, function (err, res) {",
                  "  pm.test(\"alta previa del DID de la ruta -> 201\", function () { pm.expect(err, \"sendRequest\").to.equal(null); pm.expect(res.code, \"POST /v1/dids\").to.equal(201); });",
                  "});"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: route activa\", function () {",
                  "  pm.expect(j.inbound_route.id).to.be.a(\"string\");",
                  "  pm.expect(j.inbound_route.status).to.eql(\"active\");",
                  "});",
                  "pm.collectionVariables.set(\"inbound_route_id\", j.inbound_route.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/inbound-routes (número que no es del tenant -> 422)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-routes",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-routes"
              ]
            },
            "description": "Control negativo de core#2414/#2429: la llave inbound_route_requires_did sigue en true, así que una ruta activa sobre un número que NO está en dids del tenant se rechaza. El número se genera sin darlo de alta.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"e164\": \"{{postman_inbound_foreign_e164}}\",\n  \"dest_type\": \"queue\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const nxx = 200 + Math.floor(Math.random() * 800);",
                  "const line = String(Math.floor(Math.random() * 10000)).padStart(4, \"0\");",
                  "pm.collectionVariables.set(\"postman_inbound_foreign_e164\", \"+1775\" + nxx + line);"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 422\", function () { pm.response.to.have.status(422); });",
                  "pm.test(\"shape: unprocessable, nombra que no es un número de la cuenta\", function () {",
                  "  const e = pm.response.json().error;",
                  "  pm.expect(e.code).to.eql(\"unprocessable\");",
                  "  pm.expect(e.message).to.include(\"not a number of this account\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/inbound-routes",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-routes",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-routes"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: inbound_routes[]\", function () {",
                  "  pm.expect(j.inbound_routes).to.be.an(\"array\").and.to.have.length.above(0);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/inbound-routes/:id",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-routes/{{inbound_route_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-routes",
                "{{inbound_route_id}}"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: route.id coincide\", function () {",
                  "  pm.expect(j.inbound_route.id).to.eql(pm.collectionVariables.get(\"inbound_route_id\"));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/inbound-routes/:id",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-routes/{{inbound_route_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-routes",
                "{{inbound_route_id}}"
              ]
            },
            "description": "Update de destino/flags (e164 inmutable).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"recording\": false\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: recording false\", function () { pm.expect(j.inbound_route.recording).to.eql(false); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/inbound-routes/:id/provision",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-routes/{{inbound_route_id}}/provision",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-routes",
                "{{inbound_route_id}}",
                "provision"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: provisioned|sbc_disabled\", function () {",
                  "  pm.expect([\"provisioned\", \"sbc_disabled\"]).to.include(j.status);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/inbound-routes/:id/disable",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-routes/{{inbound_route_id}}/disable",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-routes",
                "{{inbound_route_id}}",
                "disable"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: disabled\", function () { pm.expect(j.inbound_route.status).to.eql(\"disabled\"); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "17 · sms-providers",
      "description": "Registro de proveedores SMS por tenant/región (molde carriers: account_id/tenant_id JAMÁS del body; scope empresa = compartido). El webhook_secret es write-only: las respuestas exponen has_secret, nunca el secreto. Retire-only (sin DELETE).",
      "item": [
        {
          "name": "POST /v1/sms/providers",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/sms/providers",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "sms",
                "providers"
              ]
            },
            "description": "Alta per-sede (scope por defecto). El secreto viaja solo en el request.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"adapter\": \"http\",\n  \"region\": \"us\",\n  \"name\": \"postman-sms-{{$timestamp}}\",\n  \"base_url\": \"\",\n  \"webhook_secret\": \"{{$guid}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: sms_provider http scope sede\", function () {",
                  "  pm.expect(j.sms_provider.id).to.be.a(\"string\");",
                  "  pm.expect(j.sms_provider.adapter).to.eql(\"http\");",
                  "  pm.expect(j.sms_provider.scope).to.eql(\"sede\");",
                  "});",
                  "pm.test(\"secreto write-only: has_secret sin webhook_secret\", function () {",
                  "  pm.expect(j.sms_provider.has_secret).to.eql(true);",
                  "  pm.expect(j.sms_provider).to.not.have.property(\"webhook_secret\");",
                  "});",
                  "pm.collectionVariables.set(\"sms_provider_id\", j.sms_provider.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/sms/providers",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/sms/providers",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "sms",
                "providers"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: sms_providers[] sin secretos\", function () {",
                  "  pm.expect(j.sms_providers).to.be.an(\"array\").and.to.have.length.above(0);",
                  "  j.sms_providers.forEach(p => pm.expect(p).to.not.have.property(\"webhook_secret\"));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/sms/providers/:id",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/sms/providers/{{sms_provider_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "sms",
                "providers",
                "{{sms_provider_id}}"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: sms_provider.id coincide\", function () {",
                  "  pm.expect(j.sms_provider.id).to.eql(pm.collectionVariables.get(\"sms_provider_id\"));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/sms/providers/:id",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/sms/providers/{{sms_provider_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "sms",
                "providers",
                "{{sms_provider_id}}"
              ]
            },
            "description": "Config/credenciales/status; el scope es inmutable tras el alta.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"postman-sms-renamed\",\n  \"status\": \"disabled\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: patch aplicado, secreto intacto\", function () {",
                  "  pm.expect(j.sms_provider.name).to.eql(\"postman-sms-renamed\");",
                  "  pm.expect(j.sms_provider.status).to.eql(\"disabled\");",
                  "  pm.expect(j.sms_provider.has_secret).to.eql(true);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/sms/providers/:id/retire",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/sms/providers/{{sms_provider_id}}/retire",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "sms",
                "providers",
                "{{sms_provider_id}}",
                "retire"
              ]
            },
            "description": "Retiro terminal — la fila nunca se borra (sin grant DELETE)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: status retired\", function () {",
                  "  pm.expect(j.sms_provider.status).to.eql(\"retired\");",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "18 · sms-messages",
      "description": "Espejo de /v1/sms/messages (T24): el choke point Dialer.Sms.Sender. El POST prueba el contrato FAIL-CLOSED contra el stack real: se registra un provider activo (el mock del harness) y aun así el envío a un destino SIN consentimiento es 422 compliance_blocked/tcpa_no_consent con decision_id citable — cero llamadas al provider, cero filas de ledger. El ciclo 202 completo (accept→DLR→STOP→metering) vive en tests/smoke/test_sms_e2e.py.",
      "item": [
        {
          "name": "0. alta provider mock (para el send)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/sms/providers",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "sms",
                "providers"
              ]
            },
            "description": "Provider activo del harness (mocks/sms-provider) para que el POST de mensajes NO caiga en 422 sms_provider_unconfigured: lo que se prueba es la cadena de gates, no la resolución. Retirado al final de la carpeta.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"adapter\": \"http\",\n  \"region\": \"us\",\n  \"name\": \"postman-sms-send-{{$timestamp}}\",\n  \"base_url\": \"\",\n  \"webhook_secret\": \"{{$guid}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.collectionVariables.set(\"sms_send_provider_id\", j.sms_provider.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/sms/messages",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/sms/messages",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "sms",
                "messages"
              ]
            },
            "description": "UN envío gated. Canal SMS: TODO destino exige consentimiento previo (fail-closed, line_type irrelevante) y este número no lo tiene ⇒ 422 compliance_blocked / gate tcpa_no_consent con decision_id del audit chain. Cero tráfico al provider, cero filas sms_messages/ledger.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"to\": \"+19995550123\",\n  \"from\": \"+13125550199\",\n  \"body\": \"postman probe. Responda STOP para no recibir mas mensajes.\",\n  \"client_ref\": \"postman-{{$guid}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 422 (fail-closed sin consentimiento)\", function () {",
                  "  pm.response.to.have.status(422);",
                  "});",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: compliance_blocked + gate + decision_id citable\", function () {",
                  "  pm.expect(j.error.code).to.eql(\"compliance_blocked\");",
                  "  pm.expect(j.error.gate).to.eql(\"tcpa_no_consent\");",
                  "  pm.expect(j.error.decision_id).to.be.a(\"string\").and.to.not.be.empty;",
                  "});",
                  "// El decision_id que acaba de salir por el 422 es la CITA del cliente. Se",
                  "// guarda para resolverla de verdad en la carpeta 30: sin esto habria que",
                  "// inventarse un id, y un id inventado solo puede ejercitar el 404.",
                  "pm.collectionVariables.set(\"decision_id\", j.error.decision_id);"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/sms/messages",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/sms/messages?limit=20",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "sms",
                "messages"
              ]
            },
            "description": "Ledger de mensajes (originales + transiciones compensatorias, keyset-paginado). En esta corrida el envío bloqueado NO dejó filas: el shape es lo verificable."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: sms_messages[] + next_cursor\", function () {",
                  "  pm.expect(j.sms_messages).to.be.an(\"array\");",
                  "  pm.expect(j).to.have.property(\"next_cursor\");",
                  "});",
                  "pm.test(\"listas llevan body_preview, nunca body completo\", function () {",
                  "  j.sms_messages.forEach(function (m) {",
                  "    pm.expect(m).to.have.property(\"body_preview\");",
                  "    pm.expect(m).to.not.have.property(\"body\");",
                  "  });",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/sms/messages/:id",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/sms/messages/{{sms_message_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "sms",
                "messages",
                "{{sms_message_id}}"
              ]
            },
            "description": "Vista COLAPSADA de un mensaje (status actual + timestamps + snapshot + transiciones). 404 real del endpoint (id aleatorio sembrado en pre-request) porque el flujo de esta colección no produce mensajes."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404 (id inexistente)\", function () {",
                  "  pm.response.to.have.status(404);",
                  "});",
                  "pm.test(\"404 del endpoint real (no catch-all 'no such route')\", function () {",
                  "  pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "});"
                ]
              }
            },
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// La colección no siembra sms_message_id (el POST de arriba es un 422",
                  "// fail-closed a propósito): con la variable vacía la URL caería en el",
                  "// catch-all del router SIN ejercitar la ruta real — UUID aleatorio.",
                  "if (!pm.collectionVariables.get(\"sms_message_id\")) {",
                  "  pm.collectionVariables.set(\"sms_message_id\", pm.variables.replaceIn(\"{{$guid}}\"));",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "9. retire provider mock (limpieza)",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/sms/providers/{{sms_send_provider_id}}/retire",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "sms",
                "providers",
                "{{sms_send_provider_id}}",
                "retire"
              ]
            },
            "description": "Deja al tenant demo sin provider activo (el estado con el que empezó la corrida): la colección es re-ejecutable sin acumular filas activas."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"retired\", function () {",
                  "  pm.expect(pm.response.json().sms_provider.status).to.eql(\"retired\");",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "19 · calls (CTI)",
      "item": [
        {
          "name": "GET /v1/calls",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/calls",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "calls"
              ]
            },
            "description": "Superficie de LECTURA del CTI (la mitad que el WidgetAllowlist pre-autoriza para ddw_): las mismas filas CDR que /v1/call_attempts, clavadas por call_uuid para los verbos de control. Con dd_ lista el piso completo del tenant."
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: calls[] + next_cursor presente\", () => {",
                  "  pm.expect(j.calls).to.be.an(\"array\");",
                  "  pm.expect(j).to.have.property(\"next_cursor\");",
                  "});",
                  "pm.test(\"cada fila trae id + call_uuid (la clave de los verbos)\", () => {",
                  "  j.calls.forEach(c => { pm.expect(c).to.have.property(\"id\"); pm.expect(c).to.have.property(\"call_uuid\"); });",
                  "});",
                  "// Captura un call_uuid REAL para el GET por-id determinista de abajo.",
                  "const withUuid = (j.calls || []).find(c => c.call_uuid);",
                  "if (withUuid) { pm.collectionVariables.set(\"probe_call_uuid\", withUuid.call_uuid); }",
                  "else { pm.collectionVariables.unset(\"probe_call_uuid\"); }"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/calls?live=true",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/calls?live=true",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "calls"
              ],
              "query": [
                {
                  "key": "live",
                  "value": "true"
                }
              ]
            },
            "description": "Solo llamadas EN VUELO: ended_at null y solo originales (una correccion es post-llamada por definicion). Aserta la semantica del filtro sin depender de cuantas filas haya."
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test(\"todas las filas estan en vuelo (ended_at null, corrects_id null)\", () => {",
                  "  j.calls.forEach(c => { pm.expect(c.ended_at).to.eql(null); pm.expect(c.corrects_id).to.eql(null); });",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/calls?live=banana — booleano estricto: 400",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/calls?live=banana",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "calls"
              ],
              "query": [
                {
                  "key": "live",
                  "value": "banana"
                }
              ]
            },
            "description": "Un live malformado jamas ensancha en silencio una lectura \"solo en vuelo\" al CDR completo: booleano estricto o 400."
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"400 bad_request\", () => {",
                  "  pm.expect(pm.response.code).to.eql(400);",
                  "  pm.expect(pm.response.json().error.code).to.eql(\"bad_request\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/calls/:call_id (fila capturada de la lista)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/calls/{{probe_call_uuid}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "calls",
                "{{probe_call_uuid}}"
              ]
            },
            "description": "Lee por call_uuid una fila REAL capturada de GET /v1/calls (determinista en el stack sembrado). El read clava la fila ORIGINAL (corrects_id null). Si la lista estuviera vacía, un uuid ausente da 404 — tolerado, nunca 500."
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.collectionVariables.get(\"probe_call_uuid\")) { pm.collectionVariables.set(\"probe_call_uuid\", \"00000000-0000-7000-8000-0000000000ff\"); }"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const probed = pm.collectionVariables.get(\"probe_call_uuid\");",
                  "if (probed) {",
                  "  pm.test(\"status 200 (la fila capturada se lee por su call_uuid)\", () => pm.response.to.have.status(200));",
                  "  if (pm.response.code === 200) {",
                  "    const j = pm.response.json();",
                  "    pm.test(\"call.call_uuid coincide y es la fila ORIGINAL (corrects_id null)\", () => {",
                  "      pm.expect(j.call.call_uuid).to.eql(probed);",
                  "      pm.expect(j.call.corrects_id).to.eql(null);",
                  "    });",
                  "  }",
                  "} else {",
                  "  pm.test(\"sin fila que capturar -> 404 determinista (nunca 500/catch-all)\", () => {",
                  "    pm.expect(pm.response.code).to.eql(404);",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/calls/:call_id — uuid desconocido: 404 determinista",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/calls/{{unknown_call_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "calls",
                "{{unknown_call_id}}"
              ]
            },
            "description": "Sonda determinista: un uuid bien formado que no existe en el tenant es SIEMPRE 404 (tenant_id explicito + RLS fail-closed). No depende del golden call."
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.variables.set(\"unknown_call_id\", \"00000000-0000-7000-8000-000000000000\");"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"404 determinista para uuid desconocido\", function () {",
                  "  pm.expect(pm.response.code).to.eql(404);",
                  "});",
                  "pm.test(\"envelope not_found del endpoint real (no catch-all)\", function () {",
                  "  const e = pm.response.json().error;",
                  "  pm.expect(e.code).to.eql(\"not_found\");",
                  "  pm.expect(e.message).to.not.eql(\"no such route\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/calls — sin purpose: 400 determinista",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/calls",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "calls"
              ]
            },
            "description": "Llamada saliente por API de desarrollador (core#2848, ADR #352). `purpose` es obligatorio y no tiene valor por defecto: sin él la petición es un 400 antes de tocar ninguna compuerta, y no sale ninguna llamada.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"to\": \"+13125550100\",\n  \"text\": \"Probe.\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", () => pm.response.to.have.status(400));",
                  "const j = pm.response.json();",
                  "pm.test(\"bad_request que nombra purpose\", () => {",
                  "  pm.expect(j.error.code).to.eql(\"bad_request\");",
                  "  pm.expect(j.error.message).to.include(\"purpose\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/calls — destino sin consentimiento: rehusada, nunca 202",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/calls",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "calls"
              ]
            },
            "description": "Un destino sin consentimiento en fichero (ficticio, 555-01xx) NO se marca: la cadena de voz entera corre y rehusa (`422 compliance_blocked`, con `gate`, `rule_id` y `decision_id`), o el tenant del harness aun no tiene lo que la ruta exige ANTES del originador (`422 disclosure_missing`, `422 optout_instructions_missing`, `422 caller_id_unavailable`) o la plataforma no puede sintetizar el audio, dirigir la salida o arrancar la llamada (`503 prompt_unavailable`, `503 egress_unavailable`, `503 dial_unavailable`: nada salio, reintentable), o un freno del originador, que corre ANTES de los gates, la para (`402` de plan o saldo, `403 synthetic_tenant`, `422 no_rate`, `429`, `502 dial_error`). Lo que NUNCA puede ser es un 202: el consentimiento solo entra por `PUT /v1/consents/{phone}`, y esta ruta no lo lleva en el cuerpo.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"to\": \"+12025550173\",\n  \"purpose\": \"transactional\",\n  \"text\": \"Probe.\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"nunca 202: sin consentimiento no sale la llamada\", () => pm.expect(pm.response.code).to.not.eql(202));",
                  "const j = pm.response.json();",
                  "const refusals = {402: [\"plan_inactive\", \"wallet_insufficient\", \"plan_exhausted\", \"exposure_limit\"], 403: [\"synthetic_tenant\"], 422: [\"compliance_blocked\", \"no_rate\", \"disclosure_missing\", \"optout_instructions_missing\", \"caller_id_unavailable\"], 502: [\"dial_error\"], 503: [\"prompt_unavailable\", \"egress_unavailable\", \"dial_unavailable\"]};",
                  "pm.test(\"rehusada con una respuesta del contrato de createCall (nada salio)\", () => {",
                  "  pm.expect([402, 403, 422, 429, 502, 503]).to.include(pm.response.code);",
                  "  if (pm.response.code !== 429) { pm.expect(refusals[pm.response.code]).to.include(j.error.code); }",
                  "});",
                  "if (j.error.code === \"compliance_blocked\") {",
                  "  pm.test(\"el bloqueo cita su decision\", () => {",
                  "    pm.expect(j.error).to.have.property(\"gate\");",
                  "    pm.expect(j.error).to.have.property(\"decision_id\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/calls/:call_id/hangup — uuid desconocido: 404 determinista",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.variables.set(\"unknown_call_id\", \"00000000-0000-7000-8000-000000000000\");"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"404 determinista para uuid desconocido\", function () {",
                  "  pm.expect(pm.response.code).to.eql(404);",
                  "});",
                  "pm.test(\"envelope not_found del endpoint real (no catch-all)\", function () {",
                  "  const e = pm.response.json().error;",
                  "  pm.expect(e.code).to.eql(\"not_found\");",
                  "  pm.expect(e.message).to.not.eql(\"no such route\");",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/calls/{{unknown_call_id}}/hangup",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "calls",
                "{{unknown_call_id}}",
                "hangup"
              ]
            },
            "description": "Sonda determinista: un uuid bien formado que no existe en el tenant es SIEMPRE 404 (RLS fail-closed, cero comandos ESL, fila de auditoria failed/call_not_found). No depende del golden call."
          },
          "response": []
        },
        {
          "name": "POST /v1/calls/:call_id/recording/pause — uuid desconocido: 404 (bandera apagada o llamada inexistente)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.variables.set(\"unknown_call_id\", \"00000000-0000-7000-8000-000000000000\");"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"404: sin bandera recording_pause_enabled la ruta no existe; con ella, la llamada desconocida no se encuentra\", function () { pm.expect(pm.response.code).to.eql(404); });"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/calls/{{unknown_call_id}}/recording/pause",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "calls",
                "{{unknown_call_id}}",
                "recording",
                "pause"
              ]
            },
            "description": "Espejo del gate de drift (core#2700): la mascara de la grabacion durante una toma de pago. Determinista: 404 tanto con la bandera por tenant apagada como con ella encendida y un uuid que no existe."
          },
          "response": []
        },
        {
          "name": "POST /v1/calls/:call_id/recording/resume — uuid desconocido: 404 (bandera apagada o llamada inexistente)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.variables.set(\"unknown_call_id\", \"00000000-0000-7000-8000-000000000000\");"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"404: sin bandera recording_pause_enabled la ruta no existe; con ella, la llamada desconocida no se encuentra\", function () { pm.expect(pm.response.code).to.eql(404); });"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/calls/{{unknown_call_id}}/recording/resume",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "calls",
                "{{unknown_call_id}}",
                "recording",
                "resume"
              ]
            },
            "description": "Espejo del gate de drift (core#2700): la mascara de la grabacion durante una toma de pago. Determinista: 404 tanto con la bandera por tenant apagada como con ella encendida y un uuid que no existe."
          },
          "response": []
        },
        {
          "name": "POST /v1/calls/:call_id/stream — uuid desconocido: 404 (bandera apagada o llamada inexistente)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.variables.set(\"unknown_call_id\", \"00000000-0000-7000-8000-000000000000\");"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"404: sin bandera developer_audio_stream_enabled la ruta no existe; con ella, la llamada desconocida no se encuentra\", function () { pm.expect(pm.response.code).to.eql(404); });"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/calls/{{unknown_call_id}}/stream",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "calls",
                "{{unknown_call_id}}",
                "stream"
              ]
            },
            "description": "Espejo del gate de drift (core#2862, ADR #355): abre el stream de audio de la llamada hacia el wss:// del desarrollador. Determinista: 404 tanto con la bandera por tenant apagada como con ella encendida y un uuid que no existe.",
            "body": {
              "mode": "raw",
              "raw": "{\"url\": \"wss://example.com/audio\", \"direction\": \"listen\"}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "DELETE /v1/calls/:call_id/stream — uuid desconocido: 404 (bandera apagada o llamada inexistente)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.variables.set(\"unknown_call_id\", \"00000000-0000-7000-8000-000000000000\");"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"404: sin bandera developer_audio_stream_enabled la ruta no existe; con ella, la llamada desconocida no se encuentra\", function () { pm.expect(pm.response.code).to.eql(404); });"
                ]
              }
            }
          ],
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/calls/{{unknown_call_id}}/stream",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "calls",
                "{{unknown_call_id}}",
                "stream"
              ]
            },
            "description": "Espejo del gate de drift (core#2862, ADR #355): cierra el stream de audio de la llamada. Determinista: 404 con la bandera apagada y con ella encendida y un uuid que no existe."
          },
          "response": []
        },
        {
          "name": "POST /v1/calls/:call_id/hold",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/calls/{{golden_call_id}}/hold",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "calls",
                "{{golden_call_id}}",
                "hold"
              ]
            },
            "description": "CTI verb hold sobre llamada viva (evidence-first: fila call_control_actions ANTES del ESL). Con llamada viva -> 200; sin/ajena -> 404; no bridged -> 409; unhold fuera de ventana -> 422; switch rechaza (leg muerto) -> 502; audit/ESL caido -> 503."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200|404|409|502|503 (segun haya llamada viva)\", function () {",
                  "  pm.expect([200, 404, 409, 502, 503]).to.include(pm.response.code);",
                  "});",
                  "if (pm.response.code === 200) {",
                  "  const j = pm.response.json();",
                  "  pm.test(\"shape 200: call presente\", function () { pm.expect(j.call).to.be.an(\"object\"); });",
                  "  pm.test(\"shape 200: verb/audit_id/to_number enmascarado\", function () {",
                  "    pm.expect(j.call.verb).to.eql(\"hold\");",
                  "    pm.expect(j.call.audit_id).to.match(/^[0-9a-f-]{36}$/);",
                  "    if (j.call.to_number) { pm.expect(j.call.to_number).to.match(/^\\*\\*\\*/); }",
                  "  });",
                  "}",
                  "if (pm.response.code === 404) {",
                  "  pm.test(\"404 del endpoint real (no catch-all)\", function () {",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}",
                  "if (pm.response.code === 409) {",
                  "  pm.test(\"409 not_bridged del endpoint real\", function () {",
                  "    pm.expect(pm.response.json().error.code).to.eql(\"not_bridged\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/calls/:call_id/unhold",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/calls/{{golden_call_id}}/unhold",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "calls",
                "{{golden_call_id}}",
                "unhold"
              ]
            },
            "description": "CTI verb unhold sobre llamada viva (evidence-first: fila call_control_actions ANTES del ESL). Con llamada viva -> 200; sin/ajena -> 404; no bridged -> 409; unhold fuera de ventana -> 422; switch rechaza (leg muerto) -> 502; audit/ESL caido -> 503."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200|404|409|422|502|503 (segun haya llamada viva)\", function () {",
                  "  pm.expect([200, 404, 409, 422, 502, 503]).to.include(pm.response.code);",
                  "});",
                  "if (pm.response.code === 200) {",
                  "  const j = pm.response.json();",
                  "  pm.test(\"shape 200: call presente\", function () { pm.expect(j.call).to.be.an(\"object\"); });",
                  "}",
                  "if (pm.response.code === 404) {",
                  "  pm.test(\"404 del endpoint real (no catch-all)\", function () {",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}",
                  "if (pm.response.code === 422) {",
                  "  pm.test(\"422: envelope mid_call completo\", function () {",
                  "    const e = pm.response.json().error;",
                  "    pm.expect(e.gate).to.eql(\"mid_call\");",
                  "    pm.expect([\"quiet_hours\", \"consent_revoked\", \"cease_and_desist\"]).to.include(e.reason_code);",
                  "    pm.expect(e.auto_hangup_at).to.be.a(\"string\");",
                  "  });",
                  "  pm.test(\"422 compliance_blocked mid_call\", function () {",
                  "    pm.expect(pm.response.json().error.code).to.eql(\"compliance_blocked\");",
                  "  });",
                  "}",
                  "if (pm.response.code === 409) {",
                  "  pm.test(\"409 not_bridged del endpoint real\", function () {",
                  "    pm.expect(pm.response.json().error.code).to.eql(\"not_bridged\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/calls/:call_id/mute",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/calls/{{golden_call_id}}/mute",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "calls",
                "{{golden_call_id}}",
                "mute"
              ]
            },
            "description": "CTI verb mute sobre llamada viva (evidence-first: fila call_control_actions ANTES del ESL). Con llamada viva -> 200; sin/ajena -> 404; no bridged -> 409; unhold fuera de ventana -> 422; switch rechaza (leg muerto) -> 502; audit/ESL caido -> 503."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200|404|409|502|503 (segun haya llamada viva)\", function () {",
                  "  pm.expect([200, 404, 409, 502, 503]).to.include(pm.response.code);",
                  "});",
                  "if (pm.response.code === 200) {",
                  "  const j = pm.response.json();",
                  "  pm.test(\"shape 200: call presente\", function () { pm.expect(j.call).to.be.an(\"object\"); });",
                  "}",
                  "if (pm.response.code === 404) {",
                  "  pm.test(\"404 del endpoint real (no catch-all)\", function () {",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}",
                  "if (pm.response.code === 409) {",
                  "  pm.test(\"409 not_bridged del endpoint real\", function () {",
                  "    pm.expect(pm.response.json().error.code).to.eql(\"not_bridged\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/calls/:call_id/unmute",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/calls/{{golden_call_id}}/unmute",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "calls",
                "{{golden_call_id}}",
                "unmute"
              ]
            },
            "description": "CTI verb unmute sobre llamada viva (evidence-first: fila call_control_actions ANTES del ESL). Con llamada viva -> 200; sin/ajena -> 404; no bridged -> 409; unhold fuera de ventana -> 422; switch rechaza (leg muerto) -> 502; audit/ESL caido -> 503."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200|404|409|502|503 (segun haya llamada viva)\", function () {",
                  "  pm.expect([200, 404, 409, 502, 503]).to.include(pm.response.code);",
                  "});",
                  "if (pm.response.code === 200) {",
                  "  const j = pm.response.json();",
                  "  pm.test(\"shape 200: call presente\", function () { pm.expect(j.call).to.be.an(\"object\"); });",
                  "}",
                  "if (pm.response.code === 404) {",
                  "  pm.test(\"404 del endpoint real (no catch-all)\", function () {",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}",
                  "if (pm.response.code === 409) {",
                  "  pm.test(\"409 not_bridged del endpoint real\", function () {",
                  "    pm.expect(pm.response.json().error.code).to.eql(\"not_bridged\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/calls/:call_id/transfer — destino nacional sin E.164",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/calls/{{golden_call_id}}/transfer",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "calls",
                "{{golden_call_id}}",
                "transfer"
              ]
            },
            "description": "Transferencia ciega con el bearer dd_ de la coleccion. Un destino con forma de numero nacional, pero sin prefijo E.164, se rechaza 422 destination_not_supported, salvo que el re-chequeo GATE_MID_CALL frene antes con 422 compliance_blocked (el mismo orden que en unhold). Si no hay fila para el call_id responde 404; el contrato fail-closed tambien contempla 429 y 503 antes de tocar telefonia.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"destination\": \"2025550100\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404|422|429|503 (segun siga viva la llamada)\", function () {",
                  "  pm.expect([404, 422, 429, 503]).to.include(pm.response.code);",
                  "});",
                  "if (pm.response.code === 404) {",
                  "  pm.test(\"404 del endpoint real (no catch-all)\", function () {",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}",
                  "if (pm.response.code === 422) {",
                  "  pm.test(\"422: destination_not_supported, o compliance_blocked si el gate mid_call frena antes\", function () {",
                  "    const e = pm.response.json().error;",
                  "    if (e.code === \"compliance_blocked\") { pm.expect(e.gate).to.eql(\"mid_call\"); }",
                  "    else { pm.expect(e.code).to.eql(\"destination_not_supported\"); }",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/calls/:call_id/transfer/attended/start — destino externo no soportado",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/calls/{{golden_call_id}}/transfer/attended/start",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "calls",
                "{{golden_call_id}}",
                "transfer",
                "attended",
                "start"
              ]
            },
            "description": "La consulta atendida es solo interna. Con llamada viva, este destino nacional sin agente ni E.164 fija el 422 destination_not_supported. Con el canal ya muerto la fila de intento sigue existiendo, asi que la lectura de la pierna puede responder 502 call_control_failed (o 503 si ESL no contesta); el 404 queda para un call_id sin fila. El contrato contempla ademas 409 por consulta abierta y 429 por el freno.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"destination\": \"2025550100\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404|409|422|429|502|503 (segun siga viva la llamada)\", function () {",
                  "  pm.expect([404, 409, 422, 429, 502, 503]).to.include(pm.response.code);",
                  "});",
                  "if (pm.response.code === 404) {",
                  "  pm.test(\"404 del endpoint real (no catch-all)\", function () {",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}",
                  "if (pm.response.code === 409) {",
                  "  pm.test(\"409 de la maquina de consulta\", function () {",
                  "    pm.expect([\"consultation_in_progress\", \"not_bridged\"]).to.include(pm.response.json().error.code);",
                  "  });",
                  "}",
                  "if (pm.response.code === 422) {",
                  "  pm.test(\"422 destination_not_supported para transferencia atendida externa\", function () {",
                  "    pm.expect(pm.response.json().error.code).to.eql(\"destination_not_supported\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/calls/:call_id/transfer/attended/complete",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/calls/{{golden_call_id}}/transfer/attended/complete",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "calls",
                "{{golden_call_id}}",
                "transfer",
                "attended",
                "complete"
              ]
            },
            "description": "Completa la consulta abierta sin aceptar destination en el body: la maquina usa el destino guardado por start. En esta corrida no se abre una consulta valida, por lo que responde 409 no_consultation haya muerto ya el canal o no: consultation_guard corta antes de tocar la pierna. El 404 solo aparece si no hay fila de intento para ese call_id. El contrato tambien contempla 422 por el gate mid_call, 429 y 503 fail-closed."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404|409|422|429|503 (sin consulta abierta)\", function () {",
                  "  pm.expect([404, 409, 422, 429, 503]).to.include(pm.response.code);",
                  "});",
                  "if (pm.response.code === 404) {",
                  "  pm.test(\"404 del endpoint real (no catch-all)\", function () {",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}",
                  "if (pm.response.code === 409) {",
                  "  pm.test(\"409 no_consultation\", function () {",
                  "    pm.expect(pm.response.json().error.code).to.eql(\"no_consultation\");",
                  "  });",
                  "}",
                  "if (pm.response.code === 422) {",
                  "  pm.test(\"422 compliance_blocked mid_call\", function () {",
                  "    const e = pm.response.json().error;",
                  "    pm.expect(e.code).to.eql(\"compliance_blocked\");",
                  "    pm.expect(e.gate).to.eql(\"mid_call\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/calls/:call_id/transfer/attended/cancel",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/calls/{{golden_call_id}}/transfer/attended/cancel",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "calls",
                "{{golden_call_id}}",
                "transfer",
                "attended",
                "cancel"
              ]
            },
            "description": "Cancela la consulta abierta sin body ni handoff. En esta corrida no se abre una consulta valida, por lo que responde 409 no_consultation haya muerto ya el canal o no: consultation_guard corta antes de tocar la pierna. El 404 solo aparece si no hay fila de intento para ese call_id. El contrato tambien contempla 422 por el gate mid_call, 429 y 503 fail-closed."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404|409|422|429|503 (sin consulta abierta)\", function () {",
                  "  pm.expect([404, 409, 422, 429, 503]).to.include(pm.response.code);",
                  "});",
                  "if (pm.response.code === 404) {",
                  "  pm.test(\"404 del endpoint real (no catch-all)\", function () {",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}",
                  "if (pm.response.code === 409) {",
                  "  pm.test(\"409 de la maquina de consulta\", function () {",
                  "    pm.expect([\"no_consultation\", \"not_bridged\"]).to.include(pm.response.json().error.code);",
                  "  });",
                  "}",
                  "if (pm.response.code === 422) {",
                  "  pm.test(\"422 compliance_blocked mid_call\", function () {",
                  "    const e = pm.response.json().error;",
                  "    pm.expect(e.code).to.eql(\"compliance_blocked\");",
                  "    pm.expect(e.gate).to.eql(\"mid_call\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/calls/:call_id/hangup",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/calls/{{golden_call_id}}/hangup",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "calls",
                "{{golden_call_id}}",
                "hangup"
              ]
            },
            "description": "CTI verb hangup sobre llamada viva (evidence-first: fila call_control_actions ANTES del ESL). Con llamada viva -> 200; sin/ajena -> 404; no bridged -> 409; unhold fuera de ventana -> 422; switch rechaza (leg muerto) -> 502; audit/ESL caido -> 503."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200|404|502|503 (segun haya llamada viva)\", function () {",
                  "  pm.expect([200, 404, 502, 503]).to.include(pm.response.code);",
                  "});",
                  "if (pm.response.code === 200) {",
                  "  const j = pm.response.json();",
                  "  pm.test(\"shape 200: call presente\", function () { pm.expect(j.call).to.be.an(\"object\"); });",
                  "}",
                  "if (pm.response.code === 404) {",
                  "  pm.test(\"404 del endpoint real (no catch-all)\", function () {",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "20 · widget",
      "description": "CTI-4: lifecycle del widget embebible. Gestión (mint/list/revoke tokens + origins CRUD) es dd_; renew/introspect son ddw_-self (con el flag OFF, un dd_ -> 401). ddw_ nunca alcanza esta gestión (default-deny).",
      "item": [
        {
          "name": "GET /v1/widget_origins",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/widget_origins",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "widget_origins"
              ]
            },
            "description": "Lista de orígenes allowlisted del tenant (habilitados + deshabilitados)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: widget_origins[]\", () => pm.expect(j.widget_origins).to.be.an(\"array\"));"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/widget_origins",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/widget_origins",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "widget_origins"
              ]
            },
            "description": "Agrega un origen (normalizado a forma canónica). Captura id + origen canónico.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"origin\": \"https://w{{$timestamp}}.example.com\",\n  \"label\": \"postman-widget\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", () => pm.response.to.have.status(201));",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: widget_origin canónico (sin tenant_id/hash)\", () => {",
                  "  pm.expect(j.widget_origin.id).to.be.a(\"string\");",
                  "  pm.expect(j.widget_origin.origin).to.match(/^https:\\/\\//);",
                  "  pm.expect(j.widget_origin).to.not.have.property(\"tenant_id\");",
                  "});",
                  "pm.collectionVariables.set(\"widget_origin_id\", j.widget_origin.id);",
                  "pm.collectionVariables.set(\"widget_origin_canonical\", j.widget_origin.origin);"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/widget_origins (duplicado -> 409)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/widget_origins",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "widget_origins"
              ]
            },
            "description": "Re-add del mismo origen canónico -> 409 UNIQUE(tenant_id,origin).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"origin\": \"{{widget_origin_canonical}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Reenvía el origen CANÓNICO capturado en OTRA grafía (mayúsculas) para",
                  "// probar normalize-then-conflict sobre UNIQUE(tenant_id,origin) sin flakear.",
                  "pm.test(\"status 409 (normalize-then-conflict)\", () => pm.response.to.have.status(409));"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/widget_origins/:id/enable",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/widget_origins/{{widget_origin_id}}/enable",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "widget_origins",
                "{{widget_origin_id}}",
                "enable"
              ]
            },
            "description": "Re-habilita el origen (idempotente sobre uno ya habilitado)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: disabled_at null (habilitado)\", () => pm.expect(j.widget_origin.disabled_at).to.be.null);"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/widget_origins/:id (disable)",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/widget_origins/{{widget_origin_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "widget_origins",
                "{{widget_origin_id}}"
              ]
            },
            "description": "Deshabilita (soft) el origen — los orígenes nunca se borran."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: disabled_at presente (deshabilitado)\", () => pm.expect(j.widget_origin.disabled_at).to.not.be.null);"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/widget_tokens",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/widget_tokens",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "widget_tokens"
              ]
            },
            "description": "Mint de un ddw_ para el agente golden. Plaintext UNA vez; TTLs server-side.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{golden_agent_id}}\",\n  \"label\": \"postman-widget\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", () => pm.response.to.have.status(201));",
                  "const j = pm.response.json();",
                  "pm.test(\"plaintext ddw_ una vez; sin hash\", () => {",
                  "  pm.expect(j.token).to.be.a(\"string\").and.to.match(/^ddw_/);",
                  "  pm.expect(j.widget_token).to.not.have.property(\"token_hash\");",
                  "  pm.expect(j.widget_token.agent_id).to.eql(pm.collectionVariables.get(\"golden_agent_id\"));",
                  "});",
                  "pm.collectionVariables.set(\"widget_token_id\", j.widget_token.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/widget_tokens",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/widget_tokens",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "widget_tokens"
              ]
            },
            "description": "Lista de tokens (jamás el hash)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test(\"lista sin token_hash\", () => {",
                  "  pm.expect(j.widget_tokens).to.be.an(\"array\");",
                  "  j.widget_tokens.forEach((t) => pm.expect(t).to.not.have.property(\"token_hash\"));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/widget_tokens/:id",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/widget_tokens/{{widget_token_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "widget_tokens",
                "{{widget_token_id}}"
              ]
            },
            "description": "Revoca el token recién minteado (idempotente)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200 (revoca; idempotente)\", () => pm.response.to.have.status(200));"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/widget_tokens/:id (fantasma -> 404)",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/widget_tokens/{{ghost_uuid}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "widget_tokens",
                "{{ghost_uuid}}"
              ]
            },
            "description": "Revoca un id inexistente -> 404 (mismo 404 que uno cross-tenant)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// id inexistente vía VARIABLE (nunca literal — el normalizador de drift",
                  "// solo colapsa segmentos {{var}}). 404 determinista, sin oráculo de existencia.",
                  "pm.test(\"status 404\", () => pm.response.to.have.status(404));"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/widget_tokens/renew (dd_ sin sesión -> 401)",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/widget_tokens/renew",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "widget_tokens",
                "renew"
              ]
            },
            "description": "Self-renew: un dd_ no porta sesión de widget -> 401 (dark)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Con WIDGET_AUTH_ENABLED=OFF (dark) y bajo el bearer dd_ de la colección,",
                  "// no hay :widget_token_id -> 401 estable e invariante al flag.",
                  "pm.test(\"status 401\", () => pm.response.to.have.status(401));",
                  "pm.test(\"code unauthorized\", () => pm.expect(pm.response.json().error.code).to.eql(\"unauthorized\"));"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/widget_tokens/introspect (dd_ sin sesión -> 401)",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/widget_tokens/introspect",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "widget_tokens",
                "introspect"
              ]
            },
            "description": "Self-introspect: un dd_ no porta sesión de widget -> 401 (dark)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 401\", () => pm.response.to.have.status(401));"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/widget_tokens/sip_credential (dd_ sin sesión -> 401)",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/widget_tokens/sip_credential",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "widget_tokens",
                "sip_credential"
              ]
            },
            "description": "La credencial SIP efímera del frame (MT-CTI-07 fase 2). ddw_-self: el token_id sale del bearer, así que un dd_ no porta sesión de widget -> 401 (dark)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 401\", () => pm.response.to.have.status(401));",
                  "pm.test(\"code unauthorized\", () => pm.expect(pm.response.json().error.code).to.eql(\"unauthorized\"));",
                  "// Anti-catch-all: el 401 sale del handler, no del 404 del router.",
                  "pm.test(\"no es el catch-all\", () => pm.expect(pm.response.text()).to.not.include(\"no such route\"));"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/widget_tokens/sip_credential (dd_ sin sesión -> 401)",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/widget_tokens/sip_credential",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "widget_tokens",
                "sip_credential"
              ]
            },
            "description": "El frame DEVUELVE su credencial efímera sin terminar la sesión. Fija además el ORDEN del router: este literal no puede caer en el comodín delete /v1/widget_tokens/:id."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 401\", () => pm.response.to.have.status(401));",
                  "pm.test(\"code unauthorized\", () => pm.expect(pm.response.json().error.code).to.eql(\"unauthorized\"));",
                  "// EL ORDEN DEL ROUTER, fijado desde fuera: si este literal cayera en",
                  "// el comodín `delete /v1/widget_tokens/:id`, este bearer dd_ pasaría",
                  "// with_tenant_scope/2 y with_uuid/3 trataría \"sip_credential\" como id",
                  "// no-UUID -> 404 not_found, no el 401 de sesión ausente.",
                  "pm.test(\"no cae en el comodín :id\", () => pm.expect(pm.response.code).to.not.eql(404));",
                  "pm.test(\"no es el catch-all\", () => pm.expect(pm.response.text()).to.not.include(\"no such route\"));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "21 · users+auth",
      "description": "Usuarios del dashboard (email+password) y sesiones ddu_ (login/logout). Orden dependiente: crear -> login -> me -> rotar password -> disable/enable -> logout idempotente.",
      "item": [
        {
          "name": "POST /v1/users (usuario: invitacion, 202 fijo)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/users",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "users"
              ]
            },
            "description": "Alta con la dd_ (bootstrap). Desde el ADR #182 de core (core#1352) el alta INVITA: no lleva `password`, contesta un `202` con un cuerpo FIJO —el mismo, byte a byte, este la direccion libre o sea ya de otro tenant— y la persona no existe hasta que canjea el enlace del correo. El email se fija en el pre-request porque la respuesta ya no lo devuelve.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"{{postman_user_email}}\",\n  \"name\": \"Newman User\",\n  \"locale\": \"es\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set(\"postman_user_email\", \"newman-\" + Date.now() + \"@e2e.test\");",
                  "pm.collectionVariables.set(\"postman_user_id\", \"\");",
                  "pm.collectionVariables.set(\"invite_token\", \"\");"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 202\", function () { pm.response.to.have.status(202); });",
                  "pm.test(\"cuerpo FIJO de la invitacion, byte a byte (ADR #182 de core)\", function () {",
                  "  pm.expect(pm.response.text()).to.eql(\"{\\\"message\\\":\\\"invitation recorded\\\",\\\"status\\\":\\\"invited\\\"}\");",
                  "});",
                  "pm.test(\"no devuelve usuario ni material secreto\", function () {",
                  "  pm.expect(pm.response.json()).to.not.have.property(\"user\");",
                  "  pm.expect(pm.response.text()).to.not.include(\"ddr_\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/users (password en el cuerpo -> 400)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/users",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "users"
              ]
            },
            "description": "La contrasena la elige la persona invitada al canjear; mandarla en el alta es un `400` (ADR #182 de core), nunca un alta silenciosa que la ignore.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"newman-pw-{{$timestamp}}@e2e.test\",\n  \"password\": \"kT7-quartz-vexil-29\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "pm.test(\"sobre de error, sin invitacion\", function () {",
                  "  pm.expect(pm.response.json().error.code).to.be.a(\"string\");",
                  "  pm.expect(pm.response.text()).to.not.include(\"invitation recorded\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/users (usuario: invitacion abierta, la persona aun no existe)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/users",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "users"
              ]
            },
            "description": "La invitacion se lista en `invitations` como `pending`; en `users` no hay nadie con ese email hasta el canje. Nunca expone el hash del token."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "const email = pm.collectionVariables.get(\"postman_user_email\");",
                  "pm.test(\"invitacion pending con la forma del contrato\", function () {",
                  "  const inv = j.invitations.find(i => i.email === email);",
                  "  pm.expect(inv, \"invitacion abierta para \" + email).to.be.an(\"object\");",
                  "  pm.expect(inv.status).to.eql(\"pending\");",
                  "  pm.expect(inv).to.include.all.keys(\"id\", \"email\", \"status\", \"expires_at\", \"created_at\");",
                  "  pm.expect(JSON.stringify(j)).to.not.include(\"token_hash\");",
                  "});",
                  "pm.test(\"la persona NO existe antes del canje\", function () {",
                  "  pm.expect(j.users.map(u => u.email)).to.not.include(email);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/auth/password-reset/confirm (usuario: canje de la invitacion, nace la persona)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/password-reset/confirm",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "password-reset",
                "confirm"
              ]
            },
            "description": "SIN bearer. El canje de una invitacion va por la MISMA ruta anonima que el restablecimiento (ADR #182 de core): la persona nace aqui, con la contrasena que elige ella.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"token\": \"{{invite_token}}\",\n  \"new_password\": \"kT7-quartz-vexil-29\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: {status: ok}\", function () {",
                  "  pm.expect(pm.response.json().status).to.eql(\"ok\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/users (usuario: la persona existe, la invitacion salio de la lista)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/users",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "users"
              ]
            },
            "description": "Tras el canje la persona aparece en `users` (activa) y su invitacion deja `invitations`."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "const email = pm.collectionVariables.get(\"postman_user_email\");",
                  "const mine = j.users.find(u => u.email === email);",
                  "pm.test(\"la persona invitada ya es usuaria activa\", function () {",
                  "  pm.expect(mine, \"usuario \" + email).to.be.an(\"object\");",
                  "  pm.expect(mine.status).to.eql(\"active\");",
                  "  pm.expect(JSON.stringify(j)).to.not.include(\"password_hash\");",
                  "});",
                  "pm.test(\"la invitacion canjeada ya no esta abierta\", function () {",
                  "  pm.expect(j.invitations.map(i => i.email)).to.not.include(email);",
                  "});",
                  "if (mine) { pm.collectionVariables.set(\"postman_user_id\", mine.id); }"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/users (direccion ya usuaria de ESTE tenant -> 409)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/users",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "users"
              ]
            },
            "description": "La unica rama visible al tenant que invita: la direccion ya esta en SU `GET /v1/users`, asi que decirlo no es un oraculo (ADR #182 de core, punto 2).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"{{postman_user_email}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 409\", function () { pm.response.to.have.status(409); });",
                  "pm.test(\"sobre conflict\", function () {",
                  "  pm.expect(pm.response.json().error.code).to.eql(\"conflict\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/auth/login",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/login",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "login"
              ]
            },
            "description": "SIN bearer: esta llamada lo acuna. El plaintext ddu_ se muestra UNA vez.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"{{postman_user_email}}\",\n  \"password\": \"kT7-quartz-vexil-29\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: token ddu_ + session + user + tenant\", function () {",
                  "  pm.expect(j.token).to.match(/^ddu_/);",
                  "  pm.expect(j.session.user_id).to.eql(pm.collectionVariables.get(\"postman_user_id\"));",
                  "  pm.expect(j.user.email).to.eql(pm.collectionVariables.get(\"postman_user_email\"));",
                  "  pm.expect(j.tenant.id).to.be.a(\"string\");",
                  "  pm.expect(JSON.stringify(j)).to.not.include(\"token_hash\");",
                  "});",
                  "pm.collectionVariables.set(\"ddu_token\", j.token);"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/auth/login (password malo -> 401 uniforme)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/login",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "login"
              ]
            },
            "description": "",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"{{postman_user_email}}\",\n  \"password\": \"definitely-not-the-password\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 401\", function () { pm.response.to.have.status(401); });",
                  "pm.test(\"envelope anti-enumeracion\", function () {",
                  "  const j = pm.response.json();",
                  "  pm.expect(j.error.code).to.eql(\"unauthorized\");",
                  "  pm.expect(j.error.message).to.eql(\"invalid email or password\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/me (sesion ddu_)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/me",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me"
              ]
            },
            "description": "",
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"me trae tenant Y user con la ddu_\", function () {",
                  "  pm.expect(j.tenant.id).to.be.a(\"string\");",
                  "  pm.expect(j.user.id).to.eql(pm.collectionVariables.get(\"postman_user_id\"));",
                  "});",
                  "// core#1059 (ADR #137). El INVARIANTE, no el fixture: seat_source viaja",
                  "// SIEMPRE y con uno de los cuatro valores del enum, y seat es un objeto",
                  "// si y solo si vale \"token\". En ESTE carril la persona nace SIN asiento",
                  "// ligado (nadie crea un agente con su email), asi que aqui se ejercita la",
                  "// rama seat_not_linked; la rama \"token\" se cubre en 32 - me-scorecard.",
                  "pm.test(\"seat: seat_source siempre presente y coherente con seat\", function () {",
                  "  pm.expect(j.seat_source, \"seat_source\").to.be.oneOf(",
                  "    [\"token\", \"no_identity\", \"seat_not_linked\", \"seat_gone\"]);",
                  "  if (j.seat_source === \"token\") {",
                  "    pm.expect(j.seat, \"seat\").to.be.an(\"object\");",
                  "  } else {",
                  "    pm.expect(j.seat, \"seat\").to.equal(null);",
                  "  }",
                  "});",
                  "pm.test(\"capabilities.email_channel (core#1925): booleano por tenant, aditivo a conference\", function () {",
                  "  pm.expect(j.capabilities.email_channel).to.be.a(\"boolean\");",
                  "  pm.expect(j.capabilities.conference).to.be.a(\"boolean\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/auth/sign-in-events (sesion ddu_ de un member -> 403)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/sign-in-events",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "sign-in-events"
              ]
            },
            "description": "core#1520. El rastro de inicios de sesion es SOLO del account_admin. La persona de esta carpeta nace `member` (la promocion a account_admin es solo del operador, MT-SEC-31), asi que su sesion ddu_ recibe 403 role_forbidden. El 200 con filtros y cursor se ejerce en la suite :db de core: la coleccion no tiene un account_admin (la promocion es operator-only, sin ruta /v1).",
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"403 es role_forbidden y no filtra eventos\", function () {",
                  "  pm.expect(pm.response.text()).to.include(\"role_forbidden\");",
                  "  pm.expect(pm.response.text()).to.not.include(\"occurred_at\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/screen-recordings/original-playback (sesion ddu_ de un member -> 403 role_forbidden)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/screen-recordings/original-playback",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "screen-recordings",
                "original-playback"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"call_uuid\": \"postman-call\",\n  \"reason\": \"revision de una disputa del consumidor\"\n}"
            },
            "description": "core#2124, ADR 266. El original sin tapar es SOLO del account_admin. La persona de esta carpeta nace `member` (la promocion a account_admin es solo del operador, MT-SEC-31), asi que su sesion ddu_ recibe 403 role_forbidden, sin fila de acceso ni URL. El 200 de un account_admin no se puede ejercer aqui (la coleccion no tiene un account_admin, sin ruta /v1 que lo promueva): lo cubre el e2e de pantalla redactada.",
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(403); });",
                  "pm.test(\"error.code role_forbidden y sin URL\", function () {",
                  "  pm.expect(pm.response.json().error.code).to.eql(\"role_forbidden\");",
                  "  pm.expect(pm.response.text()).to.not.include(\"/v1/screen-media/\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/auth/sign-in-events (dd_ machine key -> 403)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/sign-in-events",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "sign-in-events"
              ]
            },
            "description": "core#1520. Una llave de integracion no es una persona: 403 role_forbidden (hereda el bearer de coleccion)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"403 es role_forbidden y no filtra eventos\", function () {",
                  "  pm.expect(pm.response.text()).to.include(\"role_forbidden\");",
                  "  pm.expect(pm.response.text()).to.not.include(\"occurred_at\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/me/mfa (sesion ddu_: sin segundo factor)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/me/mfa",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me",
                "mfa"
              ]
            },
            "description": "Estado del segundo factor de la PROPIA persona (core#1457): recien creada, apagado y sin codigos de recuperacion.",
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: enabled=false, 0 codigos\", function () { pm.expect(j.enabled).to.eql(false); pm.expect(j.recovery_codes_remaining).to.eql(0); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/me/mfa (dd_ machine key -> 403)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/me/mfa",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me",
                "mfa"
              ]
            },
            "description": "La dd_ no tiene persona: 403 (hereda el bearer de coleccion)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/me/mfa/enroll (password malo -> 422)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/me/mfa/enroll",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me",
                "mfa",
                "enroll"
              ]
            },
            "description": "El alta exige la contrasena de la persona; una erronea es 422 y gasta del freno por persona (5 por ventana).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"password\": \"not-the-password\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 422\", function () { pm.response.to.have.status(422); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/me/mfa/enroll (sin password -> 400)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/me/mfa/enroll",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me",
                "mfa",
                "enroll"
              ]
            },
            "description": "Cuerpo sin `password`: 400, sin gastar freno.",
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/me/mfa/enroll (alta: secreto + otpauth://)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/me/mfa/enroll",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me",
                "mfa",
                "enroll"
              ]
            },
            "description": "Con la contrasena correcta nace un alta PENDIENTE: el secreto base32 y el URI otpauth:// salen UNA vez. No hay factor activo hasta confirmar con el primer codigo.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"password\": \"kT7-quartz-vexil-29\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: secreto base32 + URI otpauth\", function () { pm.expect(j.secret).to.match(/^[A-Z2-7]+$/); pm.expect(j.otpauth_uri).to.match(/^otpauth:\\/\\/totp\\//); pm.expect(j.otpauth_uri).to.include(j.secret); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/me/mfa/confirm (codigo malo -> 422)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/me/mfa/confirm",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me",
                "mfa",
                "confirm"
              ]
            },
            "description": "Un codigo que no es el de la ventana actual: 422 (000000 coincide por azar con probabilidad ~3e-6). No se activa nada.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"code\": \"000000\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 422\", function () { pm.response.to.have.status(422); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/me/mfa/confirm (sin code -> 400)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/me/mfa/confirm",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me",
                "mfa",
                "confirm"
              ]
            },
            "description": "Cuerpo sin `code`: 400, sin gastar freno.",
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/me/mfa/disable (sin factor activo -> 409)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/me/mfa/disable",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me",
                "mfa",
                "disable"
              ]
            },
            "description": "Retirada con contrasena correcta y un codigo, pero el alta sigue pendiente: 409 (no hay factor activo). La contrasena se comprueba antes que el codigo.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"password\": \"kT7-quartz-vexil-29\",\n  \"code\": \"123456\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 409\", function () { pm.response.to.have.status(409); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/me/mfa/recovery-codes (sin factor activo -> 409)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/me/mfa/recovery-codes",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me",
                "mfa",
                "recovery-codes"
              ]
            },
            "description": "Regenerar codigos sin factor activo: 409, tras comprobar la contrasena.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"password\": \"kT7-quartz-vexil-29\",\n  \"code\": \"123456\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 409\", function () { pm.response.to.have.status(409); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/auth/mfa/verify (desafio desconocido -> 401 uniforme)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/mfa/verify",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "mfa",
                "verify"
              ]
            },
            "description": "SIN bearer. Un desafio ddm_ desconocido, caducado o gastado es UN 401 que no nombra a nadie ni gasta el presupuesto de nadie.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"challenge\": \"ddm_postman-no-such-challenge-0000000000000000000\",\n  \"code\": \"123456\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 401\", function () { pm.response.to.have.status(401); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/auth/mfa/verify (sin challenge -> 400)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/mfa/verify",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "mfa",
                "verify"
              ]
            },
            "description": "SIN bearer. Cuerpo sin `challenge`: 400.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"code\": \"123456\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/auth/login (tenant que exige MFA y persona sin factor -> mfa_required + purpose enroll, SIN sesion)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/login",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "login"
              ]
            },
            "description": "Con la exigencia ON y sin factor activo, la contrasena correcta NO acuna sesion: devuelve un desafio ddm_ de proposito enroll que solo abre /v1/auth/mfa/enroll y /enroll/confirm.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"{{postman_user_email}}\",\n  \"password\": \"kT7-quartz-vexil-29\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"mfa_required + purpose enroll + challenge ddm_, sin sesion\", function () {",
                  "  pm.expect(j.mfa_required).to.eql(true);",
                  "  pm.expect(j.purpose).to.eql(\"enroll\");",
                  "  pm.expect(j.challenge).to.match(/^ddm_/);",
                  "  pm.expect(j).to.not.have.property(\"token\");",
                  "});",
                  "pm.collectionVariables.set(\"mfa_enroll_challenge\", j.challenge);"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/auth/mfa/enroll (sin challenge -> 400)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/mfa/enroll",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "mfa",
                "enroll"
              ]
            },
            "description": "SIN bearer. Cuerpo sin `challenge`: 400.",
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/auth/mfa/enroll (desafio desconocido -> 401 uniforme)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/mfa/enroll",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "mfa",
                "enroll"
              ]
            },
            "description": "SIN bearer. Un desafio desconocido, caducado, gastado o de otro proposito es UN mismo 401.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"challenge\": \"ddm_desconocido-newman\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 401\", function () { pm.response.to.have.status(401); });",
                  "pm.test(\"envelope uniforme\", function () { const j = pm.response.json(); pm.expect(j.error.code).to.eql(\"unauthorized\"); pm.expect(j.error.message).to.eql(\"invalid or expired challenge\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/auth/mfa/enroll (alta forzada: secreto + otpauth://)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/mfa/enroll",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "mfa",
                "enroll"
              ]
            },
            "description": "Primera mitad del alta forzada: el desafio enroll del login da un secreto TOTP PENDIENTE, mostrado UNA vez. No se pide la contrasena otra vez.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"challenge\": \"{{mfa_enroll_challenge}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: secreto base32 + URI otpauth\", function () { pm.expect(j.secret).to.match(/^[A-Z2-7]+$/); pm.expect(j.otpauth_uri).to.match(/^otpauth:\\/\\/totp\\//); pm.expect(j.otpauth_uri).to.include(j.secret); });",
                  "pm.collectionVariables.set(\"mfa_enroll_secret\", j.secret);"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/auth/mfa/enroll/confirm (sin code -> 400)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/mfa/enroll/confirm",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "mfa",
                "enroll",
                "confirm"
              ]
            },
            "description": "SIN bearer. Cuerpo sin `code`: 400; el desafio sigue vivo.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"challenge\": \"{{mfa_enroll_challenge}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/auth/mfa/enroll/confirm (codigo malo -> 401 invalid code)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/mfa/enroll/confirm",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "mfa",
                "enroll",
                "confirm"
              ]
            },
            "description": "SIN bearer. Un codigo que no es el de la ventana actual (000000 coincide por azar con probabilidad ~3e-6): 401 `invalid code`, cuenta contra los 5 intentos del desafio, no activa nada ni acuna sesion.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"challenge\": \"{{mfa_enroll_challenge}}\",\n  \"code\": \"000000\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 401\", function () { pm.response.to.have.status(401); });",
                  "pm.test(\"invalid code, sin sesion\", function () { const j = pm.response.json(); pm.expect(j.error.code).to.eql(\"unauthorized\"); pm.expect(j.error.message).to.eql(\"invalid code\"); pm.expect(j).to.not.have.property(\"token\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/auth/mfa/enroll/confirm (codigo TOTP valido: factor activo + sesion ddu_ + recovery_codes)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/mfa/enroll/confirm",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "mfa",
                "enroll",
                "confirm"
              ]
            },
            "description": "Segunda mitad: el primer codigo TOTP del secreto pendiente activa el factor, emite los recovery_codes (UNA vez) y acuna la sesion ddu_ que el login retuvo.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"challenge\": \"{{mfa_enroll_challenge}}\",\n  \"code\": \"{{mfa_enroll_code}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// TOTP RFC 6238 (SHA-1, 30 s, 6 digitos) del secreto base32 que devolvio el alta.",
                  "const b32 = pm.collectionVariables.get(\"mfa_enroll_secret\") || \"\";",
                  "const A = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567\";",
                  "let bits = \"\";",
                  "b32.replace(/=+$/, \"\").split(\"\").forEach(function (ch) { bits += (\"00000\" + A.indexOf(ch).toString(2)).slice(-5); });",
                  "let hex = \"\";",
                  "for (let i = 0; i + 8 <= bits.length; i += 8) { hex += (\"0\" + parseInt(bits.substr(i, 8), 2).toString(16)).slice(-2); }",
                  "const CJS = require(\"crypto-js\");",
                  "const counter = Math.floor(Date.now() / 30000);",
                  "const msg = (\"0000000000000000\" + counter.toString(16)).slice(-16);",
                  "const mac = CJS.HmacSHA1(CJS.enc.Hex.parse(msg), CJS.enc.Hex.parse(hex)).toString(CJS.enc.Hex);",
                  "const off = parseInt(mac.substr(mac.length - 1), 16);",
                  "const bin = parseInt(mac.substr(off * 2, 8), 16) & 0x7fffffff;",
                  "pm.collectionVariables.set(\"mfa_enroll_code\", (\"000000\" + (bin % 1000000)).slice(-6));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: token ddu_ + session + user + tenant + recovery_codes\", function () {",
                  "  pm.expect(j.token).to.match(/^ddu_/);",
                  "  pm.expect(j.session.user_id).to.eql(pm.collectionVariables.get(\"postman_user_id\"));",
                  "  pm.expect(j.user.email).to.eql(pm.collectionVariables.get(\"postman_user_email\"));",
                  "  pm.expect(j.tenant.id).to.eql(pm.collectionVariables.get(\"tenant_id\"));",
                  "  pm.expect(j.recovery_codes).to.be.an(\"array\").that.is.not.empty;",
                  "  pm.expect(JSON.stringify(j)).to.not.include(\"token_hash\");",
                  "});",
                  "pm.collectionVariables.set(\"ddu_token\", j.token);",
                  "pm.collectionVariables.set(\"mfa_enroll_recovery_code\", j.recovery_codes[0]);"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/auth/mfa/enroll/confirm (desafio ya gastado -> 401 uniforme)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/mfa/enroll/confirm",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "mfa",
                "enroll",
                "confirm"
              ]
            },
            "description": "El desafio se consumio al confirmar: reusarlo es el mismo 401 `invalid or expired challenge`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"challenge\": \"{{mfa_enroll_challenge}}\",\n  \"code\": \"000000\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 401\", function () { pm.response.to.have.status(401); });",
                  "pm.test(\"envelope uniforme\", function () { pm.expect(pm.response.json().error.message).to.eql(\"invalid or expired challenge\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/me/mfa/disable (limpieza: retira el factor con un recovery_code -> 204)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/me/mfa/disable",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me",
                "mfa",
                "disable"
              ]
            },
            "description": "Deja a la persona sin factor, como estaba antes de la secuencia: el resto de la carpeta sigue sin segundo paso. Un recovery_code (no el TOTP, ya gastado en esta ventana).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"password\": \"kT7-quartz-vexil-29\",\n  \"recovery_code\": \"{{mfa_enroll_recovery_code}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 204\", function () { pm.response.to.have.status(204); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/me/password (rotacion)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/me/password",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me",
                "password"
              ]
            },
            "description": "Rota con la PROPIA sesion; las demas sesiones del usuario se revocan.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"current_password\": \"kT7-quartz-vexil-29\",\n  \"new_password\": \"kT7-quartz-vexil-30\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 204\", function () { pm.response.to.have.status(204); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/me/password (dd_ machine key -> 403)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/me/password",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me",
                "password"
              ]
            },
            "description": "La dd_ no tiene persona: 403 (hereda el bearer de coleccion).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"current_password\": \"x\",\n  \"new_password\": \"irrelevant-here-1\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/users/:id (disable)",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/users/{{postman_user_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "users",
                "{{postman_user_id}}"
              ]
            },
            "description": "Deshabilita (jamas borra) y revoca sus sesiones vivas."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"status disabled\", function () {",
                  "  pm.expect(pm.response.json().user.status).to.eql(\"disabled\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/me (sesion muerta tras disable -> 401)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/me",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me"
              ]
            },
            "description": "",
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 401\", function () { pm.response.to.have.status(401); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/users/:id/enable",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/users/{{postman_user_id}}/enable",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "users",
                "{{postman_user_id}}",
                "enable"
              ]
            },
            "description": ""
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"status active (las sesiones viejas siguen muertas)\", function () {",
                  "  pm.expect(pm.response.json().user.status).to.eql(\"active\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/users/:id/mfa/remove (sin credencial -> 401)",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/users/{{postman_user_id}}/mfa/remove",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "users",
                "{{postman_user_id}}",
                "mfa",
                "remove"
              ]
            },
            "description": "La operacion requiere una sesion de dashboard con rol account_admin y una persona con factor activo. Este caso sin credencial cubre el rechazo 401 del contrato sin mutar el factor.",
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text",
                "exec": [
                  "pm.test(\"status 401\", function () { pm.response.to.have.status(401); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/users/:id/mfa/reset-failures (sin credencial -> 401)",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/users/{{postman_user_id}}/mfa/reset-failures",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "users",
                "{{postman_user_id}}",
                "mfa",
                "reset-failures"
              ]
            },
            "description": "La operacion requiere una sesion de dashboard con rol account_admin. Este caso sin credencial cubre el rechazo 401 del contrato sin mutar los contadores de fallos.",
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 401\", function () { pm.response.to.have.status(401); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/auth/logout (idempotente -> 204)",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/logout",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "logout"
              ]
            },
            "description": "La sesion ya murio con el disable; logout sigue siendo 204 (idempotente, sin enumeracion).",
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 204\", function () { pm.response.to.have.status(204); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/auth/password-reset (solicitud: 202 opaco)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/password-reset",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "password-reset"
              ]
            },
            "description": "SIN bearer. UN solo 202 para todo desenlace (auth_router.ex @accepted): la ruta niega el oraculo de existencia por diseno. El correo con el token ddr_ cae en el mail-sink del harness.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"{{postman_user_email}}\",\n  \"locale\": \"es\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 202\", function () { pm.response.to.have.status(202); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: el 202 opaco documentado\", function () {",
                  "  pm.expect(j.status).to.eql(\"accepted\");",
                  "  pm.expect(j.message).to.eql(\"if that address has an account, a reset link is on its way\");",
                  "});",
                  "pm.collectionVariables.set(\"reset_202_body\", pm.response.text());"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/auth/password-reset (email inexistente -> MISMO 202, cero oraculo)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/password-reset",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "password-reset"
              ]
            },
            "description": "SIN bearer. La propiedad anti-enumeracion ejercitada contra el stack real: cuenta inexistente = exactamente los mismos bytes que la existente.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"nadie-{{$timestamp}}@e2e.test\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 202 tambien sin cuenta\", function () { pm.response.to.have.status(202); });",
                  "pm.test(\"cuerpo BYTE-IDENTICO al del email existente (anti-enumeracion)\", function () {",
                  "  pm.expect(pm.response.text()).to.eql(pm.collectionVariables.get(\"reset_202_body\"));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/auth/password-reset/confirm (happy: 200 y mata sesiones)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/password-reset/confirm",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "password-reset",
                "confirm"
              ]
            },
            "description": "SIN bearer. Canjea el token ddr_ capturado del mail-sink. Toda sesion ddu_ viva del usuario muere con el reset (el usuario de esta corrida es efimero: email por timestamp, nada posterior lo reusa).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"token\": \"{{reset_token}}\",\n  \"new_password\": \"jH4-marble-oxter-52\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: {status: ok}\", function () {",
                  "  pm.expect(pm.response.json().status).to.eql(\"ok\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/auth/password-reset/confirm (token quemado -> 401 uniforme)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/password-reset/confirm",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "password-reset",
                "confirm"
              ]
            },
            "description": "SIN bearer. El MISMO token otra vez: un-solo-uso ejercitado contra el stack real, y el 401 es el uniforme (quemado indistinguible de inexistente/expirado).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"token\": \"{{reset_token}}\",\n  \"new_password\": \"jH4-marble-oxter-53\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 401: un solo uso, contra el stack real\", function () { pm.response.to.have.status(401); });",
                  "pm.test(\"el 401 es el uniforme del contrato\", function () {",
                  "  pm.expect(pm.response.text()).to.include(\"invalid or expired token\");",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "22 · agent-login (widget)",
      "description": "Login de agente para el widget (CTI-4). La cara ddw_ va DARK (flag widget_auth_enabled OFF en el stack) → agent-login responde 404; la gestión de credencial (dd_) NO es flag-gated y funciona. Orden: crear agente → habilitar credencial → agent-login dark 404 → deshabilitar.",
      "item": [
        {
          "name": "POST /v1/agents (seat para login)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/agents",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents"
              ]
            },
            "description": "Crea el seat que tendrá credencial de widget-login.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Widget Agent\",\n  \"email\": \"widget-agent-{{$timestamp}}@example.test\",\n  \"role\": \"agent\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function(){ pm.response.to.have.status(201); });",
                  "const j=pm.response.json();",
                  "pm.collectionVariables.set(\"widget_agent_id\", j.agent.id);",
                  "pm.collectionVariables.set(\"widget_agent_email\", j.agent.email);",
                  "pm.test(\"login_enabled arranca en false\", function(){ pm.expect(j.agent.login_enabled).to.eql(false); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/agents/:id/credential (habilitar login)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/agents/{{widget_agent_id}}/credential",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents",
                "{{widget_agent_id}}",
                "credential"
              ]
            },
            "description": "Habilita el widget-login del agente. El password jamás vuelve en la respuesta.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"password\": \"wN3-cobalt-fjord-77\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function(){ pm.response.to.have.status(200); });",
                  "const j=pm.response.json();",
                  "pm.test(\"login_enabled ahora true, sin material secreto\", function(){",
                  "  pm.expect(j.agent.login_enabled).to.eql(true);",
                  "  pm.expect(JSON.stringify(j)).to.not.include(\"password_hash\");",
                  "  pm.expect(JSON.stringify(j)).to.not.include(\"pbkdf2\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/agents/:id/credential (password requerido -> 400)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/agents/{{widget_agent_id}}/credential",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents",
                "{{widget_agent_id}}",
                "credential"
              ]
            },
            "description": "",
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function(){ pm.response.to.have.status(400); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/auth/agent-login (DARK: flag off -> 404)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Origin",
                "value": "https://crm.example.com"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/agent-login",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "agent-login"
              ]
            },
            "description": "La cara ddw_ está DARK en el stack (widget_auth_enabled OFF): agent-login es indistinguible de una ruta inexistente (404). Cuando el operador prenda el flag, este mismo request acuña un ddw_. `mfa_code` (opcional): el TOTP vigente de la persona ligada al asiento; solo se exige si el tenant tiene `mfa_required_enforced` ON (core#2406). Con credenciales correctas puede haber 403 `login_policy_denied` (direccion u hora fuera de la politica del tenant) o 403 `mfa_required` (el asiento no puede presentar segundo factor): ver los ejemplos.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"tenant_id\": \"{{tenant_id}}\",\n  \"email\": \"{{widget_agent_email}}\",\n  \"password\": \"wN3-cobalt-fjord-77\",\n  \"mfa_code\": \"123456\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404 (lane dark)\", function(){ pm.response.to.have.status(404); });",
                  "pm.test(\"envelope not_found\", function(){ pm.expect(pm.response.json().error.code).to.eql(\"not_found\"); });"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "403 login_policy_denied (direccion u hora fuera de la politica)",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Origin",
                    "value": "https://crm.example.com"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/v1/auth/agent-login",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "auth",
                    "agent-login"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"tenant_id\": \"{{tenant_id}}\",\n  \"email\": \"{{widget_agent_email}}\",\n  \"password\": \"wN3-cobalt-fjord-77\",\n  \"mfa_code\": \"123456\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Forbidden",
              "code": 403,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"error\": {\n    \"code\": \"login_policy_denied\",\n    \"message\": \"sign-in is not allowed from here or now\"\n  }\n}"
            },
            {
              "name": "403 mfa_required (el asiento no puede presentar segundo factor)",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Origin",
                    "value": "https://crm.example.com"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/v1/auth/agent-login",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "auth",
                    "agent-login"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"tenant_id\": \"{{tenant_id}}\",\n  \"email\": \"{{widget_agent_email}}\",\n  \"password\": \"wN3-cobalt-fjord-77\",\n  \"mfa_code\": \"123456\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Forbidden",
              "code": 403,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"error\": {\n    \"code\": \"mfa_required\",\n    \"message\": \"this workspace requires a second factor for this sign-in\"\n  }\n}"
            }
          ]
        },
        {
          "name": "DELETE /v1/agents/:id/credential (deshabilitar login)",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/agents/{{widget_agent_id}}/credential",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents",
                "{{widget_agent_id}}",
                "credential"
              ]
            },
            "description": "Deshabilita el widget-login (idempotente). Revoca cualquier ddw_ vivo del agente."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function(){ pm.response.to.have.status(200); });",
                  "pm.test(\"login_enabled de vuelta a false\", function(){ pm.expect(pm.response.json().agent.login_enabled).to.eql(false); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/auth/widget-config (DARK: flag off -> 404)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/widget-config?tenant_id={{tenant_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "widget-config"
              ],
              "query": [
                {
                  "key": "tenant_id",
                  "value": "{{tenant_id}}"
                }
              ]
            },
            "description": "Bootstrap PUBLICO del widget: los origins HABILITADOS del tenant — el mismo set que agent-login value-matchea, servido al frame host (frame-ancestors) y al frame (validacion de peer postMessage). Flag-gated: con WIDGET_AUTH_ENABLED off es un 404 plano, indistinguible de una ruta inexistente (la invariante dark del carril).",
            "auth": {
              "type": "noauth"
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"DARK: 404 con el flag apagado\", () => pm.expect(pm.response.code).to.eql(404));",
                  "pm.test(\"envelope not_found (invariante dark del carril)\", () => {",
                  "  pm.expect(pm.response.json().error.code).to.eql(\"not_found\");",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "23 · recordings",
      "description": "Grabaciones: metadata retención-locked (piso regulatorio 39 meses), playback firmado (el audio NUNCA transita el API), legal hold, export bulk (top-level /v1/recording-exports) y política per-tenant. Contrato: contract-recordings.yaml. No hay POST de alta (las grabaciones nacen de llamadas reales): un id sin sembrar → 404 fail-closed.",
      "item": [
        {
          "name": "GET /v1/recordings",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/recordings?limit=20",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "recordings"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "20"
                }
              ]
            },
            "description": "Lista filtrada, keyset-paginada, newest-first. Tenant nuevo → página vacía."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: RecordingPage {recordings[], next_cursor}\", function () {",
                  "  pm.expect(j.recordings).to.be.an(\"array\");",
                  "  pm.expect(j).to.have.property(\"next_cursor\");",
                  "  if (j.recordings.length > 0) {",
                  "    const r = j.recordings[0];",
                  "    pm.expect(r.id).to.be.a(\"string\");",
                  "    pm.expect(r.call_id).to.be.a(\"string\");",
                  "    pm.expect(r.started_at).to.be.a(\"string\");",
                  "    pm.expect(r.phone_number).to.be.a(\"string\");",
                  "    pm.expect(r.ai_used).to.be.a(\"boolean\");",
                  "    pm.expect(r.legal_hold).to.be.a(\"boolean\");",
                  "    pm.collectionVariables.set(\"recording_id\", r.id);",
                  "  }",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/recording-policy",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/recording-policy",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "recording-policy"
              ]
            },
            "description": "Política efectiva (defaults materializados en la primera lectura)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: RecordingPolicy {enabled, retention_months>=39, channels}\", function () {",
                  "  pm.expect(j.recording_policy.enabled).to.be.a(\"boolean\");",
                  "  pm.expect(j.recording_policy.retention_months).to.be.at.least(39);",
                  "  pm.expect([\"mono\", \"stereo\"]).to.include(j.recording_policy.channels);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/recording-policy",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/recording-policy",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "recording-policy"
              ]
            },
            "description": "Full-set de la política singleton. retention_months se acota al piso 39.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"enabled\": true,\n  \"retention_months\": 39,\n  \"channels\": \"stereo\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: política aplicada en el piso\", function () {",
                  "  pm.expect(j.recording_policy.retention_months).to.eql(39);",
                  "  pm.expect(j.recording_policy.channels).to.eql(\"stereo\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/recording-policy — bajo el piso (422)",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/recording-policy",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "recording-policy"
              ]
            },
            "description": "retention_months < 39 (piso regulatorio) → 422 unprocessable.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"retention_months\": 12\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 422 (bajo el piso de 39 meses)\", function () { pm.response.to.have.status(422); });",
                  "pm.test(\"error envelope presente\", function () {",
                  "  pm.expect(pm.response.json().error).to.be.an(\"object\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/privacy/erasure-requests (bandera apagada -> 403 privacy_erasure_disabled)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// El harness no enciende privacy_erasure_enabled (nace APAGADA, core#2483): la ruta existe,",
                  "// el rol alcanza y contesta 403 sin escribir nada. Con la bandera encendida el contrato",
                  "// (202, informe del simulacro) lo prueba core en test/db/privacy_erasure_test.exs.",
                  "pm.test(\"status 403 (bandera apagada)\", function () { pm.response.to.have.status(403); });",
                  "const j = pm.response.json();",
                  "pm.test(\"error.code == privacy_erasure_disabled (no es role_forbidden)\", function () {",
                  "  pm.expect(j.error.code).to.eql(\"privacy_erasure_disabled\");",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/privacy/erasure-requests",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "privacy",
                "erasure-requests"
              ]
            },
            "description": "Borrado de UNA persona (core#2483, H-06), en simulacro. Con la bandera privacy_erasure_enabled apagada (default) contesta 403 privacy_erasure_disabled y no escribe nada.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"phone\": \"+13125550100\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "POST /v1/recording-exports",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/recording-exports",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "recording-exports"
              ]
            },
            "description": "Encola export async del set que matchea los filtros; 202 + job_id (evidencia durable).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"from\": \"{{export_from}}\",\n  \"to\": \"{{export_to}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Rango RELATIVO al reloj, nunca un literal de calendario.",
                  "// core#151 (anti export-bomb) rechaza con 422 range_too_large cualquier",
                  "// rango > recording_export_max_range_days, asi que un",
                  "// 'from' fijo se vuelve 422 con solo dejar pasar el tiempo: el literal",
                  "// 2020-01-01 -> 2030-01-01 eran 3653 dias y caducaba pasara lo que pasara.",
                  "// 30 dias hacia atras cubre de sobra lo que el harness graba en una corrida.",
                  "// El valor vigente del tope lo declara core y aqui NO se copia: una copia",
                  "// en comentario se desfasa en silencio. Que estos 30 dias sigan por DEBAJO",
                  "// del tope lo ata el gate .ci/qa/check_runtime_limits_drift.py en cada PR.",
                  "const to = new Date();",
                  "const from = new Date(to.getTime() - 30 * 24 * 60 * 60 * 1000);",
                  "pm.collectionVariables.set(\"export_from\", from.toISOString());",
                  "pm.collectionVariables.set(\"export_to\", to.toISOString());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 202 (export encolado)\", function () { pm.response.to.have.status(202); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: RecordingExportJob {job_id, status}\", function () {",
                  "  pm.expect(j.export.job_id).to.be.a(\"string\");",
                  "  pm.expect([\"queued\", \"running\", \"done\", \"failed\"]).to.include(j.export.status);",
                  "});",
                  "pm.collectionVariables.set(\"recording_export_job_id\", j.export.job_id);"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/privacy/subject-exports (bandera apagada -> 503 subject_export_disabled)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// El harness no enciende privacy_subject_export_enabled (nace APAGADA, core#2484):",
                  "// la ruta existe, el rol alcanza y contesta 503 sin leer nada. Con la bandera encendida",
                  "// el contrato (202, ZIP, manifiesto) lo prueba core en test/db/privacy_subject_export_test.exs.",
                  "pm.test(\"status 503 (bandera apagada)\", function () { pm.response.to.have.status(503); });",
                  "const j = pm.response.json();",
                  "pm.test(\"error.code == subject_export_disabled (no es otro 503)\", function () {",
                  "  pm.expect(j.error.code).to.eql(\"subject_export_disabled\");",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/privacy/subject-exports",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "privacy",
                "subject-exports"
              ]
            },
            "description": "Exportacion de los datos de UNA persona (core#2484, H-07). Con la bandera privacy_subject_export_enabled apagada (default) contesta 503 subject_export_disabled y no lee nada.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"phone\": \"+13125550100\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "POST /v1/recording-exports (rango excesivo -> 422: el tope anti export-bomb SIGUE puesto)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Rango deliberadamente excesivo, RELATIVO al reloj (nunca un literal:",
                  "// un literal caduca solo, que es como se rompio esta coleccion).",
                  "// 400 dias > recording_export_max_range_days. El valor vigente del tope lo",
                  "// declara core y aqui NO se copia: una copia en comentario se desfasa en",
                  "// silencio. Que estos 400 dias sigan por ENCIMA del tope (y por tanto que",
                  "// esta peticion siga comprobando algo) lo ata el gate OFFLINE",
                  "// .ci/qa/check_runtime_limits_drift.py en cada PR.",
                  "const to = new Date();",
                  "const from = new Date(to.getTime() - 400 * 24 * 60 * 60 * 1000);",
                  "pm.collectionVariables.set(\"export_from_oversized\", from.toISOString());",
                  "pm.collectionVariables.set(\"export_to_oversized\", to.toISOString());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// ROJO AQUI = SE AFLOJO EL TOPE, no = se rompio el export.",
                  "// El export normal lo cubre la peticion de arriba; esta cubre la otra",
                  "// mitad del contrato: un export desmedido DEBE rechazarse. Sin ella,",
                  "// subir el limite, dejarlo mal en una variable o borrar",
                  "// validate_export_range/1 entero pasa en silencio (MT-REP-03).",
                  "pm.test(\"status 422 (rango excesivo rechazado)\", function () { pm.response.to.have.status(422); });",
                  "const j = pm.response.json();",
                  "// El CODIGO, no solo el status: un 422 por otra causa no vale como prueba",
                  "// de que el tope siga vivo.",
                  "pm.test(\"error.code == range_too_large (es el tope, no otro 422)\", function () {",
                  "  pm.expect(j.error.code).to.eql(\"range_too_large\");",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/recording-exports",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "recording-exports"
              ]
            },
            "description": "Caso NEGATIVO del tope anti export-bomb (core#151, MT-REP-03): un rango mayor que recording_export_max_range_days (valor vigente declarado por core, no copiado aqui) debe rechazarse con 422 range_too_large. Si esta peticion se pone en rojo, lo que se aflojo es el tope, no el export.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"from\": \"{{export_from_oversized}}\",\n  \"to\": \"{{export_to_oversized}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "GET /v1/recording-exports/:job_id",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/recording-exports/{{recording_export_job_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "recording-exports",
                "{{recording_export_job_id}}"
              ]
            },
            "description": "Poll del job; done → url firmada; expired → el archivo ya caducó en el almacén de exports (sin url; se regenera con otro POST). job_id cross-tenant/desconocido → 404."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// recording_export_job_id vacío (no sembrado por una corrida real) → sembramos un UUID",
                  "// aleatorio: un segmento vacío caería en el catch-all del router",
                  "// ('no such route') sin ejercitar la ruta real; así el 404 es el del",
                  "// endpoint. Con la variable ya sembrada, la request usa el id real.",
                  "if (!pm.collectionVariables.get(\"recording_export_job_id\")) {",
                  "  pm.collectionVariables.set(\"recording_export_job_id\", pm.variables.replaceIn(\"{{$guid}}\"));",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200|404 (poll del job o id desconocido)\", function () {",
                  "  pm.expect([200, 404]).to.include(pm.response.code);",
                  "});",
                  "if (pm.response.code === 200) {",
                  "  const j = pm.response.json();",
                  "  pm.test(\"shape 200: RecordingExportJob\", function () {",
                  "    pm.expect(j.export.job_id).to.be.a(\"string\");",
                  "    // expired (core#1273): job done cuyo archivo ya caduco en el almacen de exports;",
                  "    // estado derivado al leer, sin url ni expires_at.",
                  "    pm.expect([\"queued\", \"running\", \"done\", \"failed\", \"expired\"]).to.include(j.export.status);",
                  "    if (j.export.status === \"expired\") {",
                  "      pm.expect(j.export.url).to.eql(null);",
                  "      pm.expect(j.export.expires_at).to.eql(null);",
                  "    }",
                  "  });",
                  "}",
                  "if (pm.response.code === 404) {",
                  "  pm.test(\"404 del endpoint real (no catch-all 'no such route')\", function () {",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/recordings/:id",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/recordings/{{recording_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "recordings",
                "{{recording_id}}"
              ]
            },
            "description": "Metadata completa (evidencia): consent, gates_at_dial, decision_ref, storage, legal hold. Id desconocido → 404."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// recording_id vacío (no sembrado por una corrida real) → sembramos un UUID",
                  "// aleatorio: un segmento vacío caería en el catch-all del router",
                  "// ('no such route') sin ejercitar la ruta real; así el 404 es el del",
                  "// endpoint. Con la variable ya sembrada, la request usa el id real.",
                  "if (!pm.collectionVariables.get(\"recording_id\")) {",
                  "  pm.collectionVariables.set(\"recording_id\", pm.variables.replaceIn(\"{{$guid}}\"));",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200|404 (detalle o id desconocido)\", function () {",
                  "  pm.expect([200, 404]).to.include(pm.response.code);",
                  "});",
                  "if (pm.response.code === 200) {",
                  "  const j = pm.response.json();",
                  "  pm.test(\"shape 200: Recording {id, gates_at_dial[], storage}\", function () {",
                  "    pm.expect(j.recording.id).to.be.a(\"string\");",
                  "    pm.expect(j.recording.gates_at_dial).to.be.an(\"array\");",
                  "    pm.expect(j.recording.storage).to.be.an(\"object\");",
                  "  });",
                  "}",
                  "if (pm.response.code === 404) {",
                  "  pm.test(\"404 del endpoint real (no catch-all 'no such route')\", function () {",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/recordings/:id/playback-url",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{baseUrl}}/v1/recordings/{{recording_id}}/playback-url",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "recordings",
                "{{recording_id}}",
                "playback-url"
              ]
            },
            "description": "Mint de URL firmada corta (R2/S3); el audio no transita el API. Id desconocido → 404; sin audio → 409."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// recording_id vacío (no sembrado por una corrida real) → sembramos un UUID",
                  "// aleatorio: un segmento vacío caería en el catch-all del router",
                  "// ('no such route') sin ejercitar la ruta real; así el 404 es el del",
                  "// endpoint. Con la variable ya sembrada, la request usa el id real.",
                  "if (!pm.collectionVariables.get(\"recording_id\")) {",
                  "  pm.collectionVariables.set(\"recording_id\", pm.variables.replaceIn(\"{{$guid}}\"));",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200|404|409 (mint o id desconocido/sin audio)\", function () {",
                  "  pm.expect([200, 404, 409]).to.include(pm.response.code);",
                  "});",
                  "if (pm.response.code === 200) {",
                  "  const j = pm.response.json();",
                  "  pm.test(\"shape 200: RecordingPlaybackUrl {url, expires_at}\", function () {",
                  "    pm.expect(j.url).to.be.a(\"string\");",
                  "    pm.expect(j.expires_at).to.be.a(\"string\");",
                  "  });",
                  "}",
                  "if (pm.response.code === 404) {",
                  "  pm.test(\"404 del endpoint real (no catch-all 'no such route')\", function () {",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/recordings/:id/legal-hold",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/recordings/{{recording_id}}/legal-hold",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "recordings",
                "{{recording_id}}",
                "legal-hold"
              ]
            },
            "description": "Set/lift idempotente del legal hold. active:true exige reason. Scope dd_ (ddw_ → 403). Id desconocido → 404.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"active\": true,\n  \"reason\": \"Postman integration hold \\u2014 synthetic\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// recording_id vacío (no sembrado por una corrida real) → sembramos un UUID",
                  "// aleatorio: un segmento vacío caería en el catch-all del router",
                  "// ('no such route') sin ejercitar la ruta real; así el 404 es el del",
                  "// endpoint. Con la variable ya sembrada, la request usa el id real.",
                  "if (!pm.collectionVariables.get(\"recording_id\")) {",
                  "  pm.collectionVariables.set(\"recording_id\", pm.variables.replaceIn(\"{{$guid}}\"));",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200|404 (hold seteado o id desconocido)\", function () {",
                  "  pm.expect([200, 404]).to.include(pm.response.code);",
                  "});",
                  "if (pm.response.code === 200) {",
                  "  const j = pm.response.json();",
                  "  pm.test(\"shape 200: legal_hold activo\", function () { pm.expect(j.recording.legal_hold).to.eql(true); });",
                  "}",
                  "if (pm.response.code === 404) {",
                  "  pm.test(\"404 del endpoint real (no catch-all 'no such route')\", function () {",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "24 · voice-ai",
      "description": "Voice AI: agentes (builder + policy versionada), conversaciones live/histórico + transcript/outcome, takeover del supervisor (reusa la maquinaria de supervisión), kill-switch pause/resume/status del AI-dialing, métricas del día y elegibilidad de campañas. Contrato: contract-voiceai.yaml (+ GET /v1/ai/status y GET /v1/ai/eligibility). Las conversaciones nacen de llamadas reales: id sin sembrar → 404.",
      "item": [
        {
          "name": "GET /v1/ai/agents",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/ai/agents",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "ai",
                "agents"
              ]
            },
            "description": "Roster de agentes AI del tenant."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: {ai_agents[]}\", function () {",
                  "  pm.expect(j.ai_agents).to.be.an(\"array\");",
                  "  if (j.ai_agents.length > 0) {",
                  "    const a = j.ai_agents[0];",
                  "    pm.expect(a.id).to.be.a(\"string\");",
                  "    pm.expect(a.name).to.be.a(\"string\");",
                  "    pm.expect([\"draft\", \"live\", \"paused\", \"retired\"]).to.include(a.status);",
                  "    pm.expect(a.policy_version).to.be.a(\"number\");",
                  "  }",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/ai/voices",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/ai/voices",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "ai",
                "voices"
              ]
            },
            "description": "Catalogo gobernado de voces de agentes de IA (core#1539): lo que el constructor ofrece en su select. Lectura para cualquier clave de tenant."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: {voices[]} con voice_id, provider, language y label\", function () {",
                  "  pm.expect(j.voices).to.be.an(\"array\");",
                  "  pm.expect(j.voices.length).to.be.at.least(1);",
                  "  j.voices.forEach(function (v) {",
                  "    [\"voice_id\", \"provider\", \"language\", \"label\"].forEach(function (k) {",
                  "      pm.expect(v[k], k).to.be.a(\"string\").and.not.empty;",
                  "    });",
                  "  });",
                  "});",
                  "pm.collectionVariables.set(\"ai_voice_id\", j.voices[0].voice_id);",
                  "pm.collectionVariables.set(\"ai_voice_language\", j.voices[0].language);"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/ai/voices?language= (filtro)",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/ai/voices?language={{ai_voice_language}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "ai",
                "voices"
              ],
              "query": [
                {
                  "key": "language",
                  "value": "{{ai_voice_language}}"
                }
              ]
            },
            "description": "Filtro por idioma (subtag primario, sin distinguir mayusculas): todas las voces devueltas son de ese idioma."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"el filtro devuelve voces y todas son del idioma pedido\", function () {",
                  "  pm.expect(j.voices).to.be.an(\"array\");",
                  "  pm.expect(j.voices.length).to.be.at.least(1);",
                  "  j.voices.forEach(function (v) { pm.expect(v.language).to.eql(pm.collectionVariables.get(\"ai_voice_language\")); });",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/ai/voices?language=zz (sin voces)",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/ai/voices?language=zz",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "ai",
                "voices"
              ],
              "query": [
                {
                  "key": "language",
                  "value": "zz"
                }
              ]
            },
            "description": "Un idioma sin voces es 200 con lista vacia, no un error."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"lista vacia\", function () { pm.expect(pm.response.json().voices).to.eql([]); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/ai/agents",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/ai/agents",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "ai",
                "agents"
              ]
            },
            "description": "Alta de agente draft (policy_version 1). disclosure.text mandado; regla human_requested siempre presente y locked. El voice_id sale del catalogo (GET /v1/ai/voices): uno fuera de el es 422 (core#1539).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"postman-ai-{{$timestamp}}\",\n  \"voice\": {\n    \"language\": \"en-US\",\n    \"voice_id\": \"{{ai_voice_id}}\",\n    \"pace\": 0.5\n  },\n  \"disclosure\": {\n    \"text\": \"This call is from an automated assistant calling on behalf of the account holder. This call is recorded.\"\n  },\n  \"policy\": {\n    \"max_settlement_pct\": 70,\n    \"min_payment_cents\": 5000,\n    \"max_plan_months\": 12,\n    \"take_payments\": \"none\",\n    \"prohibited\": [\n      \"legal threats\",\n      \"third-party disclosure\"\n    ]\n  },\n  \"handoff_rules\": [\n    {\n      \"trigger\": \"keywords\",\n      \"params\": {\n        \"keywords\": [\n          \"attorney\",\n          \"lawyer\",\n          \"dispute\",\n          \"cease\",\n          \"bankruptcy\"\n        ]\n      },\n      \"action\": \"transfer\",\n      \"severity\": \"hard\"\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: draft v1, human_requested handoff locked\", function () {",
                  "  pm.expect(j.ai_agent.id).to.be.a(\"string\");",
                  "  pm.expect(j.ai_agent.status).to.eql(\"draft\");",
                  "  pm.expect(j.ai_agent.policy_version).to.eql(1);",
                  "  pm.expect(j.ai_agent.disclosure.mandated).to.eql(true);",
                  "  const hr = (j.ai_agent.handoff_rules || []).find(function (r) { return r.trigger === \"human_requested\"; });",
                  "  pm.expect(hr, \"regla human_requested presente\").to.be.an(\"object\");",
                  "  pm.expect(hr.locked).to.eql(true);",
                  "});",
                  "pm.collectionVariables.set(\"ai_agent_id\", j.ai_agent.id);",
                  "pm.collectionVariables.set(\"ai_agent_name\", j.ai_agent.name);"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/ai/agents — sin disclosure (400)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/ai/agents",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "ai",
                "agents"
              ]
            },
            "description": "Create sin disclosure.text (mandado) → 400.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"postman-ai-nodisc-{{$timestamp}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400 (disclosure.text requerido)\", function () { pm.response.to.have.status(400); });",
                  "pm.test(\"error envelope presente\", function () { pm.expect(pm.response.json().error).to.be.an(\"object\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/ai/agents — nombre duplicado (409)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/ai/agents",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "ai",
                "agents"
              ]
            },
            "description": "Reusa el nombre del agente recién creado → 409 conflict (400 si el nombre no quedó sembrado).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"{{ai_agent_name}}\",\n  \"disclosure\": {\n    \"text\": \"duplicate-name probe\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 409|400 (nombre duplicado → 409; sin sembrar → 400)\", function () {",
                  "  pm.expect([400, 409]).to.include(pm.response.code);",
                  "});",
                  "pm.test(\"error envelope presente\", function () { pm.expect(pm.response.json().error).to.be.an(\"object\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/ai/agents — voice_id fuera del catalogo (422)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/ai/agents",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "ai",
                "agents"
              ]
            },
            "description": "Un voice.voice_id que no esta en GET /v1/ai/voices se rechaza al escribir con 422 (core#1539); antes era texto libre y fallaba al pre-renderizar el aviso.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"postman-ai-badvoice-{{$timestamp}}\",\n  \"voice\": {\n    \"language\": \"en\",\n    \"voice_id\": \"voz-inventada-{{$timestamp}}\"\n  },\n  \"disclosure\": {\n    \"text\": \"This call is from an automated assistant calling on behalf of the account holder. This call is recorded.\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 422 (voice_id fuera del catalogo)\", function () { pm.response.to.have.status(422); });",
                  "pm.test(\"error envelope presente\", function () { pm.expect(pm.response.json().error).to.be.an(\"object\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/ai/agents/:id",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/ai/agents/{{ai_agent_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "ai",
                "agents",
                "{{ai_agent_id}}"
              ]
            },
            "description": "Detalle del agente. Id desconocido → 404."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// ai_agent_id vacío (no sembrado por una corrida real) → sembramos un UUID",
                  "// aleatorio: un segmento vacío caería en el catch-all del router",
                  "// ('no such route') sin ejercitar la ruta real; así el 404 es el del",
                  "// endpoint. Con la variable ya sembrada, la request usa el id real.",
                  "if (!pm.collectionVariables.get(\"ai_agent_id\")) {",
                  "  pm.collectionVariables.set(\"ai_agent_id\", pm.variables.replaceIn(\"{{$guid}}\"));",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200|404\", function () { pm.expect([200, 404]).to.include(pm.response.code); });",
                  "if (pm.response.code === 200) {",
                  "  const j = pm.response.json();",
                  "  pm.test(\"shape 200: AiAgent\", function () {",
                  "    pm.expect(j.ai_agent.id).to.be.a(\"string\");",
                  "    pm.expect(j.ai_agent.policy_version).to.be.a(\"number\");",
                  "  });",
                  "}",
                  "if (pm.response.code === 404) {",
                  "  pm.test(\"404 del endpoint real (no catch-all 'no such route')\", function () {",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/ai/agents/:id",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/ai/agents/{{ai_agent_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "ai",
                "agents",
                "{{ai_agent_id}}"
              ]
            },
            "description": "Merge-update; cada cambio aceptado bumpea policy_version (monotónico). Disclosure inmutable post-live → 422.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"policy\": {\n    \"max_settlement_pct\": 65\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// ai_agent_id vacío (no sembrado por una corrida real) → sembramos un UUID",
                  "// aleatorio: un segmento vacío caería en el catch-all del router",
                  "// ('no such route') sin ejercitar la ruta real; así el 404 es el del",
                  "// endpoint. Con la variable ya sembrada, la request usa el id real.",
                  "if (!pm.collectionVariables.get(\"ai_agent_id\")) {",
                  "  pm.collectionVariables.set(\"ai_agent_id\", pm.variables.replaceIn(\"{{$guid}}\"));",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200|404|409|422\", function () {",
                  "  pm.expect([200, 404, 409, 422]).to.include(pm.response.code);",
                  "});",
                  "if (pm.response.code === 200) {",
                  "  const j = pm.response.json();",
                  "  pm.test(\"shape 200: policy_version bumpeada\", function () { pm.expect(j.ai_agent.policy_version).to.be.at.least(2); });",
                  "}",
                  "if (pm.response.code === 404) {",
                  "  pm.test(\"404 del endpoint real (no catch-all 'no such route')\", function () {",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/ai/agents/:id/test",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/ai/agents/{{ai_agent_id}}/test",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "ai",
                "agents",
                "{{ai_agent_id}}",
                "test"
              ]
            },
            "description": "Conversación SANDBOX (sin llamada real, sin fila ai_conversations). Id desconocido → 404.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"message\": \"Yes \\u2014 I can pay $150 on the 27th.\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// ai_agent_id vacío (no sembrado por una corrida real) → sembramos un UUID",
                  "// aleatorio: un segmento vacío caería en el catch-all del router",
                  "// ('no such route') sin ejercitar la ruta real; así el 404 es el del",
                  "// endpoint. Con la variable ya sembrada, la request usa el id real.",
                  "if (!pm.collectionVariables.get(\"ai_agent_id\")) {",
                  "  pm.collectionVariables.set(\"ai_agent_id\", pm.variables.replaceIn(\"{{$guid}}\"));",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Camino VIVO en el stack e2e: core proxya al sandbox de voice-ai",
                  "// (POST /sandbox/messages :8767, bearer service-token \\u2014 voice-ai 430f08f)",
                  "// v\\u00eda VOICEAI_RUNTIME_URL, y el LocalLLM responde \\u2192 200 {session_id, reply}.",
                  "// 404 = agente inexistente. 503 SOLO como rama documentada fail-closed",
                  "// (un env sin runtime deja VOICEAI_RUNTIME_URL vac\\u00eda \\u2192 sandbox_unavailable).",
                  "pm.test(\"status 200|404|503\", function () { pm.expect([200, 404, 503]).to.include(pm.response.code); });",
                  "if (pm.response.code === 200) {",
                  "  const j = pm.response.json();",
                  "  pm.test(\"shape 200: AiTestReply {session_id, reply}\", function () {",
                  "    pm.expect(j.session_id).to.be.a(\"string\").and.to.not.be.empty;",
                  "    pm.expect(j.reply).to.be.a(\"string\").and.to.not.be.empty;",
                  "  });",
                  "}",
                  "if (pm.response.code === 503) {",
                  "  // core#2421: sin techo mensual de gasto el freno responde sandbox_budget_unavailable ANTES de llamar al runtime.",
                  "  pm.test(\"503 fail-closed: sandbox_unavailable | sandbox_budget_unavailable\", function () {",
                  "    pm.expect([\"sandbox_unavailable\", \"sandbox_budget_unavailable\"]).to.include(pm.response.json().error.code);",
                  "  });",
                  "}",
                  "if (pm.response.code === 404) {",
                  "  pm.test(\"404 del endpoint real (no catch-all 'no such route')\", function () {",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/ai/agents/:id/voice-preview (sin credencial -> 401)",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/ai/agents/{{ai_agent_id}}/voice-preview",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "ai",
                "agents",
                "{{ai_agent_id}}",
                "voice-preview"
              ]
            },
            "description": "La operación requiere una sesión autenticada y un agente con voz del catálogo. Este caso sin credencial cubre el rechazo 401 del contrato sin renderizar audio ni consumir el freno por tenant.",
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 401\", function () { pm.response.to.have.status(401); });"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/ai/agents/:id",
          "request": {
            "method": "DELETE",
            "url": {
              "raw": "{{baseUrl}}/v1/ai/agents/{{ai_agent_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "ai",
                "agents",
                "{{ai_agent_id}}"
              ]
            },
            "description": "Soft-retire (status retired): la fila y sus snapshots sobreviven. Id desconocido → 404."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// ai_agent_id vacío (no sembrado por una corrida real) → sembramos un UUID",
                  "// aleatorio: un segmento vacío caería en el catch-all del router",
                  "// ('no such route') sin ejercitar la ruta real; así el 404 es el del",
                  "// endpoint. Con la variable ya sembrada, la request usa el id real.",
                  "if (!pm.collectionVariables.get(\"ai_agent_id\")) {",
                  "  pm.collectionVariables.set(\"ai_agent_id\", pm.variables.replaceIn(\"{{$guid}}\"));",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200|404\", function () { pm.expect([200, 404]).to.include(pm.response.code); });",
                  "if (pm.response.code === 200) {",
                  "  const j = pm.response.json();",
                  "  pm.test(\"shape 200: agente retired\", function () { pm.expect(j.ai_agent.status).to.eql(\"retired\"); });",
                  "}",
                  "if (pm.response.code === 404) {",
                  "  pm.test(\"404 del endpoint real (no catch-all 'no such route')\", function () {",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/ai/conversations",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/ai/conversations?limit=20",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "ai",
                "conversations"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "20"
                }
              ]
            },
            "description": "Conversaciones cerradas, keyset-paginadas."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: {ai_conversations[], next_cursor}\", function () {",
                  "  pm.expect(j.ai_conversations).to.be.an(\"array\");",
                  "  pm.expect(j).to.have.property(\"next_cursor\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/ai/conversations?active=true",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/ai/conversations?active=true",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "ai",
                "conversations"
              ],
              "query": [
                {
                  "key": "active",
                  "value": "true"
                }
              ]
            },
            "description": "Feed LIVE (en curso), unpaginated (next_cursor null)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: feed activo (next_cursor null)\", function () {",
                  "  pm.expect(j.ai_conversations).to.be.an(\"array\");",
                  "  pm.expect(j.next_cursor).to.be.null;",
                  "});",
                  "if (j.ai_conversations.length > 0) {",
                  "  pm.collectionVariables.set(\"ai_conversation_id\", j.ai_conversations[0].id);",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/ai/conversations/:id",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/ai/conversations/{{ai_conversation_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "ai",
                "conversations",
                "{{ai_conversation_id}}"
              ]
            },
            "description": "Conversación con transcript + outcome. Id desconocido → 404."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// ai_conversation_id vacío (no sembrado por una corrida real) → sembramos un UUID",
                  "// aleatorio: un segmento vacío caería en el catch-all del router",
                  "// ('no such route') sin ejercitar la ruta real; así el 404 es el del",
                  "// endpoint. Con la variable ya sembrada, la request usa el id real.",
                  "if (!pm.collectionVariables.get(\"ai_conversation_id\")) {",
                  "  pm.collectionVariables.set(\"ai_conversation_id\", pm.variables.replaceIn(\"{{$guid}}\"));",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200|404\", function () { pm.expect([200, 404]).to.include(pm.response.code); });",
                  "if (pm.response.code === 200) {",
                  "  const j = pm.response.json();",
                  "  pm.test(\"shape 200: AiConversation\", function () { pm.expect(j.ai_conversation.id).to.be.a(\"string\"); });",
                  "}",
                  "if (pm.response.code === 404) {",
                  "  pm.test(\"404 del endpoint real (no catch-all 'no such route')\", function () {",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/ai/conversations/:id/takeover",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/ai/conversations/{{ai_conversation_id}}/takeover",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "ai",
                "conversations",
                "{{ai_conversation_id}}",
                "takeover"
              ]
            },
            "description": "Takeover del supervisor (evidence-first, reusa Dialer.Supervision). Conv viva → 200; sin conv → 404; switch/audit caído → 502/503.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"supervisor_ext\": \"{{golden_agent_ext}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// ai_conversation_id vacío (no sembrado por una corrida real) → sembramos un UUID",
                  "// aleatorio: un segmento vacío caería en el catch-all del router",
                  "// ('no such route') sin ejercitar la ruta real; así el 404 es el del",
                  "// endpoint. Con la variable ya sembrada, la request usa el id real.",
                  "if (!pm.collectionVariables.get(\"ai_conversation_id\")) {",
                  "  pm.collectionVariables.set(\"ai_conversation_id\", pm.variables.replaceIn(\"{{$guid}}\"));",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200|404|409|502|503 (conv viva → transfer; si no, fail-closed)\", function () {",
                  "  pm.expect([200, 404, 409, 502, 503]).to.include(pm.response.code);",
                  "});",
                  "if (pm.response.code === 200) {",
                  "  const j = pm.response.json();",
                  "  pm.test(\"shape 200: supervision presente\", function () { pm.expect(j.supervision).to.be.an(\"object\"); });",
                  "}",
                  "if (pm.response.code === 404) {",
                  "  pm.test(\"404 del endpoint real (no catch-all 'no such route')\", function () {",
                  "    pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "  });",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/ai/pause",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{baseUrl}}/v1/ai/pause",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "ai",
                "pause"
              ]
            },
            "description": "Kill-switch: pausa TODO el AI-dialing del tenant (idempotente). In-flight sigue."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: ai_dialing paused\", function () {",
                  "  pm.expect(j.ai_dialing).to.eql(\"paused\");",
                  "  pm.expect(j).to.have.property(\"paused_at\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/ai/status",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/ai/status",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "ai",
                "status"
              ]
            },
            "description": "Estado del kill-switch: {ai_dialing, paused_at?, paused_by?}."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: ai_dialing active|paused\", function () {",
                  "  pm.expect([\"active\", \"paused\"]).to.include(j.ai_dialing);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/ai/resume",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{baseUrl}}/v1/ai/resume",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "ai",
                "resume"
              ]
            },
            "description": "Inverso idempotente de pause: reanuda el AI-dialing."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: ai_dialing active\", function () { pm.expect(j.ai_dialing).to.eql(\"active\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/ai/metrics/today",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/ai/metrics/today",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "ai",
                "metrics",
                "today"
              ]
            },
            "description": "Agregado del día UTC (ai_conversations + outcomes + meter ai_usage). handled_delta_pct opcional; containment_rate = contenidas / liquidadas, null con containment_reason si no hay liquidadas (core#1121)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: AiMetricsToday\", function () {",
                  "  pm.expect(j).to.be.an(\"object\");",
                  "  if (j.handled !== undefined) pm.expect(j.handled).to.be.a(\"number\");",
                  "  // containment_rate = contained / settled (core#1121, core ADR #147): number in 0..1, or null",
                  "  // with containment_reason = no_settled_conversations when nothing settled today. Fields",
                  "  // unsettled/containment_reason are asserted only when present (a core before core#1121 lacks them).",
                  "  if (j.containment_rate !== undefined && j.containment_rate !== null) {",
                  "    pm.expect(j.containment_rate).to.be.a(\"number\");",
                  "    pm.expect(j.containment_rate).to.be.within(0, 1);",
                  "  }",
                  "  if (j.containment_reason !== undefined) {",
                  "    if (j.containment_reason === null) pm.expect(j.containment_rate).to.be.a(\"number\");",
                  "    else {",
                  "      pm.expect(j.containment_reason).to.eql(\"no_settled_conversations\");",
                  "      pm.expect(j.containment_rate).to.equal(null);",
                  "    }",
                  "  } else if (j.containment_rate !== undefined) pm.expect(j.containment_rate).to.be.a(\"number\");",
                  "  if (j.unsettled !== undefined) {",
                  "    pm.expect(j.unsettled).to.be.a(\"number\");",
                  "    pm.expect(j.unsettled).to.be.within(0, j.handled);",
                  "  }",
                  "  if (j.avg_handle_secs !== undefined) pm.expect(j.avg_handle_secs).to.be.a(\"number\");",
                  "  if (j.handoffs !== undefined) pm.expect(j.handoffs).to.be.an(\"object\");",
                  "  if (j.ai_minutes !== undefined) pm.expect(j.ai_minutes).to.be.a(\"number\");",
                  "  // handled_delta_pct is nullable per openapi v0.15.0 (oneOf number|null): null when",
                  "  // yesterday had no AI conversations up to this time-of-day. Assert number only when present.",
                  "  if (j.handled_delta_pct !== undefined && j.handled_delta_pct !== null) pm.expect(j.handled_delta_pct).to.be.a(\"number\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/ai/eligibility",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/ai/eligibility",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "ai",
                "eligibility"
              ]
            },
            "description": "Elegibilidad AI por campaña: {campaigns:[{campaign_id, name, eligible, reason}]}."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: eligibility[] con veredicto por campaña\", function () {",
                  "  pm.expect(j.eligibility).to.be.an(\"array\");",
                  "  if (j.eligibility.length > 0) {",
                  "    const c = j.eligibility[0];",
                  "    pm.expect(c).to.have.property(\"campaign_id\");",
                  "    pm.expect(c.name).to.be.a(\"string\");",
                  "    pm.expect(c.eligible).to.be.a(\"boolean\");",
                  "    pm.expect(c).to.have.property(\"reason\");",
                  "  }",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "25 · ws-ticket",
      "item": [
        {
          "name": "POST /v1/ws/ticket",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/ws/ticket",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "ws",
                "ticket"
              ]
            },
            "description": "Acuna el credencial EFIMERO y de UN SOLO USO con el que se abre `GET /v1/ws` (MT-SEC-05 / MT-SEC-10).\n\nEl bearer de larga vida viaja donde SI puede — la cabecera `Authorization` de esta peticion, que la coleccion pone a nivel raiz. Lo unico que acaba viajando en la URL del handshake (donde los navegadores obligan a poner el credencial, y donde queda en logs, historial y `Referer`) es el ticket, que caduca en segundos y se quema al primer canje.\n\nEl ticket hereda el carril EXACTO del bearer que lo pidio y lo anuncia en el prefijo: `ddtt_` tenant, `ddta_` agente. Aqui el bearer es el token de tenant de la coleccion, asi que el ticket tiene que salir `ddtt_`: el borde publico admite en `/v1/ws` SOLO el carril de agente mirando ese prefijo, de modo que un `ddta_` acunado por un bearer de tenant seria un socket con el suelo entero de la empresa detras.\n\nEl canje NO se ejercita aqui: es un handshake WebSocket, fuera del alcance de newman. Sus propiedades (un solo uso incluso con dos canjes concurrentes, y caducidad) viven en test/db/ws_ticket_test.exs de core."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", () => pm.response.to.have.status(201));",
                  "const j = pm.response.json();",
                  "pm.test(\"el ticket sale en el carril del bearer que lo pidio (ddtt_, no ddta_)\", () => {",
                  "  pm.expect(j.ticket).to.be.a(\"string\").and.to.match(/^ddtt_/);",
                  "});",
                  "pm.test(\"es efimero: vida en segundos, no en horas\", () => {",
                  "  pm.expect(j.expires_in).to.be.a(\"number\").and.to.be.above(0);",
                  "  pm.expect(j.expires_in).to.be.at.most(120);",
                  "  const restan = (new Date(j.expires_at) - Date.now()) / 1000;",
                  "  pm.expect(restan).to.be.above(0);",
                  "  pm.expect(restan).to.be.at.most(j.expires_in + 30);",
                  "});",
                  "pm.test(\"no devuelve el hash almacenado\", () => {",
                  "  pm.expect(j).to.not.have.property(\"token_hash\");",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "26 · suppressions",
      "description": "Carga de la lista de supresion INTERNA del tenant (MT-SEC-60). Las filas entran en la lista DNC `internal_tenant` que el gate PRE-DIAL ya consulta en cada llamada y cada SMS — la misma que escribe el `STOP` de SMS. Tercera arista del triangulo de docs: sin este request, `POST /v1/suppressions/import` viviria en la spec publicable sin que nadie la ejercite.",
      "item": [
        {
          "name": "POST /v1/suppressions/import",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Numeros NUEVOS por corrida: `recorded` solo cuenta filas realmente",
                  "// escritas, asi que reusar un fijo haria que la 2a corrida contra el",
                  "// mismo stack leyera 0 y el test fallara por solapamiento, no por bug.",
                  "const rnd = () => String(Math.floor(Math.random() * 9000000) + 1000000);",
                  "pm.variables.set(\"sup_e164\", \"+1312\" + rnd());",
                  "pm.variables.set(\"sup_bare\", \"312\" + rnd());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: los 5 campos que el contrato declara required\", () => {",
                  "  [\"received\", \"accepted\", \"recorded\", \"rejected_count\"].forEach(k =>",
                  "    pm.expect(j[k], k).to.be.a(\"number\"));",
                  "  pm.expect(j.rejected).to.be.an(\"array\");",
                  "});",
                  "pm.test(\"comentario y linea en blanco NO cuentan como filas de datos\", () => {",
                  "  pm.expect(j.received).to.eql(3);",
                  "});",
                  "pm.test(\"la NANP desnuda normaliza igual que la E.164: 2 aceptadas\", () => {",
                  "  pm.expect(j.accepted).to.eql(2);",
                  "});",
                  "pm.test(\"numeros nuevos por corrida: lo aceptado se escribe\", () => {",
                  "  pm.expect(j.recorded).to.eql(j.accepted);",
                  "});",
                  "pm.test(\"la fila mala se REPORTA, no aborta el documento\", () => {",
                  "  pm.expect(j.rejected_count).to.eql(1);",
                  "  pm.expect(j.rejected).to.have.lengthOf(1);",
                  "  const r = j.rejected[0];",
                  "  pm.expect(r.line, \"linea 1-based del documento\").to.be.a(\"number\").and.to.be.above(0);",
                  "  pm.expect(r.value).to.be.a(\"string\");",
                  "  pm.expect(r.reason).to.be.a(\"string\");",
                  "});",
                  "pm.test(\"el valor rechazado vuelve ENMASCARADO, no en claro\", () => {",
                  "  pm.expect(j.rejected[0].value).to.not.eql(\"12345\");",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "text/csv"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "number,reason\n{{sup_e164}},client internal DNC\n{{sup_bare}},litigation hold\n# una linea de comentario se salta\n\n12345,numero corto: fila malformada a proposito\n",
              "options": {
                "raw": {
                  "language": "text"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/suppressions/import",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "suppressions",
                "import"
              ]
            },
            "description": "Sube el DNC interno del tenant como `text/csv`, una fila por numero (`number[,reason]`).\n\nEl documento de este request ejercita a proposito las cuatro clases de fila que la spec declara: una E.164, una NANP desnuda de 10 digitos (que debe normalizarse a `+1XXXXXXXXXX`), una linea de comentario y una en blanco (ambas se saltan), y una fila malformada.\n\nLa fila malformada es la que importa: la spec promete que un error tipografico NO es fatal — se REPORTA y el resto del fichero entra igual. Rechazar el documento entero por una fila dejaria marcables a los demas consumidores, que es la direccion CARA del error. Por eso el 200 no significa que todo se acepto: `rejected_count` es el autoritativo."
          }
        }
      ]
    },
    {
      "name": "26b · consents",
      "description": "El escritor del consentimiento de voz y SMS (core#2552, M-05): registrar con su prueba, revocar y cargar en bloque. Tercera arista del triangulo de docs: sin estos requests, `PUT`/`DELETE /v1/consents/{phone}` y `POST /v1/consents/import` vivirian en la spec publicable sin que nadie los ejercite. El orden importa: el PUT valido va primero y el DELETE antes del 409.",
      "item": [
        {
          "name": "PUT /v1/consents/{phone}",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Numero NUEVO por corrida: el stack sobrevive entre corridas y un numero fijo",
                  "// llegaria a la 2a ya revocado por el DELETE de esta carpeta.",
                  "const rnd = () => String(Math.floor(Math.random() * 9000000) + 1000000);",
                  "// El `+` va codificado: es un segmento de ruta.",
                  "pm.collectionVariables.set(\"consent_phone\", \"%2B1312\" + rnd());",
                  "pm.collectionVariables.set(\"consent_csv_phone\", \"+1312\" + rnd());",
                  "pm.collectionVariables.set(\"consent_at\", new Date(Date.now() - 30 * 86400000).toISOString());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", () => pm.response.to.have.status(200));",
                  "const c = pm.response.json().consent;",
                  "pm.test(\"shape: los 7 campos que el contrato declara required\", () => {",
                  "  [\"number\", \"status\", \"channel\", \"consented_at\", \"source\", \"evidence_ref\", \"text_version\"].forEach(k =>",
                  "    pm.expect(c, k).to.have.property(k));",
                  "  pm.expect(c.status).to.eql(\"granted\");",
                  "});",
                  "pm.test(\"el numero vuelve ENMASCARADO, no entero\", () => {",
                  "  pm.expect(c.number).to.match(/^\\*\\*\\*[0-9]{4}$/);",
                  "});",
                  "pm.test(\"la prueba se guarda tal cual\", () => {",
                  "  pm.expect(c.evidence_ref).to.eql(\"https://forms.example.com/submissions/8841\");",
                  "  pm.expect(c.text_version).to.eql(\"tcpa-v3\");",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/consents/{{consent_phone}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "consents",
                "{{consent_phone}}"
              ]
            },
            "description": "Registra el consentimiento que el tenant tiene (core#2552): `channel`, `consented_at`, `source` y `evidence_ref` obligatorios, `text_version` opcional. Solo una llave `dd_` o un login `account_admin`. El numero vuelve enmascarado.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"channel\": \"voice\",\n  \"consented_at\": \"{{consent_at}}\",\n  \"source\": \"web_form\",\n  \"evidence_ref\": \"https://forms.example.com/submissions/8841\",\n  \"text_version\": \"tcpa-v3\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "PUT /v1/consents/{phone} — sin evidence_ref (422)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 422\", () => pm.response.to.have.status(422));",
                  "pm.test(\"el error nombra el campo que falta\", () => {",
                  "  pm.expect(pm.response.json().error.message).to.include(\"evidence_ref\");",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/consents/{{consent_phone}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "consents",
                "{{consent_phone}}"
              ]
            },
            "description": "La prueba es obligatoria: un consentimiento sin rastro no se registra.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"channel\": \"voice\",\n  \"consented_at\": \"{{consent_at}}\",\n  \"source\": \"web_form\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "DELETE /v1/consents/{phone}",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", () => pm.response.to.have.status(200));",
                  "const c = pm.response.json().consent;",
                  "pm.test(\"queda revocado, con el instante y el numero enmascarado\", () => {",
                  "  pm.expect(c.status).to.eql(\"revoked\");",
                  "  pm.expect(c.revoked_at).to.be.a(\"string\");",
                  "  pm.expect(c.number).to.match(/^\\*\\*\\*[0-9]{4}$/);",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/consents/{{consent_phone}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "consents",
                "{{consent_phone}}"
              ]
            },
            "description": "Revoca el consentimiento (Reg F). Siempre aplica y no pide nada al llamante. Idempotente."
          }
        },
        {
          "name": "PUT /v1/consents/{phone} — tras la revocacion, con fecha anterior (409)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 409\", () => pm.response.to.have.status(409));",
                  "pm.test(\"codigo consent_revoked\", () => {",
                  "  pm.expect(pm.response.json().error.code).to.eql(\"consent_revoked\");",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/consents/{{consent_phone}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "consents",
                "{{consent_phone}}"
              ]
            },
            "description": "Un consentimiento solo levanta una revocacion si su `consented_at` es POSTERIOR a ella: el consumidor que dijo basta pesa mas que lo que el tenant afirma de antes.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"channel\": \"voice\",\n  \"consented_at\": \"{{consent_at}}\",\n  \"source\": \"web_form\",\n  \"evidence_ref\": \"https://forms.example.com/submissions/8841\",\n  \"text_version\": \"tcpa-v3\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "POST /v1/consents/import",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: los campos que el contrato declara required\", () => {",
                  "  [\"received\", \"recorded\", \"rejected_count\"].forEach(k => pm.expect(j[k], k).to.be.a(\"number\"));",
                  "  pm.expect(j.rejected).to.be.an(\"array\");",
                  "});",
                  "pm.test(\"una fila entra y la que no trae prueba se reporta\", () => {",
                  "  pm.expect(j.received).to.eql(2);",
                  "  pm.expect(j.recorded).to.eql(1);",
                  "  pm.expect(j.rejected_count).to.eql(1);",
                  "  pm.expect(j.rejected[0].reason).to.eql(\"missing_evidence_ref\");",
                  "});",
                  "pm.test(\"el valor rechazado vuelve ENMASCARADO\", () => {",
                  "  pm.expect(j.rejected[0].value).to.match(/^\\*\\*\\*[0-9]{4}$/);",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "text/csv"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/consents/import",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "consents",
                "import"
              ]
            },
            "description": "Carga consentimientos desde un `text/csv` (`number,channel,consented_at,source,evidence_ref[,text_version]`). Las filas malas se REPORTAN y su numero queda sin consentimiento.",
            "body": {
              "mode": "raw",
              "raw": "number,channel,consented_at,source,evidence_ref,text_version\n{{consent_csv_phone}},any,{{consent_at}},ivr_recording,rec-2026-10-01-77123,tcpa-v3\n+13125550123,voice,{{consent_at}},web_form,,\n",
              "options": {
                "raw": {
                  "language": "text"
                }
              }
            }
          }
        }
      ]
    },
    {
      "name": "27 · artificial-voice-disclosure",
      "description": "Divulgacion de voz artificial del carril NO-IA (core#211). El orden importa: el PUT valido va PRIMERO para que el GET tenga algo que leer. El caso 404 (tenant sin divulgacion) NO se ejercita aqui a proposito: la coleccion corre contra un stack cuyo tenant sobrevive entre corridas, asi que un 404 solo pasaria la primera vez — es justo la no-idempotencia de docs#67. Su cobertura vive en la suite :db de core.",
      "item": [
        {
          "name": "PUT /v1/artificial-voice-disclosure",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/artificial-voice-disclosure",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "artificial-voice-disclosure"
              ]
            },
            "description": "Alta del texto. Upsert por tenant: una sola fila viva, gana la ultima. source y actor los pone el SERVIDOR y no se mandan. La instruccion de baja lleva el marcador {optout_digit} donde va la tecla (core#1305): cada campana de TTS / IVR / encuesta lo sustituye por SU optout_digit al sintetizar, y sin marcador no marca (prompt_unavailable_reason: optout_instructions_missing). Se guarda tal cual, con el marcador.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"text\": \"Hola, esta llamada usa un asistente virtual automatizado de Acme Recovery. Para no recibir mas llamadas, pulse {optout_digit}.\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: ArtificialVoiceDisclosure {text, source, actor, valid}\", function () {",
                  "  pm.expect(j.artificial_voice_disclosure.text).to.be.a(\"string\");",
                  "  pm.expect(j.artificial_voice_disclosure.valid).to.eql(true);",
                  "});",
                  "pm.test(\"el marcador de la tecla se guarda tal cual (core#1305)\", function () {",
                  "  pm.expect(j.artificial_voice_disclosure.text).to.include(\"{optout_digit}\");",
                  "});",
                  "pm.test(\"la procedencia la pone el servidor, no el cuerpo\", function () {",
                  "  pm.expect(j.artificial_voice_disclosure.source).to.eql(\"api\");",
                  "  pm.expect(j.artificial_voice_disclosure.actor).to.be.a(\"string\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/artificial-voice-disclosure",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/artificial-voice-disclosure",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "artificial-voice-disclosure"
              ]
            },
            "description": "Lectura de lo almacenado. Un tenant SIN divulgacion contesta 404, nunca un 200 con cuerpo nulo."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"devuelve el texto que acaba de escribir el PUT\", function () {",
                  "  pm.expect(j.artificial_voice_disclosure.text).to.include(\"automatizado\");",
                  "  pm.expect(j.artificial_voice_disclosure.valid).to.eql(true);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/artificial-voice-disclosure — texto que no divulga (400)",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/artificial-voice-disclosure",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "artificial-voice-disclosure"
              ]
            },
            "description": "El contenido lo juzga el MISMO validador del carril de IA. Un texto que nunca dice que la voz es automatica es 400 y no se almacena nada.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"text\": \"Hello, this is Acme Recovery calling about your account.\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "const j = pm.response.json();",
                  "pm.test(\"el error nombra que el texto debe identificarse como automatizado\", function () {",
                  "  pm.expect(j.error.message).to.include(\"automated\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/artificial-voice-disclosure — la procedencia no es del cliente (400)",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/artificial-voice-disclosure",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "artificial-voice-disclosure"
              ]
            },
            "description": "source y actor son procedencia del servidor. Mandarlos es 400 nombrando la clave, no un descarte silencioso: un cliente que cree haber firmado una procedencia que no viajo es peor que uno que recibe un error.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"text\": \"Hola, esta llamada usa un asistente virtual automatizado de Acme Recovery.\",\n  \"source\": \"migration\",\n  \"actor\": \"someone-else\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "const j = pm.response.json();",
                  "pm.test(\"el error nombra las claves rechazadas\", function () {",
                  "  pm.expect(j.error.message).to.include(\"source\");",
                  "  pm.expect(j.error.message).to.include(\"actor\");",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "27b · disclosure-scripts",
      "description": "Contrato de la biblioteca de guiones por jurisdicción (core#1689): crear, aprobar, retirar y aislamiento RLS con un segundo tenant real. El alta auxiliar del tenant usa /v1/admin y queda fuera del triángulo tenant /v1.",
      "item": [
        {
          "name": "POST /v1/disclosure-scripts — draft del segundo tenant",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{disclosure_foreign_token}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/disclosure-scripts",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "disclosure-scripts"
              ]
            },
            "auth": {
              "type": "noauth"
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"jurisdiction\": \"*\",\n  \"debt_type\": \"*\",\n  \"text\": \"Foreign tenant notice {{$guid}}\",\n  \"legal_ref\": \"Postman RLS control\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const s = pm.response.json().script;",
                  "pm.collectionVariables.set(\"disclosure_foreign_script_id\", s.id);",
                  "pm.test(\"draft pertenece al segundo tenant\", function () {",
                  "  pm.expect(s.status).to.eql(\"draft\");",
                  "  pm.expect(s.tenant_id).to.eql(pm.collectionVariables.get(\"disclosure_foreign_tenant_id\"));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/disclosure-scripts — crear draft",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/disclosure-scripts",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "disclosure-scripts"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"jurisdiction\": \"*\",\n  \"debt_type\": \"*\",\n  \"text\": \"Tenant default notice {{$guid}}\",\n  \"legal_ref\": \"Postman contract integration-tests#720\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const s = pm.response.json().script;",
                  "pm.collectionVariables.set(\"disclosure_script_id\", s.id);",
                  "pm.collectionVariables.set(\"disclosure_script_sha256\", s.text_sha256);",
                  "pm.test(\"draft inmutable con digest derivado\", function () {",
                  "  pm.expect(s.status).to.eql(\"draft\");",
                  "  pm.expect(s.version).to.be.above(0);",
                  "  pm.expect(s.text_sha256).to.match(/^[0-9a-f]{64}$/);",
                  "  pm.expect(s.approval).to.eql(null);",
                  "  pm.expect(s.retirement).to.eql(null);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/disclosure-scripts/:id/approve",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/disclosure-scripts/{{disclosure_script_id}}/approve",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "disclosure-scripts",
                "{{disclosure_script_id}}",
                "approve"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"reviewed_by\": \"Postman legal reviewer\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const s = pm.response.json().script;",
                  "pm.test(\"aprobación sellada sobre la misma versión\", function () {",
                  "  pm.expect(s.id).to.eql(pm.collectionVariables.get(\"disclosure_script_id\"));",
                  "  pm.expect(s.status).to.eql(\"approved\");",
                  "  pm.expect(s.text_sha256).to.eql(pm.collectionVariables.get(\"disclosure_script_sha256\"));",
                  "  pm.expect(s.approval.reviewed_by).to.eql(\"Postman legal reviewer\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/disclosure-scripts — RLS entre tenants",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/disclosure-scripts",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "disclosure-scripts"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const rows = pm.response.json().scripts;",
                  "const mine = pm.collectionVariables.get(\"disclosure_script_id\");",
                  "const foreign = pm.collectionVariables.get(\"disclosure_foreign_script_id\");",
                  "pm.test(\"lista contiene la versión propia y no filtra la ajena\", function () {",
                  "  pm.expect(rows.some(function (s) { return s.id === mine && s.status === \"approved\"; })).to.eql(true);",
                  "  pm.expect(rows.some(function (s) { return s.id === foreign; })).to.eql(false);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/disclosure-scripts/:id/approve — id ajeno (404)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/disclosure-scripts/{{disclosure_foreign_script_id}}/approve",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "disclosure-scripts",
                "{{disclosure_foreign_script_id}}",
                "approve"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"reviewed_by\": \"must not cross tenants\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404 uniforme\", function () { pm.response.to.have.status(404); });",
                  "pm.test(\"404 del handler, no catch-all\", function () {",
                  "  const e = pm.response.json().error;",
                  "  pm.expect(e.code).to.eql(\"not_found\");",
                  "  pm.expect(e.message).to.not.include(\"no such route\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/disclosure-scripts/:id/retire — id ajeno (404)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/disclosure-scripts/{{disclosure_foreign_script_id}}/retire",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "disclosure-scripts",
                "{{disclosure_foreign_script_id}}",
                "retire"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404 uniforme\", function () { pm.response.to.have.status(404); });",
                  "pm.test(\"404 del handler, no catch-all\", function () {",
                  "  const e = pm.response.json().error;",
                  "  pm.expect(e.code).to.eql(\"not_found\");",
                  "  pm.expect(e.message).to.not.include(\"no such route\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/disclosure-scripts/:id/retire",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{baseUrl}}/v1/disclosure-scripts/{{disclosure_script_id}}/retire",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "disclosure-scripts",
                "{{disclosure_script_id}}",
                "retire"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const s = pm.response.json().script;",
                  "pm.test(\"retirada sellada y versión intacta\", function () {",
                  "  pm.expect(s.id).to.eql(pm.collectionVariables.get(\"disclosure_script_id\"));",
                  "  pm.expect(s.status).to.eql(\"retired\");",
                  "  pm.expect(s.text_sha256).to.eql(pm.collectionVariables.get(\"disclosure_script_sha256\"));",
                  "  pm.expect(s.retirement.actor).to.be.a(\"string\").and.to.not.be.empty;",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "28 · planned-shifts",
      "description": "Turno planificado (MT-ROAD-R11 FASE 2b, ADR #113 de core). Rutas de gestión, tenant scope. Los dos requests están aquí porque el triángulo router↔OpenAPI↔Postman se gatea en cada PR de este repo contra core@main y esas rutas ya viven allí (core#896); integration-tests#266 las trae también, reescribiendo el fichero entero.",
      "item": [
        {
          "name": "POST /v1/planned-shifts",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/planned-shifts",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "planned-shifts"
              ]
            },
            "description": "Crea un turno planificado para el agente del golden-flow. tenant_id nunca viaja en el cuerpo: lo pone el principal autenticado.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{postman_agent_id}}\",\n  \"start_time\": \"{{planned_shift_start}}\",\n  \"end_time\": \"{{planned_shift_end}}\",\n  \"planned_state\": \"available\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Un turno de una hora que empieza dentro de un día: el agente es el del golden-flow.",
                  "const start = new Date(Date.now() + 24 * 3600 * 1000);",
                  "const end = new Date(start.getTime() + 3600 * 1000);",
                  "pm.collectionVariables.set(\"planned_shift_start\", start.toISOString());",
                  "pm.collectionVariables.set(\"planned_shift_end\", end.toISOString());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: id uuid\", function () { pm.expect(j.id).to.match(/^[0-9a-f-]{36}$/); });",
                  "pm.collectionVariables.set(\"planned_shift_id\", j.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/planned-shifts",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/planned-shifts",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "planned-shifts"
              ]
            },
            "description": "Lista los turnos planificados del tenant."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: shifts array con el turno recién creado\", function () {",
                  "  pm.expect(j.shifts).to.be.an(\"array\");",
                  "  const mine = j.shifts.find(s => s.id === pm.collectionVariables.get(\"planned_shift_id\"));",
                  "  pm.expect(mine, \"el turno creado aparece\").to.be.an(\"object\");",
                  "  pm.expect(mine.planned_state).to.eql(\"available\");",
                  "  pm.expect(mine.agent_id).to.eql(pm.collectionVariables.get(\"postman_agent_id\"));",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "29 · webhooks",
      "description": "Webhooks salientes para CRMs (core#959, MT-CTI-04 fase 1, ADR #121): registro de endpoints. El secreto de firma viaja SOLO en el 201 del alta y en el 200 del rotate; aquí se asserta su forma y nunca se persiste (regla de las api_keys). La URL es un destino inerte que el allowlist del operador del compose (`WEBHOOK_EGRESS_ALLOWLIST`, entrada `crm.example.test`) exime del juicio de DNS: solo se guarda, nunca se llama en esta fase.",
      "item": [
        {
          "name": "POST /v1/webhooks",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks"
              ]
            },
            "description": "Alta. `tenant_id` y `secret` nunca viajan en el cuerpo (400 si se envían). El secreto se muestra UNA vez, fuera del objeto `webhook`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"https://crm.example.test/hooks/dialerdigital\",\n  \"events\": [\"call.ended\", \"promise.recorded\"]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: webhook enabled con url y events\", function () {",
                  "  pm.expect(j.webhook.id).to.match(/^[0-9a-f-]{36}$/);",
                  "  pm.expect(j.webhook.url).to.eql(\"https://crm.example.test/hooks/dialerdigital\");",
                  "  pm.expect(j.webhook.events).to.eql([\"call.ended\", \"promise.recorded\"]);",
                  "  pm.expect(j.webhook.enabled).to.eql(true);",
                  "  pm.expect(j.webhook.disabled_reason).to.eql(null);",
                  "  pm.expect(j.webhook.previous_secret_expires_at).to.eql(null);",
                  "});",
                  "pm.test(\"secreto una sola vez: whsec_ de 49 chars fuera del objeto, secret_hint = ultimos 4\", function () {",
                  "  pm.expect(j.secret).to.be.a(\"string\");",
                  "  pm.expect(j.secret).to.match(/^whsec_[A-Za-z0-9_-]{43}$/);",
                  "  pm.expect(j.webhook.secret_hint).to.eql(j.secret.slice(-4));",
                  "  pm.expect(j.webhook).to.not.have.property(\"secret\");",
                  "  pm.expect(j.webhook).to.not.have.property(\"tenant_id\");",
                  "});",
                  "// Solo el id y el hint se guardan: el secreto NO se persiste (regla de las api_keys).",
                  "pm.collectionVariables.set(\"webhook_id\", j.webhook.id);",
                  "pm.collectionVariables.set(\"webhook_secret_hint\", j.webhook.secret_hint);"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/webhooks",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks"
              ]
            },
            "description": "Listado: hint, nunca el secreto."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"lista contiene el endpoint recien creado, sin secreto\", function () {",
                  "  const mine = j.webhooks.find(w => w.id === pm.collectionVariables.get(\"webhook_id\"));",
                  "  pm.expect(mine).to.be.an(\"object\");",
                  "  pm.expect(mine.secret_hint).to.eql(pm.collectionVariables.get(\"webhook_secret_hint\"));",
                  "  j.webhooks.forEach(w => pm.expect(w).to.not.have.property(\"secret\"));",
                  "  pm.expect(pm.response.text()).to.not.include(\"whsec_\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/webhooks/{{webhook_id}}",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks/{{webhook_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks",
                "{{webhook_id}}"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: el mismo endpoint, hint y sin secreto\", function () {",
                  "  pm.expect(j.webhook.id).to.eql(pm.collectionVariables.get(\"webhook_id\"));",
                  "  pm.expect(j.webhook.secret_hint).to.eql(pm.collectionVariables.get(\"webhook_secret_hint\"));",
                  "  pm.expect(j.webhook).to.not.have.property(\"secret\");",
                  "  pm.expect(pm.response.text()).to.not.include(\"whsec_\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/webhooks/{{webhook_id}}",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks/{{webhook_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks",
                "{{webhook_id}}"
              ]
            },
            "description": "Cambia la suscripcion; el secreto no es campo del PATCH.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"events\": [\"call.ended\", \"sms.received\"]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"events actualizados, hint intacto\", function () {",
                  "  pm.expect(j.webhook.events).to.eql([\"call.ended\", \"sms.received\"]);",
                  "  pm.expect(j.webhook.secret_hint).to.eql(pm.collectionVariables.get(\"webhook_secret_hint\"));",
                  "  pm.expect(j.webhook).to.not.have.property(\"secret\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/webhooks/{{webhook_id}}/rotate-secret",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks/{{webhook_id}}/rotate-secret",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks",
                "{{webhook_id}}",
                "rotate-secret"
              ]
            },
            "description": "Rotacion: secreto NUEVO una vez; el anterior verifica hasta previous_secret_expires_at (24 h)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"secreto nuevo una vez, hint cambia, ventana abierta\", function () {",
                  "  pm.expect(j.secret).to.match(/^whsec_[A-Za-z0-9_-]{43}$/);",
                  "  pm.expect(j.webhook.secret_hint).to.eql(j.secret.slice(-4));",
                  "  pm.expect(j.webhook.secret_hint).to.not.eql(pm.collectionVariables.get(\"webhook_secret_hint\"));",
                  "  pm.expect(j.webhook.previous_secret_expires_at).to.be.a(\"string\");",
                  "  const closes = new Date(j.webhook.previous_secret_expires_at).getTime() - Date.now();",
                  "  pm.expect(closes).to.be.within(23 * 3600 * 1000, 25 * 3600 * 1000);",
                  "  pm.expect(j.webhook).to.not.have.property(\"secret\");",
                  "});",
                  "pm.collectionVariables.set(\"webhook_secret_hint\", j.webhook.secret_hint);"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/webhooks/{{webhook_id}}/deliveries",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks/{{webhook_id}}/deliveries",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks",
                "{{webhook_id}}",
                "deliveries"
              ]
            },
            "description": "Evidencia append-only del endpoint, mas nuevas primero y paginada por keyset. El endpoint creado en esta corrida todavia no tiene deliveries: la pagina vacia con `next_cursor: null` ES el contrato (200, nunca 404), y por eso el assert es determinista."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: pagina keyset con deliveries y next_cursor\", function () {",
                  "  pm.expect(j.deliveries).to.be.an(\"array\");",
                  "  pm.expect(j).to.have.property(\"next_cursor\");",
                  "  const STATUSES = [\"pending\", \"retry\", \"delivered\", \"deadletter\", \"endpoint_disabled\"];",
                  "  j.deliveries.forEach(function (d) {",
                  "    pm.expect(d.endpoint_id).to.eql(pm.collectionVariables.get(\"webhook_id\"));",
                  "    pm.expect(d.status).to.be.oneOf(STATUSES);",
                  "    pm.expect(d.attempts).to.be.an(\"array\");",
                  "  });",
                  "});",
                  "pm.test(\"endpoint recien creado: pagina vacia, no 404\", function () {",
                  "  pm.expect(j.deliveries.length).to.eql(0);",
                  "  pm.expect(j.next_cursor).to.eql(null);",
                  "});",
                  "pm.test(\"el secreto tampoco viaja en la evidencia\", function () {",
                  "  pm.expect(pm.response.text()).to.not.include(\"whsec_\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/webhooks/{{webhook_id}}/deliveries?status=inexistente",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks/{{webhook_id}}/deliveries?status=inexistente",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks",
                "{{webhook_id}}",
                "deliveries"
              ],
              "query": [
                {
                  "key": "status",
                  "value": "inexistente"
                }
              ]
            },
            "description": "Un filtro con valor desconocido es 400 NOMBRANDO el conjunto aceptado, nunca una pagina vacia: una lista vacia se lee como `no se envio nada`, que es justo la conclusion que este endpoint existe para desmentir."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "const j = pm.response.json();",
                  "pm.test(\"400 que nombra el conjunto aceptado de status\", function () {",
                  "  pm.expect(j.error.code).to.eql(\"bad_request\");",
                  "  pm.expect(j.error.message).to.include(\"status must be one of\");",
                  "  pm.expect(j.error.message).to.include(\"deadletter\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/webhooks/{{webhook_id}}/deliveries/{{ghost_uuid}}/redeliver",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks/{{webhook_id}}/deliveries/{{ghost_uuid}}/redeliver",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks",
                "{{webhook_id}}",
                "deliveries",
                "{{ghost_uuid}}",
                "redeliver"
              ]
            },
            "description": "Reintento manual: APPEND de un intento `retry` (`reason: manual_redelivery`) que el dispatcher recoge en su siguiente tick, de ahi el 202 y no 200. Aqui se ejercita con un delivery_id inexistente via VARIABLE (el endpoint de la corrida no tiene deliveries que reintentar), asi que el determinista es el 404 del handler. Sin cuerpo."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "const j = pm.response.json();",
                  "pm.test(\"404 del handler (not_found), no del catch-all\", function () {",
                  "  pm.expect(j.error.code).to.eql(\"not_found\");",
                  "  pm.expect(j.error.message).to.not.include(\"no such route\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/webhooks/{{webhook_id}}/test",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks/{{webhook_id}}/test",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks",
                "{{webhook_id}}",
                "test"
              ]
            },
            "description": "Envio de prueba sincrono (core#2704): UN `webhook.test` al endpoint ahora; un fallo del receptor sigue siendo 200 y el resultado va en `status` (`delivered` | `deadletter`) y `attempt.reason`. Sin cuerpo. Es el 1.º de los 5 envios por minuto permitidos al webhook; el siguiente request es el 6.º."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: resultado sincrono del envio de prueba\", function () {",
                  "  pm.expect(j.delivery_id).to.be.a(\"string\");",
                  "  pm.expect(j.event).to.eql(\"webhook.test\");",
                  "  pm.expect(j.status).to.be.oneOf([\"delivered\", \"deadletter\"]);",
                  "  pm.expect(j.attempt).to.be.an(\"object\");",
                  "  pm.expect(j.latency_ms).to.be.a(\"number\");",
                  "  pm.expect(j.latency_ms).to.be.at.least(0);",
                  "});",
                  "pm.test(\"el secreto no viaja en la respuesta\", function () {",
                  "  pm.expect(pm.response.text()).to.not.include(\"whsec_\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/webhooks/{{webhook_id}}/test (sexto en un minuto)",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks/{{webhook_id}}/test",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks",
                "{{webhook_id}}",
                "test"
              ]
            },
            "description": "Limite de 5 envios de prueba por webhook y minuto: el request anterior fue el 1.º; el prerequest manda 4 mas (2.º a 5.º, todos admitidos) y este es el 6.º -> 429 `rate_limited` con Retry-After. Sin cuerpo."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// core#2704: llena el cupo de 5/min del webhook (el request anterior fue el 1.º) para que este sea el 6.º.",
                  "for (let i = 0; i < 4; i++) {",
                  "  pm.sendRequest({",
                  "    url: pm.variables.get(\"baseUrl\") + \"/v1/webhooks/\" + pm.collectionVariables.get(\"webhook_id\") + \"/test\",",
                  "    method: \"POST\",",
                  "    header: { \"Authorization\": \"Bearer \" + pm.variables.get(\"api_token\") }",
                  "  }, function (err, res) {",
                  "    pm.test(\"envio previo \" + (i + 2) + \" de 5 -> 200\", function () { pm.expect(err, \"sendRequest\").to.equal(null); pm.expect(res.code).to.equal(200); });",
                  "  });",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 429\", function () { pm.response.to.have.status(429); });",
                  "const j = pm.response.json();",
                  "pm.test(\"429 rate_limited del webhook con Retry-After\", function () {",
                  "  pm.expect(j.error.code).to.eql(\"rate_limited\");",
                  "  const ra = Number(pm.response.headers.get(\"Retry-After\"));",
                  "  pm.expect(Number.isInteger(ra) && ra >= 1, \"Retry-After entero >= 1\").to.be.true;",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/webhooks/{{webhook_id}}",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks/{{webhook_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks",
                "{{webhook_id}}"
              ]
            },
            "description": "Deshabilita, nunca borra: la fila sigue en el listado con enabled=false."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"deshabilitado con razon, fila conservada\", function () {",
                  "  pm.expect(j.webhook.id).to.eql(pm.collectionVariables.get(\"webhook_id\"));",
                  "  pm.expect(j.webhook.enabled).to.eql(false);",
                  "  pm.expect(j.webhook.disabled_reason).to.eql(\"disabled_by_api\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/webhooks/{{ghost_uuid}}",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks/{{ghost_uuid}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks",
                "{{ghost_uuid}}"
              ]
            },
            "description": "id inexistente via VARIABLE (nunca literal): 404 determinista del handler, no del catch-all."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "const j = pm.response.json();",
                  "pm.test(\"404 del handler (not_found), no del catch-all\", function () {",
                  "  pm.expect(j.error.code).to.eql(\"not_found\");",
                  "  pm.expect(j.error.message).to.not.include(\"no such route\");",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "30 · compliance-audit-chain",
      "item": [
        {
          "name": "GET /v1/compliance/decisions/:decision_id (la cita del 422, resuelta)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/compliance/decisions/{{decision_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "compliance",
                "decisions",
                "{{decision_id}}"
              ]
            },
            "description": "Resuelve el decision_id que el 422 de POST /v1/sms/messages acaba de devolver: el eslabon sellado de la cadena append-only mas el veredicto de integridad sobre el. Es el round-trip entero de la promesa de los docs: la cita se entrega en el refusal y aqui se comprueba."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test(\"resuelve EL MISMO decision_id que entrego el 422\", function () {",
                  "  pm.expect(j.decision.decision_id).to.eql(pm.collectionVariables.get(\"decision_id\"));",
                  "});",
                  "pm.test(\"el eslabon trae el payload canonico que se hasheo\", function () {",
                  "  pm.expect(j.decision.payload).to.be.an(\"object\");",
                  "  pm.expect(j.decision.payload.decision_id).to.eql(j.decision.decision_id);",
                  "  pm.expect(j.decision.seq).to.be.a(\"number\");",
                  "  pm.expect(j.decision.allowed).to.eql(false);",
                  "});",
                  "pm.test(\"veredicto: TRES booleanos, no uno\", function () {",
                  "  pm.expect(j.verification.payload_hash_matches).to.eql(true);",
                  "  pm.expect(j.verification.chains_to_previous).to.eql(true);",
                  "});",
                  "pm.test(\"el estado distingue la PUNTA de la cadena en vez de disfrazarla\", function () {",
                  "  // Este eslabon acaba de sellarse, asi que lo normal en una corrida limpia",
                  "  // es que sea la PUNTA: no hay sucesor que lo atestigue todavia y el estado",
                  "  // es `intact_unwitnessed`, no `intact`. Si otra decision se sello despues",
                  "  // (la coleccion crece), pasa a `intact` con el testigo en true. Los dos son",
                  "  // sanos; `tampered` no lo seria. Lo que se ancla es la COHERENCIA entre el",
                  "  // estado, `is_chain_tip` y el testigo — que es lo que un solo booleano no",
                  "  // podia expresar.",
                  "  pm.expect([\"intact\", \"intact_unwitnessed\"]).to.include(j.verification.status);",
                  "  if (j.verification.is_chain_tip) {",
                  "    pm.expect(j.verification.status).to.eql(\"intact_unwitnessed\");",
                  "    pm.expect(j.verification.successor_chains_to_this).to.eql(null);",
                  "  } else {",
                  "    pm.expect(j.verification.status).to.eql(\"intact\");",
                  "    pm.expect(j.verification.successor_chains_to_this).to.eql(true);",
                  "  }",
                  "});",
                  "pm.test(\"la receta viaja declarada y los hashes son sha256 hex\", function () {",
                  "  pm.expect(j.verification.algorithm).to.eql(\"sha256\");",
                  "  pm.expect(j.verification.recipe).to.be.a(\"string\").and.to.not.be.empty;",
                  "  [j.verification.hash, j.verification.prev_hash, j.verification.genesis_hash]",
                  "    .forEach((h) => pm.expect(h).to.match(/^[0-9a-f]{64}$/));",
                  "});",
                  "// NO se re-ejecuta el sha256 aqui A PROPOSITO: reproducir el JSON canonico de",
                  "// Elixir en el sandbox (escapes, unicode, orden de claves) es una fuente de",
                  "// rojos falsos. La recomputacion ya esta probada donde se puede hacer sin",
                  "// ambiguedad: core, test/db/compliance_decision_lookup_api_test.exs. Aqui se",
                  "// ancla el CONTRATO: que la receta y todos sus insumos llegan completos.",
                  "pm.test(\"nunca el E.164: solo el last-4 enmascarado\", function () {",
                  "  pm.expect(j.decision.to_number_masked).to.match(/^\\*\\*\\*[0-9]{4}$/);",
                  "  pm.expect(pm.response.text()).to.not.include(\"+19995550123\");",
                  "});"
                ]
              }
            },
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Si el 422 de sms no llego a sembrar la variable, la URL tendria un segmento",
                  "// vacio y caeria en el catch-all del router SIN ejercitar la ruta real.",
                  "// Se siembra un UUID aleatorio para que el fallo sea legible.",
                  "if (!pm.collectionVariables.get(\"decision_id\")) {",
                  "  pm.collectionVariables.set(\"decision_id\", pm.variables.replaceIn(\"{{$guid}}\"));",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/compliance/decisions/:decision_id (fantasma -> 404)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/compliance/decisions/{{ghost_uuid}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "compliance",
                "decisions",
                "{{ghost_uuid}}"
              ]
            },
            "description": "Un decision_id inexistente -> 404, el MISMO que devuelve el de otro tenant (RLS lee cero filas): la ruta no es un oraculo de existencia."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", () => pm.response.to.have.status(404));",
                  "pm.test(\"404 del endpoint real (no catch-all 'no such route')\", function () {",
                  "  pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\");",
                  "});",
                  "pm.test(\"el 404 no filtra nada de la cadena\", function () {",
                  "  const j = pm.response.json();",
                  "  pm.expect(j.error.code).to.eql(\"not_found\");",
                  "  pm.expect(j).to.not.have.property(\"decision\");",
                  "  pm.expect(j).to.not.have.property(\"verification\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/compliance/sources (estado de las fuentes: DNC, registros estatales, litigator, RND)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/compliance/sources",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "compliance",
                "sources"
              ]
            },
            "description": "Lectura barata para el panel de ajustes (core#2167, dashboard#958): por fuente DNC (federal, state, internal_global y cada registro estatal declarado) el status que el gate usa de verdad, updated_at (cuando se corto el dato de origen), expires_at (primer instante en que el gate lo da por stale), source y record_count; litigator (no es lista propia: available=false, cubierto por internal_global); y el RND del tenant (proveedor, last_collected_at, valid_since, status current/stale/never). No recorre leads ni evalua gates. No se asertan valores de estado concretos: dependen de lo que el stack tenga importado; se ancla el CONTRATO (forma, enums y coherencia entre estado y fechas)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "const isoOrNull = (v) => v === null || (typeof v === \"string\" && !isNaN(Date.parse(v)));",
                  "pm.test(\"forma: as_of, dnc, litigator, rnd\", function () {",
                  "  pm.expect(Object.keys(j).sort()).to.eql([\"as_of\", \"dnc\", \"litigator\", \"rnd\"]);",
                  "  pm.expect(isNaN(Date.parse(j.as_of))).to.eql(false);",
                  "  pm.expect(Object.keys(j.dnc).sort()).to.eql([\"federal\", \"internal_global\", \"state\", \"state_registries\"]);",
                  "});",
                  "pm.test(\"cada fuente DNC: estado, fechas y techo coherentes (ausente = fechas null, nunca una fecha inventada)\", function () {",
                  "  const sources = [j.dnc.federal, j.dnc.state, j.dnc.internal_global].concat(Object.values(j.dnc.state_registries));",
                  "  pm.expect(Object.keys(j.dnc.state_registries)).to.not.be.empty;",
                  "  sources.forEach((s) => {",
                  "    pm.expect(Object.keys(s).sort()).to.eql([\"age_days\", \"expires_at\", \"max_age_days\", \"record_count\", \"source\", \"status\", \"updated_at\"]);",
                  "    pm.expect([\"provisioned\", \"stale\", \"absent\"]).to.include(s.status);",
                  "    pm.expect(s.max_age_days).to.be.a(\"number\");",
                  "    pm.expect(isoOrNull(s.updated_at) && isoOrNull(s.expires_at)).to.eql(true);",
                  "    if (s.status === \"absent\") {",
                  "      [s.updated_at, s.expires_at, s.age_days, s.record_count].forEach((v) => pm.expect(v).to.eql(null));",
                  "    } else {",
                  "      pm.expect(Date.parse(s.expires_at)).to.be.above(Date.parse(s.updated_at));",
                  "      pm.expect(s.age_days).to.be.a(\"number\");",
                  "      pm.expect(s.status === \"stale\").to.eql(Date.parse(j.as_of) >= Date.parse(s.expires_at));",
                  "    }",
                  "  });",
                  "});",
                  "pm.test(\"litigator no es una lista de core: available=false y dice quien la cubre\", function () {",
                  "  pm.expect(j.litigator).to.eql({ available: false, covered_by: \"internal_global\" });",
                  "});",
                  "pm.test(\"rnd: proveedor, estado del ciclo y fechas\", function () {",
                  "  pm.expect(Object.keys(j.rnd).sort()).to.eql([\"database_updated_at\", \"last_collected_at\", \"provider\", \"status\", \"valid_since\"]);",
                  "  pm.expect([\"declared\", \"stub\", \"no_verificable\"]).to.include(j.rnd.provider.status);",
                  "  pm.expect([\"current\", \"stale\", \"never\"]).to.include(j.rnd.status);",
                  "  pm.expect(isNaN(Date.parse(j.rnd.valid_since))).to.eql(false);",
                  "  pm.expect(isoOrNull(j.rnd.last_collected_at) && isoOrNull(j.rnd.database_updated_at)).to.eql(true);",
                  "  pm.expect(j.rnd.status === \"never\").to.eql(j.rnd.last_collected_at === null);",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "31 · academy",
      "description": "La constancia de entrenamiento de la Academy (core#1020, MT-CLI-20 + MT-UX-02 (a)). Reusa el asiento que creo la carpeta 09 ({{postman_agent_id}}) a proposito: el progreso es POR AGENTE, que es exactamente lo que la version en localStorage no podia hacer.",
      "item": [
        {
          "name": "POST /v1/academy/completions",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/academy/completions",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "academy",
                "completions"
              ]
            },
            "description": "Registra un hito del agente. `occurred_at` NO es entrada: lo sella el servidor, porque una constancia cuyo instante elige el navegador del auditado no es evidencia.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{postman_agent_id}}\",\n  \"kind\": \"lesson\",\n  \"item_id\": \"ch1.dashboard\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: el hito queda atribuido al agente\", function () {",
                  "  pm.expect(j.id).to.be.a(\"string\");",
                  "  pm.expect(j.agent_id).to.eql(pm.collectionVariables.get(\"postman_agent_id\"));",
                  "  pm.expect(j.kind).to.eql(\"lesson\");",
                  "  pm.expect(j.item_id).to.eql(\"ch1.dashboard\");",
                  "  pm.expect(j.occurred_at).to.be.a(\"string\");",
                  "});",
                  "pm.collectionVariables.set(\"academy_completion_id\", j.id);",
                  "pm.collectionVariables.set(\"academy_completed_at\", j.occurred_at);"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/academy/completions (idempotente: misma fila, misma fecha)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/academy/completions",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "academy",
                "completions"
              ]
            },
            "description": "Completar dos veces la misma leccion no es un hecho nuevo: vuelve la PRIMERA fila con su fecha original. Es lo que impide que un auditor tenga que elegir entre dos fechas.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{postman_agent_id}}\",\n  \"kind\": \"lesson\",\n  \"item_id\": \"ch1.dashboard\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"misma fila y misma fecha que la primera vez\", function () {",
                  "  pm.expect(j.id).to.eql(pm.collectionVariables.get(\"academy_completion_id\"));",
                  "  pm.expect(j.occurred_at).to.eql(pm.collectionVariables.get(\"academy_completed_at\"));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/academy/progress",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/academy/progress?agent_id={{postman_agent_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "academy",
                "progress"
              ],
              "query": [
                {
                  "key": "agent_id",
                  "value": "{{postman_agent_id}}"
                }
              ]
            },
            "description": "El estado DERIVADO del agente: capitulo y nivel no se guardan, se calculan del stream."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: el doc derivado del agente\", function () {",
                  "  pm.expect(j.agent_id).to.eql(pm.collectionVariables.get(\"postman_agent_id\"));",
                  "  pm.expect(j.lessons_done).to.include(\"ch1.dashboard\");",
                  "  pm.expect(j.badges).to.be.an(\"object\");",
                  "  pm.expect(j.updated_at).to.eql(pm.collectionVariables.get(\"academy_completed_at\"));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/academy/transcript",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/academy/transcript",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "academy",
                "transcript"
              ]
            },
            "description": "La constancia: quien completo que y cuando, con nombre, correo y rol del agente."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: filas con la persona y el hito\", function () {",
                  "  pm.expect(j.total).to.be.at.least(1);",
                  "  const row = j.rows.find(function (r) { return r.item_id === \"ch1.dashboard\"; });",
                  "  pm.expect(row).to.not.be.undefined;",
                  "  pm.expect(row.agent_id).to.eql(pm.collectionVariables.get(\"postman_agent_id\"));",
                  "  pm.expect(row.agent_email).to.be.a(\"string\");",
                  "  pm.expect(row.agent_role).to.be.a(\"string\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/academy/transcript?format=csv",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/academy/transcript?format=csv",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "academy",
                "transcript"
              ],
              "query": [
                {
                  "key": "format",
                  "value": "csv"
                }
              ]
            },
            "description": "El mismo artefacto en el formato que un auditor abre, con su sha256 de cadena de custodia."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"es el artefacto CSV con su sha256\", function () {",
                  "  pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"text/csv\");",
                  "  pm.expect(pm.response.headers.get(\"Content-Disposition\")).to.include(\"academy-transcript_\");",
                  "  pm.expect(pm.response.headers.get(\"X-Artifact-SHA256\")).to.match(/^[0-9a-f]{64}$/);",
                  "  const body = pm.response.text();",
                  "  pm.expect(body).to.include(\"report,academy_transcript\");",
                  "  pm.expect(body).to.include(\"agent_id,agent_name,agent_email,agent_role,kind,item_id,completed_at\");",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "32 · me-scorecard",
      "description": "GET /v1/me/scorecard (core#1036, core#1039): el marcador del cobrador se deriva del TOKEN, no de un parametro. Carpeta auto-sembrada: persona ddu_ (invitada y canjeada, ADR #182 de core) -> 403 sin enlace -> asiento con el mismo email (el motor los liga, ADR #131 de core) -> 200 con ventana fallback_12h / explicit -> agent_id rechazado por nombre -> dd_ sin asiento.",
      "item": [
        {
          "name": "POST /v1/users (persona del marcador: invitacion, 202 fijo)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/users",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "users"
              ]
            },
            "description": "La persona (`users`, carril ddu_) que va a leer SU marcador. Nace sin asiento: el enlace `users_agents.user_id` lo pone el motor por email (ADR #131 de core) cuando se cree el asiento con el mismo correo, mas abajo. Desde el ADR #182 de core (core#1352) el alta INVITA: no lleva `password`, contesta un `202` con un cuerpo FIJO —el mismo, byte a byte, este la direccion libre o sea ya de otro tenant— y la persona no existe hasta que canjea el enlace del correo. El email se fija en el pre-request porque la respuesta ya no lo devuelve.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"{{scorecard_user_email}}\",\n  \"name\": \"Scorecard User\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set(\"scorecard_user_email\", \"scorecard-\" + Date.now() + \"@e2e.test\");",
                  "pm.collectionVariables.set(\"scorecard_user_id\", \"\");",
                  "pm.collectionVariables.set(\"scorecard_invite_token\", \"\");"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 202\", function () { pm.response.to.have.status(202); });",
                  "pm.test(\"cuerpo FIJO de la invitacion, byte a byte (ADR #182 de core)\", function () {",
                  "  pm.expect(pm.response.text()).to.eql(\"{\\\"message\\\":\\\"invitation recorded\\\",\\\"status\\\":\\\"invited\\\"}\");",
                  "});",
                  "pm.test(\"no devuelve usuario ni material secreto\", function () {",
                  "  pm.expect(pm.response.json()).to.not.have.property(\"user\");",
                  "  pm.expect(pm.response.text()).to.not.include(\"ddr_\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/users (persona del marcador: invitacion abierta, la persona aun no existe)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/users",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "users"
              ]
            },
            "description": "La invitacion se lista en `invitations` como `pending`; en `users` no hay nadie con ese email hasta el canje. Nunca expone el hash del token."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "const email = pm.collectionVariables.get(\"scorecard_user_email\");",
                  "pm.test(\"invitacion pending con la forma del contrato\", function () {",
                  "  const inv = j.invitations.find(i => i.email === email);",
                  "  pm.expect(inv, \"invitacion abierta para \" + email).to.be.an(\"object\");",
                  "  pm.expect(inv.status).to.eql(\"pending\");",
                  "  pm.expect(inv).to.include.all.keys(\"id\", \"email\", \"status\", \"expires_at\", \"created_at\");",
                  "  pm.expect(JSON.stringify(j)).to.not.include(\"token_hash\");",
                  "});",
                  "pm.test(\"la persona NO existe antes del canje\", function () {",
                  "  pm.expect(j.users.map(u => u.email)).to.not.include(email);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/auth/password-reset/confirm (persona del marcador: canje de la invitacion, nace la persona)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/password-reset/confirm",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "password-reset",
                "confirm"
              ]
            },
            "description": "SIN bearer. El canje de una invitacion va por la MISMA ruta anonima que el restablecimiento (ADR #182 de core): la persona nace aqui, con la contrasena que elige ella.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"token\": \"{{scorecard_invite_token}}\",\n  \"new_password\": \"sB9-copper-lynx-61\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: {status: ok}\", function () {",
                  "  pm.expect(pm.response.json().status).to.eql(\"ok\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/users (persona del marcador: la persona existe, la invitacion salio de la lista)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/users",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "users"
              ]
            },
            "description": "Tras el canje la persona aparece en `users` (activa) y su invitacion deja `invitations`."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "const email = pm.collectionVariables.get(\"scorecard_user_email\");",
                  "const mine = j.users.find(u => u.email === email);",
                  "pm.test(\"la persona invitada ya es usuaria activa\", function () {",
                  "  pm.expect(mine, \"usuario \" + email).to.be.an(\"object\");",
                  "  pm.expect(mine.status).to.eql(\"active\");",
                  "  pm.expect(JSON.stringify(j)).to.not.include(\"password_hash\");",
                  "});",
                  "pm.test(\"la invitacion canjeada ya no esta abierta\", function () {",
                  "  pm.expect(j.invitations.map(i => i.email)).to.not.include(email);",
                  "});",
                  "if (mine) { pm.collectionVariables.set(\"scorecard_user_id\", mine.id); }"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/auth/login (sesion ddu_ del marcador)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/login",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "login"
              ]
            },
            "description": "Sesion ddu_ propia de esta carpeta: la de `21 · users+auth` muere en el confirm del reset. El freno de login es por email, asi que un correo nuevo trae presupuesto nuevo.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"{{scorecard_user_email}}\",\n  \"password\": \"sB9-copper-lynx-61\"\n}"
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"token ddu_ de la persona del marcador\", function () {",
                  "  pm.expect(j.token).to.match(/^ddu_/);",
                  "  pm.expect(j.user.id).to.eql(pm.collectionVariables.get(\"scorecard_user_id\"));",
                  "});",
                  "pm.collectionVariables.set(\"scorecard_ddu_token\", j.token);"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/me/scorecard (ddu_ sin asiento ligado -> 403 seat_not_linked)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/me/scorecard",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me",
                "scorecard"
              ]
            },
            "description": "Fail-closed: una persona sin asiento activo ligado recibe 403 `seat_not_linked`. NO se cae a un parametro ni al cuerpo, que seria volver a creerle al cliente de quien es el asiento.",
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{scorecard_ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "const j = pm.response.json();",
                  "pm.test(\"403 seat_not_linked, sin cifra alguna\", function () {",
                  "  pm.expect(j.error.code).to.eql(\"seat_not_linked\");",
                  "  pm.expect(j).to.not.have.property(\"calls\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/me (ddu_ del marcador, ANTES de ligar -> seat_not_linked)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/me",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me"
              ]
            },
            "description": "core#1059 (ADR #137). El ANTES del par: la MISMA persona y el MISMO token que el request de despues del POST /v1/agents. Lo unico que cambia entre los dos es el enlace del asiento, asi que el par es un control y no dos aserciones sueltas. Y aqui la respuesta es 200, no el 403 de /me/scorecard de arriba: /me es el validador del bearer del SPA y una persona sin asiento es un estado legitimo.",
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{scorecard_ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"seat: sin asiento ligado -> seat_not_linked, y 200 (no 403)\", function () {",
                  "  pm.expect(j.seat_source, \"seat_source\").to.equal(\"seat_not_linked\");",
                  "  pm.expect(j.seat, \"seat\").to.equal(null);",
                  "  pm.expect(j.user.id, \"user.id\").to.eql(pm.collectionVariables.get(\"scorecard_user_id\"));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/agents (asiento con el MISMO email: el motor lo liga)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/agents",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents"
              ]
            },
            "description": "Asiento (`users_agents`) con el mismo correo que la persona de arriba: el trigger `users_agents_link_user` pone `user_id` al insertar (ADR #131 de core). `user_id` no es casteable por la API a proposito: ninguna superficie deja al cliente decidir de quien es el asiento.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Scorecard Seat\",\n  \"email\": \"{{scorecard_user_email}}\",\n  \"role\": \"agent\"\n}"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"asiento activo con el correo de la persona\", function () {",
                  "  pm.expect(j.agent.email).to.eql(pm.collectionVariables.get(\"scorecard_user_email\"));",
                  "  pm.expect(j.agent.status).to.eql(\"active\");",
                  "});",
                  "pm.collectionVariables.set(\"scorecard_agent_id\", j.agent.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/me (ddu_ del marcador, YA ligada -> seat_source token)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/me",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me"
              ]
            },
            "description": "core#1059 (ADR #137). El DESPUES del par, y la UNICA cobertura de la rama `token` en toda la coleccion: es la rama que existe para desbloquear la UI de MT-CLI-15 (que el agente deje de elegir asiento en un desplegable donde N-1 opciones dan 403 seat_mismatch). Sin este request, core podria romper esa rama y la coleccion seguiria verde.",
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{scorecard_ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"seat: la rama token, con el asiento recien ligado\", function () {",
                  "  pm.expect(j.seat_source, \"seat_source\").to.equal(\"token\");",
                  "  pm.expect(j.seat.id, \"seat.id\").to.eql(pm.collectionVariables.get(\"scorecard_agent_id\"));",
                  "});",
                  "// sip_extension VIAJA siempre, pero su valor puede ser null: el changeset de Agent no lo",
                  "// exige y este mismo asiento se creo sin el. Se asserta la PRESENCIA, no el tipo.",
                  "pm.test(\"seat trae sip_extension (el valor puede ser null)\", function () {",
                  "  pm.expect(j.seat).to.have.property(\"sip_extension\");",
                  "});",
                  "// Este guard SI puede ponerse rojo: aqui seat es un objeto de verdad.",
                  "pm.test(\"seat no filtra password_hash\", function () {",
                  "  pm.expect(j.seat).to.not.have.property(\"password_hash\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/me/scorecard (ddu_ ligada, sin fichaje -> 200 ventana fallback_12h)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/me/scorecard",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me",
                "scorecard"
              ]
            },
            "description": "El marcador PROPIO: el asiento sale del token (enlace `users_agents.user_id`), nunca de un parametro. Sin fichaje activo la ventana por defecto es `now - 12h` y el contrato obliga a decirlo (`window.source: fallback_12h`): un cliente muestra esa cifra como estimada, nunca como `el turno`. `calls` cuenta INTENTOS (el original mas sus correcciones son UNA llamada) y no trae ratios ni minutos: la ocupacion derivada es `GET /v1/agents/occupancy`.",
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{scorecard_ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"ventana estimada declarada como tal\", function () {",
                  "  pm.expect(j.window.source).to.eql(\"fallback_12h\");",
                  "});",
                  "pm.test(\"shape: agent DERIVADO del token, ventana con source, calls y promises a cero\", function () {",
                  "  pm.expect(j.agent.id).to.eql(pm.collectionVariables.get(\"scorecard_agent_id\"));",
                  "  pm.expect(j.agent).to.have.all.keys(\"id\", \"name\", \"sip_extension\");",
                  "  pm.expect(j.window).to.have.all.keys(\"from\", \"to\", \"source\");",
                  "  pm.expect(j.window.from < j.window.to).to.eql(true);",
                  "  pm.expect(j.calls).to.have.all.keys(\"total\", \"in_flight\", \"by_disposition\");",
                  "  const settled = Object.values(j.calls.by_disposition).reduce(function (a, b) { return a + b; }, 0);",
                  "  pm.expect(j.calls.total).to.eql(j.calls.in_flight + settled);",
                  "  pm.expect(j.calls.total).to.eql(0);",
                  "  pm.expect(j.promises).to.have.all.keys(\"count\", \"by_status\", \"amount_cents\");",
                  "  pm.expect(j.promises.count).to.eql(0);",
                  "  pm.expect(j.promises.by_status).to.eql({ pending: 0, kept: 0, broken: 0, cancelled: 0 });",
                  "  pm.expect(j.promises.amount_cents).to.eql({ USD: 0, MXN: 0 });",
                  "});",
                  "pm.test(\"sin nota de promesa, sin telefonos, sin comision\", function () {",
                  "  pm.expect(j).to.have.all.keys(\"agent\", \"window\", \"calls\", \"promises\");",
                  "  const text = pm.response.text();",
                  "  pm.expect(text).to.not.include(\"\\\"note\\\"\");",
                  "  pm.expect(text).to.not.include(\"commission\");",
                  "  pm.expect(text).to.not.include(\"phone\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/me/scorecard?from&to (ventana explicita -> source explicit)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/me/scorecard?from={{scorecard_from}}&to={{scorecard_to}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me",
                "scorecard"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "{{scorecard_from}}"
                },
                {
                  "key": "to",
                  "value": "{{scorecard_to}}"
                }
              ]
            },
            "description": "`from`/`to` opcionales (RFC 3339, semiabierta). Con `from` del cliente la respuesta lo dice: `window.source: explicit`, y la ventana vuelve tal cual se pidio.",
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{scorecard_ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Ventana explicita RELATIVA al reloj: la ultima hora hasta ahora.",
                  "const to = new Date();",
                  "const from = new Date(to.getTime() - 60 * 60 * 1000);",
                  "pm.collectionVariables.set(\"scorecard_from\", from.toISOString());",
                  "pm.collectionVariables.set(\"scorecard_to\", to.toISOString());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"ventana del cliente, devuelta tal cual y marcada explicit\", function () {",
                  "  pm.expect(j.window.source).to.eql(\"explicit\");",
                  "  pm.expect(Date.parse(j.window.from)).to.eql(Date.parse(pm.collectionVariables.get(\"scorecard_from\")));",
                  "  pm.expect(Date.parse(j.window.to)).to.eql(Date.parse(pm.collectionVariables.get(\"scorecard_to\")));",
                  "});",
                  "pm.test(\"shape: agent DERIVADO del token, ventana con source, calls y promises a cero\", function () {",
                  "  pm.expect(j.agent.id).to.eql(pm.collectionVariables.get(\"scorecard_agent_id\"));",
                  "  pm.expect(j.agent).to.have.all.keys(\"id\", \"name\", \"sip_extension\");",
                  "  pm.expect(j.window).to.have.all.keys(\"from\", \"to\", \"source\");",
                  "  pm.expect(j.window.from < j.window.to).to.eql(true);",
                  "  pm.expect(j.calls).to.have.all.keys(\"total\", \"in_flight\", \"by_disposition\");",
                  "  const settled = Object.values(j.calls.by_disposition).reduce(function (a, b) { return a + b; }, 0);",
                  "  pm.expect(j.calls.total).to.eql(j.calls.in_flight + settled);",
                  "  pm.expect(j.calls.total).to.eql(0);",
                  "  pm.expect(j.promises).to.have.all.keys(\"count\", \"by_status\", \"amount_cents\");",
                  "  pm.expect(j.promises.count).to.eql(0);",
                  "  pm.expect(j.promises.by_status).to.eql({ pending: 0, kept: 0, broken: 0, cancelled: 0 });",
                  "  pm.expect(j.promises.amount_cents).to.eql({ USD: 0, MXN: 0 });",
                  "});",
                  "pm.test(\"sin nota de promesa, sin telefonos, sin comision\", function () {",
                  "  pm.expect(j).to.have.all.keys(\"agent\", \"window\", \"calls\", \"promises\");",
                  "  const text = pm.response.text();",
                  "  pm.expect(text).to.not.include(\"\\\"note\\\"\");",
                  "  pm.expect(text).to.not.include(\"commission\");",
                  "  pm.expect(text).to.not.include(\"phone\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/me/scorecard?agent_id={{ghost_uuid}} (rechazado POR NOMBRE -> 400)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/me/scorecard?agent_id={{ghost_uuid}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me",
                "scorecard"
              ],
              "query": [
                {
                  "key": "agent_id",
                  "value": "{{ghost_uuid}}"
                }
              ]
            },
            "description": "`agent_id` se rechaza POR NOMBRE con 400, nunca se ignora: un cliente no puede creer que leyo las cifras de un companero. Se comprueba ANTES que nada, asi que la respuesta no trae ningun dato ni dice cual es el asiento propio.",
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{scorecard_ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "const j = pm.response.json();",
                  "pm.test(\"400 que explica la regla y no contesta la pregunta\", function () {",
                  "  pm.expect(j.error.code).to.eql(\"bad_request\");",
                  "  pm.expect(j.error.message).to.include(\"agent_id not accepted here\");",
                  "  pm.expect(j).to.not.have.property(\"agent\");",
                  "  pm.expect(pm.response.text()).to.not.include(pm.collectionVariables.get(\"scorecard_agent_id\"));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/me/scorecard (dd_ machine key -> 403 sin asiento)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/me/scorecard",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me",
                "scorecard"
              ]
            },
            "description": "La llave de MAQUINA (`dd_`, la auth de la coleccion) no tiene persona detras de la que derivar un asiento: 403 `forbidden`, no un asiento vacio ni el del cuerpo."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "const j = pm.response.json();",
                  "pm.test(\"403 forbidden: una dd_ no tiene asiento propio\", function () {",
                  "  pm.expect(j.error.code).to.eql(\"forbidden\");",
                  "  pm.expect(j.error.message).to.include(\"has no seat of its own\");",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "33 · disposition-rules",
      "description": "Politica de reintentos por resultado del tenant (core#1158, R10 / ADR #31 de core). El orden importa y la carpeta es idempotente entre corridas: siembra las canonicas, las lee, las reemplaza, prueba el 422 y CIERRA con `PUT {rules: []}`, que apaga todas las reglas. Asi el tenant del stack, que sobrevive entre corridas, vuelve siempre a la politica inerte y ninguna corrida posterior del golden-flow ve una deuda aparcada por esta carpeta. El aislamiento entre tenants (RLS) y la auditoria atomica viven en la suite :db de core.",
      "item": [
        {
          "name": "POST /v1/disposition-rules/seed-defaults",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{baseUrl}}/v1/disposition-rules/seed-defaults",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "disposition-rules",
                "seed-defaults"
              ]
            },
            "description": "Siembra las cadencias canonicas. Explicito (sembrar cambia lo que el tenant marca) e idempotente. No lleva cuerpo."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "const busy = j.disposition_rules.find(r => r.code === \"busy\" && r.effect === \"reschedule\");",
                  "pm.test(\"busy: reschedule canonico con sus cuatro parametros guardados\", function () {",
                  "  pm.expect(busy.active).to.eql(true);",
                  "  pm.expect(busy.params).to.eql({delay_seconds: 1200, factor: 2.0, max_delay_seconds: 86400, max_attempts: 6});",
                  "});",
                  "pm.test(\"invalid_number: suppress canonico\", function () {",
                  "  const s = j.disposition_rules.find(r => r.code === \"invalid_number\" && r.effect === \"suppress\");",
                  "  pm.expect(s.active).to.eql(true);",
                  "  pm.expect(s.params.reason).to.eql(\"invalid_number\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/disposition-rules",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/disposition-rules",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "disposition-rules"
              ]
            },
            "description": "Todas las reglas del tenant, tambien las apagadas (nunca se borran)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: DispositionRule {id, code, effect, params, priority, active, updated_at}\", function () {",
                  "  pm.expect(j.disposition_rules).to.be.an(\"array\").that.is.not.empty;",
                  "  j.disposition_rules.forEach(function (r) {",
                  "    pm.expect(r.id).to.be.a(\"string\");",
                  "    pm.expect(r.code).to.be.a(\"string\").and.not.empty;",
                  "    pm.expect(r.effect).to.be.a(\"string\").and.not.empty;",
                  "    pm.expect(r.updated_at).to.be.a(\"string\");",
                  "    pm.expect(r.params).to.be.an(\"object\");",
                  "    pm.expect(r.priority).to.be.a(\"number\");",
                  "    pm.expect(r.active).to.be.a(\"boolean\");",
                  "  });",
                  "});",
                  "pm.test(\"la respuesta no llama tope legal a max_attempts\", function () {",
                  "  pm.expect(pm.response.text()).to.not.match(/legal|regulat|tope/i);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/disposition-rules",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/disposition-rules",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "disposition-rules"
              ]
            },
            "description": "Reemplaza la politica reschedule/suppress: lo listado se escribe, lo activo no listado se apaga. factor y max_delay_seconds omitidos se guardan con el valor que aplica el motor.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"rules\": [\n    {\n      \"code\": \"busy\",\n      \"effect\": \"reschedule\",\n      \"priority\": 10,\n      \"params\": {\n        \"delay_seconds\": 1800,\n        \"factor\": 1.5,\n        \"max_attempts\": 4\n      }\n    },\n    {\n      \"code\": \"invalid_number\",\n      \"effect\": \"suppress\",\n      \"params\": {\n        \"reason\": \"invalid_number\"\n      }\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "const find = (c, e) => j.disposition_rules.find(r => r.code === c && r.effect === e);",
                  "pm.test(\"busy queda como se escribio, con max_delay_seconds materializado\", function () {",
                  "  pm.expect(find(\"busy\", \"reschedule\").params).to.eql({delay_seconds: 1800, factor: 1.5, max_delay_seconds: 86400, max_attempts: 4});",
                  "});",
                  "pm.test(\"no_answer no venia en el cuerpo: queda apagada, no borrada\", function () {",
                  "  pm.expect(find(\"no_answer\", \"reschedule\").active).to.eql(false);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/disposition-rules — validacion estricta (422)",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/disposition-rules",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "disposition-rules"
              ]
            },
            "description": "Un solo 422 nombra cada campo malo, y no se escribe nada.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"rules\": [\n    {\n      \"code\": \"\",\n      \"effect\": \"suppress\"\n    },\n    {\n      \"code\": \"busy\",\n      \"effect\": \"reschedule\",\n      \"params\": {\n        \"delay_seconds\": -1,\n        \"max_attempts\": 0\n      }\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 422\", function () { pm.response.to.have.status(422); });",
                  "const j = pm.response.json();",
                  "pm.test(\"el error nombra cada elemento y campo\", function () {",
                  "  pm.expect(j.error.code).to.eql(\"unprocessable\");",
                  "  pm.expect(j.error.message).to.include(\"rules[0].code must not be empty\");",
                  "  pm.expect(j.error.message).to.include(\"rules[1].params.delay_seconds\");",
                  "  pm.expect(j.error.message).to.include(\"rules[1].params.max_attempts must be an integer between 1 and 100\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/disposition-rules — vuelve a la politica inerte",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/disposition-rules",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "disposition-rules"
              ]
            },
            "description": "Cierre de la carpeta: una lista vacia apaga todas las reglas reschedule/suppress. Deja el tenant del stack como estaba antes de sembrar, para que la siguiente corrida no herede una politica.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"rules\": []\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"ninguna regla reschedule/suppress queda activa\", function () {",
                  "  const live = j.disposition_rules.filter(r => [\"reschedule\", \"suppress\"].includes(r.effect) && r.active);",
                  "  pm.expect(live).to.eql([]);",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "34 · campaign disposition-rules",
      "description": "Politica de reintentos de UNA campana (core#1159, ADR #157 de core): la regla de campana gana a la del tenant para su codigo. Usa la campana del golden-flow. El orden importa y la carpeta es idempotente entre corridas: escribe una regla de campana, la lee (propias y efectivas), prueba el 404 de una campana ajena y CIERRA con `PUT {rules: []}`, que apaga las reglas propias de la campana y la devuelve a la politica del tenant. Una regla `busy` activa en la campana del golden-flow aparcaria sus deudas en la corrida siguiente. La precedencia, la unicidad por alcance y el aislamiento viven en la suite :db de core.",
      "item": [
        {
          "name": "PUT /v1/campaigns/:id/disposition-rules",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{golden_campaign_id}}/disposition-rules",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{golden_campaign_id}}",
                "disposition-rules"
              ]
            },
            "description": "Reemplaza las reglas reschedule/suppress PROPIAS de la campana; las del tenant y las de otras campanas no se tocan.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"rules\": [\n    {\n      \"code\": \"busy\",\n      \"effect\": \"reschedule\",\n      \"priority\": 10,\n      \"params\": {\n        \"delay_seconds\": 3600,\n        \"max_attempts\": 3\n      }\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"la respuesta es de la campana pedida\", function () {",
                  "  pm.expect(j.campaign_id).to.eql(pm.collectionVariables.get(\"golden_campaign_id\"));",
                  "});",
                  "pm.test(\"busy queda como regla de la campana, con los defaults del motor guardados\", function () {",
                  "  const busy = j.disposition_rules.find(r => r.code === \"busy\" && r.effect === \"reschedule\");",
                  "  pm.expect(busy.campaign_id).to.eql(j.campaign_id);",
                  "  pm.expect(busy.active).to.eql(true);",
                  "  pm.expect(busy.params).to.eql({delay_seconds: 3600, factor: 2.0, max_delay_seconds: 86400, max_attempts: 3});",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/campaigns/:id/disposition-rules",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{golden_campaign_id}}/disposition-rules",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{golden_campaign_id}}",
                "disposition-rules"
              ]
            },
            "description": "Las reglas propias de la campana y las que aplican a un intento suyo (effective_rules)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: {campaign_id, disposition_rules[], effective_rules[]}\", function () {",
                  "  pm.expect(j.campaign_id).to.be.a(\"string\");",
                  "  pm.expect(j.disposition_rules).to.be.an(\"array\");",
                  "  pm.expect(j.effective_rules).to.be.an(\"array\");",
                  "});",
                  "pm.test(\"effective_rules de busy: al menos una y todas de la campana\", function () {",
                  "  const busy = j.effective_rules.filter(r => r.code === \"busy\");",
                  "  pm.expect(busy.length).to.be.above(0);",
                  "  busy.forEach(function (r) { pm.expect(r.campaign_id).to.eql(j.campaign_id); });",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/campaigns/:id/disposition-rules — campana ajena (404)",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{random_campaign_id}}/disposition-rules",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{random_campaign_id}}",
                "disposition-rules"
              ]
            },
            "description": "Una campana que no es del tenant es un 404 del endpoint (no `no such route`) y no se escribe nada.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"rules\": [\n    {\n      \"code\": \"busy\",\n      \"effect\": \"reschedule\",\n      \"priority\": 10,\n      \"params\": {\n        \"delay_seconds\": 3600,\n        \"max_attempts\": 3\n      }\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set(\"random_campaign_id\", pm.variables.replaceIn(\"{{$guid}}\"));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "pm.test(\"404 del endpoint, no una ruta inexistente\", function () {",
                  "  const j = pm.response.json();",
                  "  pm.expect(j.error.code).to.eql(\"not_found\");",
                  "  pm.expect(pm.response.text()).to.not.include(\"no such route\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/campaigns/:id/disposition-rules — vuelve a la politica del tenant",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{golden_campaign_id}}/disposition-rules",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{golden_campaign_id}}",
                "disposition-rules"
              ]
            },
            "description": "Cierre de la carpeta: una lista vacia apaga las reglas propias de la campana, que vuelve a marcar bajo la politica del tenant.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"rules\": []\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"ninguna regla propia de la campana queda activa\", function () {",
                  "  pm.expect(j.disposition_rules.filter(r => r.active)).to.eql([]);",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "35 · debt-lists",
      "description": "Listas reutilizables (core#1220, ADR #162 de core): crear, leer, importar a una lista, asociar/desasociar con una campana y quitar un miembro. Asocia con la campana de la carpeta 04, ya archivada, y la desasocia al final: ninguna deuda de la lista entra en un feed vivo del stack. La deuda importada no lleva contactos. Cada corrida deja una lista con nombre unico (no hay verbo de borrado de lista). La carrera Reg F con dos campanas, el callback 422 y el aislamiento RLS viven en la suite :db de core.",
      "item": [
        {
          "name": "POST /v1/debt-lists",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/debt-lists",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debt-lists"
              ]
            },
            "description": "Crea una lista reutilizable con un nombre unico por corrida. Nace con size 0 y sin campanas: el tamano se cuenta al leer, no hay contador.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"{{debt_list_name}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set(\"debt_list_name\", \"postman-list-\" + Date.now());",
                  "pm.collectionVariables.set(\"debt_list_ref\", \"postman-list-debt-\" + Date.now());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const l = pm.response.json().debt_list;",
                  "pm.test(\"shape DebtList: id, name, size 0, campaign_ids []\", function () {",
                  "  pm.expect(l.id).to.be.a(\"string\");",
                  "  pm.expect(l.name).to.eql(pm.collectionVariables.get(\"debt_list_name\"));",
                  "  pm.expect(l.size).to.eql(0);",
                  "  pm.expect(l.campaign_ids).to.eql([]);",
                  "});",
                  "pm.collectionVariables.set(\"debt_list_id\", l.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/debt-lists — nombre repetido (409)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/debt-lists",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debt-lists"
              ]
            },
            "description": "El nombre es unico en el tenant: repetirlo es un 409 conflict y no crea otra lista.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"{{debt_list_name}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 409\", function () { pm.response.to.have.status(409); });",
                  "pm.test(\"conflict\", function () { pm.expect(pm.response.json().error.code).to.eql(\"conflict\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/debt-lists",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/debt-lists",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debt-lists"
              ]
            },
            "description": "Las listas del tenant por nombre, con size y campaign_ids contados al leer."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: debt_lists[] y la lista de esta corrida esta\", function () {",
                  "  pm.expect(j.debt_lists).to.be.an(\"array\");",
                  "  const mine = j.debt_lists.find(l => l.id === pm.collectionVariables.get(\"debt_list_id\"));",
                  "  pm.expect(mine).to.be.an(\"object\");",
                  "  pm.expect(mine.size).to.eql(0);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/debt-lists/:id/import",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/debt-lists/{{debt_list_id}}/import",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debt-lists",
                "{{debt_list_id}}",
                "import"
              ]
            },
            "description": "El pipeline de /v1/debts/import sobre una lista. El item 0 entra en la lista; el item 1 trae campaign_id y falla ESE item (ADR #162 §7 de core): la campana de una deuda de lista la da la asociacion. La deuda no lleva contactos: no es marcable y no entra en ningun feed del stack.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"debts\": [\n    {\n      \"external_ref\": \"{{debt_list_ref}}\",\n      \"consumer_ref\": \"{{debt_list_ref}}-c\",\n      \"amount_cents\": 1000,\n      \"currency\": \"USD\"\n    },\n    {\n      \"external_ref\": \"{{debt_list_ref}}-with-campaign\",\n      \"consumer_ref\": \"{{debt_list_ref}}-c2\",\n      \"campaign_id\": \"{{campaign_id}}\"\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"imported 1, el item con campaign_id falla por item\", function () {",
                  "  pm.expect(j.imported).to.eql(1);",
                  "  pm.expect(j.failed.length).to.eql(1);",
                  "  pm.expect(j.failed[0].index).to.eql(1);",
                  "  pm.expect(j.failed[0].error).to.include(\"campaign_id is not accepted on a list import\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/debts?external_ref= — la deuda importada a la lista",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/debts?external_ref={{debt_list_ref}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debts"
              ],
              "query": [
                {
                  "key": "external_ref",
                  "value": "{{debt_list_ref}}"
                }
              ]
            },
            "description": "Resuelve el id de la deuda que la peticion anterior metio en la lista, para el DELETE del miembro. Su campaign_id (la ranura directa) sigue vacio: el import a lista no la toca."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"una deuda, sin ranura directa\", function () {",
                  "  pm.expect(j.debts.length).to.eql(1);",
                  "  pm.expect(j.debts[0].campaign_id).to.eql(null);",
                  "});",
                  "pm.collectionVariables.set(\"debt_list_debt_id\", j.debts[0].id);"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/debt-lists/:id/campaigns/:campaign_id",
          "request": {
            "method": "PUT",
            "url": {
              "raw": "{{baseUrl}}/v1/debt-lists/{{debt_list_id}}/campaigns/{{campaign_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debt-lists",
                "{{debt_list_id}}",
                "campaigns",
                "{{campaign_id}}"
              ]
            },
            "description": "Asocia la lista con la campana de la carpeta 04 (ya archivada: su runner no corre, asi que la asociacion no marca nada en el stack). Idempotente, sin cuerpo."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const l = pm.response.json().debt_list;",
                  "pm.test(\"la lista tiene la campana y un miembro\", function () {",
                  "  pm.expect(l.campaign_ids).to.eql([pm.collectionVariables.get(\"campaign_id\")]);",
                  "  pm.expect(l.size).to.eql(1);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/debt-lists/:id",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/debt-lists/{{debt_list_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debt-lists",
                "{{debt_list_id}}"
              ]
            },
            "description": "Una lista: size y campaign_ids contados al leer."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const l = pm.response.json().debt_list;",
                  "pm.test(\"size 1 y la campana asociada\", function () {",
                  "  pm.expect(l.id).to.eql(pm.collectionVariables.get(\"debt_list_id\"));",
                  "  pm.expect(l.size).to.eql(1);",
                  "  pm.expect(l.campaign_ids).to.eql([pm.collectionVariables.get(\"campaign_id\")]);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/debt-lists/:id/campaigns/:campaign_id — campana ajena (404)",
          "request": {
            "method": "PUT",
            "url": {
              "raw": "{{baseUrl}}/v1/debt-lists/{{debt_list_id}}/campaigns/{{random_campaign_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debt-lists",
                "{{debt_list_id}}",
                "campaigns",
                "{{random_campaign_id}}"
              ]
            },
            "description": "Una campana que no es del tenant es un 404 del endpoint (no `no such route`) y no se asocia nada."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set(\"random_campaign_id\", pm.variables.replaceIn(\"{{$guid}}\"));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "pm.test(\"404 del endpoint, no una ruta inexistente\", function () {",
                  "  const j = pm.response.json();",
                  "  pm.expect(j.error.code).to.eql(\"not_found\");",
                  "  pm.expect(pm.response.text()).to.not.include(\"no such route\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/debt-lists/:id/campaigns/:campaign_id",
          "request": {
            "method": "DELETE",
            "url": {
              "raw": "{{baseUrl}}/v1/debt-lists/{{debt_list_id}}/campaigns/{{campaign_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debt-lists",
                "{{debt_list_id}}",
                "campaigns",
                "{{campaign_id}}"
              ]
            },
            "description": "Desasocia: los miembros salen de la poblacion de la campana. La lista y la deuda quedan."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 204\", function () { pm.response.to.have.status(204); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/debt-lists/:id/members",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/debt-lists/{{debt_list_id}}/members?limit=500",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debt-lists",
                "{{debt_list_id}}",
                "members"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "500"
                }
              ]
            },
            "description": "Los miembros de la lista, paginados por cursor opaco (limit <= 500). Sin telefono ni nombre del deudor."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"members con debt_id, external_ref y added_at, sin PII\", function () {",
                  "  const j = pm.response.json();",
                  "  pm.expect(j.members).to.be.an(\"array\");",
                  "  pm.expect(j).to.have.property(\"next_cursor\");",
                  "  pm.expect(j.members).to.have.lengthOf(1);",
                  "  pm.expect(j.members[0].debt_id).to.eql(pm.collectionVariables.get(\"debt_list_debt_id\"));",
                  "  pm.expect(j.next_cursor).to.eql(null);",
                  "  j.members.forEach(function (m) { pm.expect(Object.keys(m).sort()).to.eql([\"added_at\", \"debt_id\", \"external_ref\"]); });",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/debt-lists/:id/members — lote idempotente",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\"debt_ids\": [\"00000000-0000-4000-8000-000000000000\"]}"
            },
            "url": {
              "raw": "{{baseUrl}}/v1/debt-lists/{{debt_list_id}}/members",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debt-lists",
                "{{debt_list_id}}",
                "members"
              ]
            },
            "description": "Saca hasta 500 deudas de la lista de una vez. Idempotente: un id que no es miembro no saca nada y no es error (aqui, un UUID que no existe: removed 0)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"removed cuenta las que salieron\", function () { pm.expect(pm.response.json().removed).to.eql(0); });"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/debt-lists/:id/members/:debt_id",
          "request": {
            "method": "DELETE",
            "url": {
              "raw": "{{baseUrl}}/v1/debt-lists/{{debt_list_id}}/members/{{debt_list_debt_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debt-lists",
                "{{debt_list_id}}",
                "members",
                "{{debt_list_debt_id}}"
              ]
            },
            "description": "Quita la deuda de la lista: el verbo propio que ningun import emite. La deuda queda."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 204\", function () { pm.response.to.have.status(204); });"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/debt-lists/:id/members/:debt_id — ya no es miembro (404)",
          "request": {
            "method": "DELETE",
            "url": {
              "raw": "{{baseUrl}}/v1/debt-lists/{{debt_list_id}}/members/{{debt_list_debt_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debt-lists",
                "{{debt_list_id}}",
                "members",
                "{{debt_list_debt_id}}"
              ]
            },
            "description": "Repetir el DELETE es un 404 del endpoint: ya no es miembro."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "pm.test(\"404 del endpoint, no una ruta inexistente\", function () {",
                  "  const j = pm.response.json();",
                  "  pm.expect(j.error.code).to.eql(\"not_found\");",
                  "  pm.expect(pm.response.text()).to.not.include(\"no such route\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/debt-lists/:id/archive",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/debt-lists/{{debt_list_id}}/archive",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debt-lists",
                "{{debt_list_id}}",
                "archive"
              ]
            },
            "description": "Archivo terminal (core#2160): nunca un borrado. 409 mientras una campana no archivada la use."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: archived\", function () { pm.expect(j.debt_list.archived).to.eql(true); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "36 · transfer-destinations",
      "description": "Registro por tenant de los destinos de transferencia EXTERNA (core#1261): una transferencia ciega a la PSTN sólo sale hacia un número registrado aquí; cualquier otro es 422 compliance_blocked con rule_id transfer.destination_unregistered.v1. Alta y retirada: llave dd_ o account_admin. DELETE retira (hecho append-only), nunca borra. GET /v1/transfer/targets (core#1266) lista los destinos sin el número: el selector manda su id. Cada destino declara su line_type (core#1408): mobile | landline | voip | unknown, la lista y el default del import de contactos. El gate de consentimiento lo lee para la pierna: landline levanta sólo el consentimiento previo, el resto de gates corre igual. PATCH lo cambia y deja un hecho line_type_declared con su actor.",
      "item": [
        {
          "name": "POST /v1/transfer-destinations",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/transfer-destinations",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "transfer-destinations"
              ]
            },
            "description": "Alta: E.164 estricto, etiqueta obligatoria y el line_type que declara el tenant (core#1408; ausente es unknown). Número aleatorio por corrida para evitar el 409 por duplicado vigente.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"number\": \"{{postman_transfer_destination_e164}}\",\n  \"label\": \"postman smoke\",\n  \"line_type\": \"landline\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const nxx = 200 + Math.floor(Math.random() * 800);",
                  "const line = String(Math.floor(Math.random() * 10000)).padStart(4, \"0\");",
                  "pm.collectionVariables.set(\"postman_transfer_destination_e164\", \"+1312\" + nxx + line);"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: destino registrado\", function () {",
                  "  pm.expect(j.transfer_destination.id).to.be.a(\"string\");",
                  "  pm.expect(j.transfer_destination.number).to.eql(pm.collectionVariables.get(\"postman_transfer_destination_e164\"));",
                  "  pm.expect(j.transfer_destination.label).to.eql(\"postman smoke\");",
                  "  pm.expect(j.transfer_destination.registered_at).to.be.a(\"string\");",
                  "  pm.expect(j.transfer_destination.line_type).to.eql(\"landline\");",
                  "  pm.expect(j.transfer_destination.line_type_declared_at).to.eql(j.transfer_destination.registered_at);",
                  "  pm.expect(j.transfer_destination.line_type_declared_by).to.be.a(\"string\");",
                  "});",
                  "pm.collectionVariables.set(\"transfer_destination_id\", j.transfer_destination.id);",
                  "pm.collectionVariables.set(\"transfer_destination_registered_at\", j.transfer_destination.registered_at);"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/transfer-destinations",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/transfer-destinations",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "transfer-destinations"
              ]
            },
            "description": "Los destinos vigentes del tenant, del registro más antiguo al más nuevo."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: incluye el recién registrado\", function () {",
                  "  const ids = j.transfer_destinations.map(function (d) { return d.id; });",
                  "  pm.expect(ids).to.include(pm.collectionVariables.get(\"transfer_destination_id\"));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/transfer/targets",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/transfer/targets",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "transfer",
                "targets"
              ]
            },
            "description": "De dónde elige destino un selector de transferencia, también el del widget (core#1266): agentes activos con extensión, extensiones sin agente (hoy siempre vacía) y los destinos externos registrados con id y etiqueta, SIN el E.164. La transferencia ciega acepta ese id como destination."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: las tres clases\", function () {",
                  "  pm.expect(j.agents).to.be.an(\"array\");",
                  "  pm.expect(j.extensions).to.be.an(\"array\");",
                  "  pm.expect(j.external_destinations).to.be.an(\"array\");",
                  "});",
                  "pm.test(\"el destino recién registrado sale por id y etiqueta\", function () {",
                  "  const d = j.external_destinations.find(function (x) { return x.id === pm.collectionVariables.get(\"transfer_destination_id\"); });",
                  "  pm.expect(d).to.eql({ id: pm.collectionVariables.get(\"transfer_destination_id\"), label: \"postman smoke\" });",
                  "});",
                  "pm.test(\"el E.164 no viaja\", function () {",
                  "  const e164 = pm.collectionVariables.get(\"postman_transfer_destination_e164\");",
                  "  pm.expect(e164).to.be.a(\"string\");",
                  "  pm.expect(pm.response.text()).to.not.include(e164.slice(1));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/transfer-destinations/:id",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/transfer-destinations/{{transfer_destination_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "transfer-destinations",
                "{{transfer_destination_id}}"
              ]
            },
            "description": "Declara otro line_type para el registro vigente (core#1408): un hecho line_type_declared con su actor; el registro no se mueve (mismo id y misma ancla RND). Sólo acepta line_type.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"line_type\": \"mobile\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: el line_type nuevo, el mismo registro\", function () {",
                  "  pm.expect(j.transfer_destination.id).to.eql(pm.collectionVariables.get(\"transfer_destination_id\"));",
                  "  pm.expect(j.transfer_destination.line_type).to.eql(\"mobile\");",
                  "  pm.expect(j.transfer_destination.registered_at).to.eql(pm.collectionVariables.get(\"transfer_destination_registered_at\"));",
                  "  pm.expect(j.transfer_destination.line_type_declared_at).to.be.a(\"string\");",
                  "  pm.expect(j.transfer_destination.line_type_declared_by).to.be.a(\"string\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/transfer-destinations/:id",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/transfer-destinations/{{transfer_destination_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "transfer-destinations",
                "{{transfer_destination_id}}"
              ]
            },
            "description": "Retira el registro vigente (un hecho `retired`, append-only) y devuelve el destino retirado."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: el destino retirado\", function () {",
                  "  pm.expect(j.transfer_destination.id).to.eql(pm.collectionVariables.get(\"transfer_destination_id\"));",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "37 · inbound-ivr-menus",
      "description": "Los menús del IVR de entrada (core#1318, ADR #174 de core; F1-1 de MT-IVR-01): tabla de hechos append-only, vocabulario de entrada (queue:<id>, agent:<id>, hangup, optout y submenús, con acción por defecto obligatoria), render TTS al publicar (fail-closed) y la versión siguiente por menu_id. Publicar y retirar: llave dd_ o account_admin. Una ruta ivr ejecuta su menú desde el runtime (core#1319, F1-3): sólo se escribe con un menú vigente del tenant en dest_ref (400 si no) y un menú que usa una ruta activa no se retira (409).",
      "item": [
        {
          "name": "POST /v1/inbound-ivr-menus",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-ivr-menus",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-ivr-menus"
              ]
            },
            "description": "Publica un menú nuevo (versión 1). Valida el árbol, comprueba que el agente es del tenant y renderiza cada prompt contra el TTS del stack en el momento: si uno no se renderiza, 503 prompt_render_failed y no se publica nada. Reusa el asiento de la carpeta 09 ({{postman_agent_id}}); las colas de entrada tienen su carpeta, la 38 (core#1347).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"postman smoke\",\n  \"menu\": {\n    \"prompt\": \"Thanks for calling. To reach an agent press 1. For more options press 2. To stop receiving our calls press 9.\",\n    \"default\": \"agent:{{postman_agent_id}}\",\n    \"entries\": {\n      \"1\": { \"action\": \"agent:{{postman_agent_id}}\" },\n      \"2\": {\n        \"submenu\": {\n          \"prompt\": \"To end this call press 1.\",\n          \"default\": \"hangup\",\n          \"entries\": { \"1\": { \"action\": \"hangup\" } }\n        }\n      },\n      \"9\": { \"action\": \"optout\" }\n    }\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: versión 1 publicada\", function () {",
                  "  const m = j.inbound_ivr_menu;",
                  "  pm.expect(m.id).to.be.a(\"string\");",
                  "  pm.expect(m.status).to.eql(\"published\");",
                  "  pm.expect(m.version).to.eql(1);",
                  "  pm.expect(m.menu.default).to.eql(\"agent:\" + pm.collectionVariables.get(\"postman_agent_id\"));",
                  "  pm.expect(m.menu.entries[\"9\"]).to.eql({ action: \"optout\" });",
                  "  pm.expect(m.retired_at).to.eql(null);",
                  "});",
                  "pm.test(\"la clave del audio no viaja\", function () { pm.expect(pm.response.text()).to.not.include(\"prompt_media\"); });",
                  "pm.collectionVariables.set(\"inbound_ivr_menu_id\", j.inbound_ivr_menu.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/inbound-ivr-menus (sin default -> 400)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-ivr-menus",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-ivr-menus"
              ]
            },
            "description": "Todo nodo lleva una acción por defecto obligatoria (sin tecla se repite el prompt y a la segunda se ejecuta): un menú sin ella es 400 y no se publica.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"postman sin default\",\n  \"menu\": {\n    \"prompt\": \"Press 1.\",\n    \"entries\": { \"1\": { \"action\": \"hangup\" } }\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "pm.test(\"nombra el default\", function () { pm.expect(pm.response.json().error.message).to.include(\"default is required\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/inbound-ivr-menus (menu_id -> versión 2)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-ivr-menus",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-ivr-menus"
              ]
            },
            "description": "Con menu_id se publica la versión siguiente del mismo menú: nada se edita en su sitio.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"postman smoke v2\",\n  \"menu_id\": \"{{inbound_ivr_menu_id}}\",\n  \"menu\": {\n    \"prompt\": \"Thanks for calling. To reach an agent press 1. To stop receiving our calls press 9.\",\n    \"default\": \"hangup\",\n    \"entries\": {\n      \"1\": { \"action\": \"agent:{{postman_agent_id}}\" },\n      \"9\": { \"action\": \"optout\" }\n    }\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const m = pm.response.json().inbound_ivr_menu;",
                  "pm.test(\"shape: el mismo menú, versión 2\", function () {",
                  "  pm.expect(m.id).to.eql(pm.collectionVariables.get(\"inbound_ivr_menu_id\"));",
                  "  pm.expect(m.version).to.eql(2);",
                  "  pm.expect(m.name).to.eql(\"postman smoke v2\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/inbound-ivr-menus",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-ivr-menus",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-ivr-menus"
              ]
            },
            "description": "Los menús vigentes del tenant, en su versión más alta."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: incluye el menú en su versión 2\", function () {",
                  "  const m = pm.response.json().inbound_ivr_menus.find(function (x) { return x.id === pm.collectionVariables.get(\"inbound_ivr_menu_id\"); });",
                  "  pm.expect(m).to.be.an(\"object\");",
                  "  pm.expect(m.version).to.eql(2);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/inbound-ivr-menus/:id",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-ivr-menus/{{inbound_ivr_menu_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-ivr-menus",
                "{{inbound_ivr_menu_id}}"
              ]
            },
            "description": "La versión vigente y su historia, del primer hecho al último."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const m = pm.response.json().inbound_ivr_menu;",
                  "pm.test(\"shape: vigente la 2, historia de dos publicaciones\", function () {",
                  "  pm.expect(m.version).to.eql(2);",
                  "  pm.expect(m.history.map(function (f) { return f.event + \":\" + f.version; })).to.eql([\"published:1\", \"published:2\"]);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/inbound-routes (ivr con el menú vigente -> 201)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-routes",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-routes"
              ]
            },
            "description": "core#1319: una ruta ivr nombra en dest_ref el id del menú (el de su versión 1), vigente y del mismo tenant. Número aleatorio por corrida.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"e164\": \"{{postman_ivr_e164}}\",\n  \"dest_type\": \"ivr\",\n  \"dest_ref\": \"{{inbound_ivr_menu_id}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const nxx = 200 + Math.floor(Math.random() * 800);",
                  "const line = String(Math.floor(Math.random() * 10000)).padStart(4, \"0\");",
                  "const e164 = \"+1774\" + nxx + line;",
                  "pm.collectionVariables.set(\"postman_ivr_e164\", e164);",
                  "// core#2414/#2429: una ruta activa exige un DID activo del tenant; se da de alta antes por la API.",
                  "pm.sendRequest({",
                  "  url: pm.variables.get(\"baseUrl\") + \"/v1/dids\",",
                  "  method: \"POST\",",
                  "  header: { \"Content-Type\": \"application/json\", \"Authorization\": \"Bearer \" + pm.variables.get(\"api_token\") },",
                  "  body: { mode: \"raw\", raw: JSON.stringify({ e164: e164, us_state: \"MA\" }) }",
                  "}, function (err, res) {",
                  "  pm.test(\"alta previa del DID de la ruta -> 201\", function () { pm.expect(err, \"sendRequest\").to.equal(null); pm.expect(res.code, \"POST /v1/dids\").to.equal(201); });",
                  "});"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const r = pm.response.json().inbound_route;",
                  "pm.test(\"shape: ruta ivr hacia el menú\", function () {",
                  "  pm.expect(r.dest_type).to.eql(\"ivr\");",
                  "  pm.expect(r.dest_ref).to.eql(pm.collectionVariables.get(\"inbound_ivr_menu_id\"));",
                  "  pm.expect(r.status).to.eql(\"active\");",
                  "});",
                  "pm.collectionVariables.set(\"inbound_ivr_route_id\", r.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/inbound-ivr-menus/:id/retire (una ruta activa lo usa -> 409)",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-ivr-menus/{{inbound_ivr_menu_id}}/retire",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-ivr-menus",
                "{{inbound_ivr_menu_id}}",
                "retire"
              ]
            },
            "description": "core#1319: retirar un menú que usa una ruta ACTIVA dejaría esa ruta rechazando cada llamada; es 409 y nombra la ruta."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 409\", function () { pm.response.to.have.status(409); });",
                  "pm.test(\"nombra la ruta\", function () { pm.expect(pm.response.json().error.message).to.include(pm.collectionVariables.get(\"inbound_ivr_route_id\")); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/inbound-routes/:id/disable (libera el menú)",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-routes/{{inbound_ivr_route_id}}/disable",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-routes",
                "{{inbound_ivr_route_id}}",
                "disable"
              ]
            },
            "description": "La ruta ivr se desactiva (las rutas nunca se borran); desde aquí el menú ya se puede retirar."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"desactivada\", function () { pm.expect(pm.response.json().inbound_route.status).to.eql(\"disabled\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/inbound-ivr-menus/:id/retire",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-ivr-menus/{{inbound_ivr_menu_id}}/retire",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-ivr-menus",
                "{{inbound_ivr_menu_id}}",
                "retire"
              ]
            },
            "description": "Retira el menú con un hecho nuevo que nombra su versión vigente. Un menú retirado no se vuelve a publicar (409)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const m = pm.response.json().inbound_ivr_menu;",
                  "pm.test(\"shape: retirado en su versión 2\", function () {",
                  "  pm.expect(m.id).to.eql(pm.collectionVariables.get(\"inbound_ivr_menu_id\"));",
                  "  pm.expect(m.status).to.eql(\"retired\");",
                  "  pm.expect(m.version).to.eql(2);",
                  "  pm.expect(m.retired_at).to.be.a(\"string\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/inbound-routes (ivr con un menú retirado -> 400)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-routes",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-routes"
              ]
            },
            "description": "core#1319: un menú retirado no vale como destino; 400 invalid sobre dest_ref y la ruta no se escribe.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"e164\": \"{{postman_ivr_e164}}\",\n  \"dest_type\": \"ivr\",\n  \"dest_ref\": \"{{inbound_ivr_menu_id}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const nxx = 200 + Math.floor(Math.random() * 800);",
                  "const line = String(Math.floor(Math.random() * 10000)).padStart(4, \"0\");",
                  "const e164 = \"+1774\" + nxx + line;",
                  "pm.collectionVariables.set(\"postman_ivr_e164\", e164);",
                  "// core#2414/#2429: una ruta activa exige un DID activo del tenant; se da de alta antes por la API.",
                  "pm.sendRequest({",
                  "  url: pm.variables.get(\"baseUrl\") + \"/v1/dids\",",
                  "  method: \"POST\",",
                  "  header: { \"Content-Type\": \"application/json\", \"Authorization\": \"Bearer \" + pm.variables.get(\"api_token\") },",
                  "  body: { mode: \"raw\", raw: JSON.stringify({ e164: e164, us_state: \"MA\" }) }",
                  "}, function (err, res) {",
                  "  pm.test(\"alta previa del DID de la ruta -> 201\", function () { pm.expect(err, \"sendRequest\").to.equal(null); pm.expect(res.code, \"POST /v1/dids\").to.equal(201); });",
                  "});"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "pm.test(\"nombra dest_ref\", function () { pm.expect(pm.response.json().error.message).to.include(\"dest_ref\"); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "38 · inbound-queues",
      "description": "Las colas entrantes en autoservicio (core#1347, addendum 2026-09-30 al ADR #104 de core): crear con nombre (único entre las vigentes del tenant sin distinguir mayúsculas), listar con el desborde y las rutas activas que la usan, configurar y limpiar el umbral de oferta de callback, desbordar a otra cola vigente del tenant sin ciclos, y retirar de forma terminal con 409 mientras una ruta activa, un menú vigente o el desborde de otra cola la nombre. La lista dice si la capacidad está encendida (inbound_queue_stream_enabled por tenant; esta API no la enciende). Escribir: llave dd_ o account_admin. Una ruta queue nombra su cola en queue_id.",
      "item": [
        {
          "name": "POST /v1/inbound-queues",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-queues",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-queues"
              ]
            },
            "description": "Crea una cola vigente con un nombre único entre las vigentes del tenant (sin distinguir mayúsculas), sin desborde. Nombre aleatorio por corrida.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"{{inbound_queue_name}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set(\"inbound_queue_name\", \"postman \" + Date.now() + \"-\" + Math.floor(Math.random() * 100000));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const q = pm.response.json().inbound_queue;",
                  "pm.test(\"shape: vigente, sin desborde, sin rutas\", function () {",
                  "  pm.expect(q.id).to.be.a(\"string\");",
                  "  pm.expect(q.name).to.eql(pm.collectionVariables.get(\"inbound_queue_name\"));",
                  "  pm.expect(q.status).to.eql(\"active\");",
                  "  pm.expect(q.overflow_max_wait_ms).to.eql(null);",
                  "  pm.expect(q.overflow_queue_id).to.eql(null);",
                  "  pm.expect(q.active_route_count).to.eql(0);",
                  "  pm.expect(q.retired_at).to.eql(null);",
                  "});",
                  "pm.collectionVariables.set(\"inbound_queue_id\", q.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/inbound-queues (mismo nombre en mayúsculas -> 409)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-queues",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-queues"
              ]
            },
            "description": "El nombre es único entre las colas VIGENTES del tenant sin distinguir mayúsculas: 409 conflict.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"{{inbound_queue_name_upper}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set(\"inbound_queue_name_upper\", pm.collectionVariables.get(\"inbound_queue_name\").toUpperCase());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 409\", function () { pm.response.to.have.status(409); });",
                  "pm.test(\"nombra el nombre\", function () { pm.expect(pm.response.json().error.message).to.include(\"name\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/inbound-queues (sin nombre -> 400)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-queues",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-queues"
              ]
            },
            "description": "Sin name no hay cola: 400 invalid.",
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "pm.test(\"nombra name\", function () { pm.expect(pm.response.json().error.message).to.include(\"name\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/inbound-queues (destino del desborde)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-queues",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-queues"
              ]
            },
            "description": "Una segunda cola del mismo tenant, para el desborde.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"{{inbound_queue_dest_name}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set(\"inbound_queue_dest_name\", \"postman \" + Date.now() + \"-\" + Math.floor(Math.random() * 100000));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "pm.collectionVariables.set(\"inbound_queue_dest_id\", pm.response.json().inbound_queue.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/inbound-queues",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-queues",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-queues"
              ]
            },
            "description": "Las colas vigentes del tenant, y si la cola admite llamadas para él: enabled es verdadero sólo con los dos interruptores de switches encendidos, inbound_queue_stream_enabled y agent_session_journal_enabled, resueltos por tenant (core#1354; esta API no cambia ninguno)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: incluye la cola y la capacidad\", function () {",
                  "  const q = j.inbound_queues.find(function (x) { return x.id === pm.collectionVariables.get(\"inbound_queue_id\"); });",
                  "  pm.expect(q).to.be.an(\"object\");",
                  "  pm.expect(j.capability.flag).to.eql(\"inbound_queue_stream_enabled\");",
                  "  pm.expect(j.capability.enabled).to.be.a(\"boolean\");",
                  "  pm.expect(j.capability.switches).to.be.an(\"object\");",
                  "  pm.expect(j.capability.switches.inbound_queue_stream_enabled).to.be.a(\"boolean\");",
                  "  pm.expect(j.capability.switches.agent_session_journal_enabled).to.be.a(\"boolean\");",
                  "  pm.expect(j.capability.enabled).to.eql(j.capability.switches.inbound_queue_stream_enabled && j.capability.switches.agent_session_journal_enabled);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/inbound-queues/:id",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-queues/{{inbound_queue_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-queues",
                "{{inbound_queue_id}}"
              ]
            },
            "description": "Una cola del tenant, vigente o retirada, con la capacidad."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: la cola pedida\", function () {",
                  "  pm.expect(j.inbound_queue.id).to.eql(pm.collectionVariables.get(\"inbound_queue_id\"));",
                  "  pm.expect(j.capability).to.be.an(\"object\");",
                  "  pm.expect(j.capability.flag).to.eql(\"inbound_queue_stream_enabled\");",
                  "  pm.expect(j.capability.switches).to.be.an(\"object\");",
                  "  pm.expect(j.capability.switches.inbound_queue_stream_enabled).to.be.a(\"boolean\");",
                  "  pm.expect(j.capability.switches.agent_session_journal_enabled).to.be.a(\"boolean\");",
                  "  pm.expect(j.capability.enabled).to.eql(j.capability.switches.inbound_queue_stream_enabled && j.capability.switches.agent_session_journal_enabled);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/inbound-queues/live",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-queues/live",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-queues",
                "live"
              ]
            },
            "description": "La cola entrante en vivo de todas las colas vigentes del tenant (core#1526): depth (admisiones vivas = enqueued sin terminal, derivadas del stream) y oldest_wait_ms. Con inbound_queue_stream_enabled apagada responde stream_enabled false y depth null, nunca 0. Las admisiones huérfanas más viejas que wait_bound_ms no cuentan."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: honesto con la bandera — apagada es null, nunca 0\", function () {",
                  "  pm.expect(j.stream_enabled).to.be.a(\"boolean\");",
                  "  pm.expect(j.as_of).to.be.a(\"string\");",
                  "  const q = j.inbound_queues.find(function (x) { return x.queue_id === pm.collectionVariables.get(\"inbound_queue_id\"); });",
                  "  pm.expect(q).to.be.an(\"object\");",
                  "  pm.expect(q.wait_bound_ms).to.be.a(\"number\");",
                  "  if (j.stream_enabled) {",
                  "    pm.expect(q.depth).to.be.a(\"number\");",
                  "    pm.expect(q.depth).to.be.at.least(0);",
                  "  } else {",
                  "    pm.expect(q.depth).to.eql(null);",
                  "    pm.expect(q.oldest_wait_ms).to.eql(null);",
                  "  }",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/inbound-queues/:id/live",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-queues/{{inbound_queue_id}}/live",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-queues",
                "{{inbound_queue_id}}",
                "live"
              ]
            },
            "description": "La misma lectura en vivo para una cola vigente (core#1526); una retirada o ajena es 404."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: honesto con la bandera — apagada es null, nunca 0\", function () {",
                  "  pm.expect(j.stream_enabled).to.be.a(\"boolean\");",
                  "  pm.expect(j.as_of).to.be.a(\"string\");",
                  "  const q = j.inbound_queue;",
                  "  pm.expect(q).to.be.an(\"object\");",
                  "  pm.expect(q.wait_bound_ms).to.be.a(\"number\");",
                  "  if (j.stream_enabled) {",
                  "    pm.expect(q.depth).to.be.a(\"number\");",
                  "    pm.expect(q.depth).to.be.at.least(0);",
                  "  } else {",
                  "    pm.expect(q.depth).to.eql(null);",
                  "    pm.expect(q.oldest_wait_ms).to.eql(null);",
                  "  }",
                  "});",
                  "pm.test(\"es la cola pedida\", function () { pm.expect(j.inbound_queue.queue_id).to.eql(pm.collectionVariables.get(\"inbound_queue_id\")); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/inbound-queues/:id/callback-offer/stats",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-queues/{{inbound_queue_id}}/callback-offer/stats?from=2026-10-01T00:00:00Z&to=2026-10-02T00:00:00Z",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-queues",
                "{{inbound_queue_id}}",
                "callback-offer",
                "stats"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "2026-10-01T00:00:00Z"
                },
                {
                  "key": "to",
                  "value": "2026-10-02T00:00:00Z"
                }
              ]
            },
            "description": "Ofertas de callback de la cola en [from, to) y cómo cerró cada una (core#1884, ADR #215): offered, accepted, declined_or_abandoned y open, derivados del stream queue_events. Ventana obligatoria de 31 días como máximo; una cola ajena o inexistente es 404."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: cuatro enteros y la cohorte cuadra\", function () {",
                  "  pm.expect(j.inbound_queue_id).to.eql(pm.collectionVariables.get(\"inbound_queue_id\"));",
                  "  const c = j.callback_offer;",
                  "  [\"offered\", \"accepted\", \"declined_or_abandoned\", \"open\"].forEach(function (k) {",
                  "    pm.expect(c[k]).to.be.a(\"number\");",
                  "    pm.expect(c[k]).to.be.at.least(0);",
                  "  });",
                  "  pm.expect(c.offered).to.eql(c.accepted + c.declined_or_abandoned + c.open);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/inbound-queues/:id/callback-offer/stats (ventana de más de 31 días -> 422)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-queues/{{inbound_queue_id}}/callback-offer/stats?from=2026-01-01T00:00:00Z&to=2026-03-01T00:00:00Z",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-queues",
                "{{inbound_queue_id}}",
                "callback-offer",
                "stats"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "2026-01-01T00:00:00Z"
                },
                {
                  "key": "to",
                  "value": "2026-03-01T00:00:00Z"
                }
              ]
            },
            "description": "Una ventana de más de 31 días es 422 unprocessable (core#1884), no 400."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 422\", function () { pm.response.to.have.status(422); });",
                  "pm.test(\"shape: error unprocessable\", function () { pm.expect(pm.response.json().error.code).to.eql(\"unprocessable\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/inbound-queues/:id/overflow",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-queues/{{inbound_queue_id}}/overflow",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-queues",
                "{{inbound_queue_id}}",
                "overflow"
              ]
            },
            "description": "El desborde (variante única de ADR #104 FASE 2c): tras overflow_max_wait_ms la llamada pasa a overflow_queue_id, otra cola VIGENTE del tenant. Los dos campos juntos.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"overflow_max_wait_ms\": 120000,\n  \"overflow_queue_id\": \"{{inbound_queue_dest_id}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const q = pm.response.json().inbound_queue;",
                  "pm.test(\"shape: desborde escrito\", function () {",
                  "  pm.expect(q.overflow_max_wait_ms).to.eql(120000);",
                  "  pm.expect(q.overflow_queue_id).to.eql(pm.collectionVariables.get(\"inbound_queue_dest_id\"));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/inbound-queues/:id/overflow (ciclo -> 400)",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-queues/{{inbound_queue_dest_id}}/overflow",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-queues",
                "{{inbound_queue_dest_id}}",
                "overflow"
              ]
            },
            "description": "El destino ya desborda a la primera: B→A cerraría un ciclo A→B→A y es 400 invalid.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"overflow_max_wait_ms\": 60000,\n  \"overflow_queue_id\": \"{{inbound_queue_id}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "pm.test(\"nombra el ciclo\", function () { pm.expect(pm.response.json().error.message).to.include(\"cycle\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/inbound-queues/:id/overflow",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-queues/{{inbound_queue_id}}/overflow",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-queues",
                "{{inbound_queue_id}}",
                "overflow"
              ]
            },
            "description": "Quita el desborde: los dos campos vuelven a null."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const q = pm.response.json().inbound_queue;",
                  "pm.test(\"shape: sin desborde\", function () {",
                  "  pm.expect(q.overflow_max_wait_ms).to.eql(null);",
                  "  pm.expect(q.overflow_queue_id).to.eql(null);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/inbound-queues/:id/callback-offer",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-queues/{{inbound_queue_id}}/callback-offer",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-queues",
                "{{inbound_queue_id}}",
                "callback-offer"
              ]
            },
            "description": "Configura en la cola creada por este flujo el umbral medido a partir del cual se ofrece callback.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"callback_offer_after_ms\": 90000\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const q = pm.response.json().inbound_queue;",
                  "pm.test(\"shape: umbral configurado en la cola del flujo\", function () {",
                  "  pm.expect(q.id).to.eql(pm.collectionVariables.get(\"inbound_queue_id\"));",
                  "  pm.expect(q.callback_offer_after_ms).to.eql(90000);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/inbound-queues/:id/callback-offer",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-queues/{{inbound_queue_id}}/callback-offer",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-queues",
                "{{inbound_queue_id}}",
                "callback-offer"
              ]
            },
            "description": "Limpia el umbral de callback de la cola creada por este flujo."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const q = pm.response.json().inbound_queue;",
                  "pm.test(\"shape: umbral retirado de la cola del flujo\", function () {",
                  "  pm.expect(q.id).to.eql(pm.collectionVariables.get(\"inbound_queue_id\"));",
                  "  pm.expect(q.callback_offer_after_ms).to.eql(null);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/agents/:id/skills",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/agents/{{postman_agent_id}}/skills",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents",
                "{{postman_agent_id}}",
                "skills"
              ]
            },
            "description": "Skills de enrutado del agente (ADR #216 D3, core#1675): el PUT reemplaza el conjunto entero, ordenado y sin repetidos. Sólo admin.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"skills\": [\"idioma_es\", \"cobranza\"]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: skills ordenados\", function () {",
                  "  pm.expect(j.agent_id).to.eql(pm.collectionVariables.get(\"postman_agent_id\"));",
                  "  pm.expect(j.skills).to.eql([\"cobranza\", \"idioma_es\"]);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/agents/:id/skills",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/agents/{{postman_agent_id}}/skills",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "agents",
                "{{postman_agent_id}}",
                "skills"
              ]
            },
            "description": "Lee los skills del agente."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: skills ordenados\", function () {",
                  "  pm.expect(j.agent_id).to.eql(pm.collectionVariables.get(\"postman_agent_id\"));",
                  "  pm.expect(j.skills).to.eql([\"cobranza\", \"idioma_es\"]);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/inbound-queues/:id/skills",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-queues/{{inbound_queue_id}}/skills",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-queues",
                "{{inbound_queue_id}}",
                "skills"
              ]
            },
            "description": "Skills que exige la cola (requisito duro, todo-de). Sin desborde configurado la respuesta lleva el aviso required_skills_without_overflow.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"skills\": [\"idioma_es\", \"cobranza\"]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: skills ordenados\", function () {",
                  "  pm.expect(j.queue_id).to.eql(pm.collectionVariables.get(\"inbound_queue_id\"));",
                  "  pm.expect(j.skills).to.eql([\"cobranza\", \"idioma_es\"]);",
                  "});",
                  "pm.test(\"aviso D6: cola sin desborde\", function () { pm.expect(j.warnings).to.eql([\"required_skills_without_overflow\"]); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/inbound-queues/:id/skills",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-queues/{{inbound_queue_id}}/skills",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-queues",
                "{{inbound_queue_id}}",
                "skills"
              ]
            },
            "description": "Lee los skills que exige la cola."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: skills ordenados\", function () {",
                  "  pm.expect(j.queue_id).to.eql(pm.collectionVariables.get(\"inbound_queue_id\"));",
                  "  pm.expect(j.skills).to.eql([\"cobranza\", \"idioma_es\"]);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/inbound-routes (queue con queue_id de la API -> 201)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-routes",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-routes"
              ]
            },
            "description": "core#1347: una ruta queue nombra en queue_id una cola creada por /v1/inbound-queues, vigente y del mismo tenant. Número aleatorio por corrida.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"e164\": \"{{postman_queue_e164}}\",\n  \"dest_type\": \"queue\",\n  \"queue_id\": \"{{inbound_queue_id}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const nxx = 200 + Math.floor(Math.random() * 800);",
                  "const line = String(Math.floor(Math.random() * 10000)).padStart(4, \"0\");",
                  "const e164 = \"+1774\" + nxx + line;",
                  "pm.collectionVariables.set(\"postman_queue_e164\", e164);",
                  "// core#2414/#2429: una ruta activa exige un DID activo del tenant; se da de alta antes por la API.",
                  "pm.sendRequest({",
                  "  url: pm.variables.get(\"baseUrl\") + \"/v1/dids\",",
                  "  method: \"POST\",",
                  "  header: { \"Content-Type\": \"application/json\", \"Authorization\": \"Bearer \" + pm.variables.get(\"api_token\") },",
                  "  body: { mode: \"raw\", raw: JSON.stringify({ e164: e164, us_state: \"MA\" }) }",
                  "}, function (err, res) {",
                  "  pm.test(\"alta previa del DID de la ruta -> 201\", function () { pm.expect(err, \"sendRequest\").to.equal(null); pm.expect(res.code, \"POST /v1/dids\").to.equal(201); });",
                  "});"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const r = pm.response.json().inbound_route;",
                  "pm.test(\"shape: la ruta devuelve su cola\", function () { pm.expect(r.queue_id).to.eql(pm.collectionVariables.get(\"inbound_queue_id\")); });",
                  "pm.collectionVariables.set(\"inbound_queue_route_id\", r.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/inbound-routes/:id (hours: cerrado hacia otra cola -> 200)",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-routes/{{inbound_queue_route_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-routes",
                "{{inbound_queue_route_id}}"
              ]
            },
            "description": "core#2530 (ADR #304): la ruta lleva horario y festivos propios; fuera de ellos una llamada nueva se trata como la ruta que `closed` nombra (aquí otra cola vigente del tenant; también puede ser un mensaje que se renderiza a audio al guardar). Se lee de vuelta tal cual, con la ventana normalizada. Una ruta con horario se provisiona anclada en FreeSWITCH: tras escribirlo, POST .../provision.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"hours\": {\n    \"timezone\": \"America/Chicago\",\n    \"weekly\": {\n      \"mon\": {\n        \"start\": \"09:00\",\n        \"end\": \"17:00\"\n      },\n      \"tue\": {\n        \"start\": \"09:00\",\n        \"end\": \"17:00\"\n      }\n    },\n    \"holidays\": [\n      \"2026-12-25\"\n    ],\n    \"closed\": {\n      \"action\": \"route\",\n      \"dest_type\": \"queue\",\n      \"queue_id\": \"{{inbound_queue_dest_id}}\"\n    }\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const h = pm.response.json().inbound_route.hours;",
                  "pm.test(\"shape: horario, festivos y destino cerrado\", function () {",
                  "  pm.expect(h.timezone).to.eql(\"America/Chicago\");",
                  "  pm.expect(h.weekly.mon.start).to.eql(\"09:00:00\");",
                  "  pm.expect(h.holidays).to.eql([\"2026-12-25\"]);",
                  "  pm.expect(h.closed.action).to.eql(\"route\");",
                  "  pm.expect(h.closed.queue_id).to.eql(pm.collectionVariables.get(\"inbound_queue_dest_id\"));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/inbound-routes/:id (hours con una zona que no existe -> 422)",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-routes/{{inbound_queue_route_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-routes",
                "{{inbound_queue_route_id}}"
              ]
            },
            "description": "core#2530: todo rechazo del horario es 422 unprocessable y no guarda nada; el horario anterior sigue.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"hours\": {\n    \"timezone\": \"Mars/Olympus\",\n    \"weekly\": {\n      \"mon\": {\n        \"start\": \"09:00\",\n        \"end\": \"17:00\"\n      }\n    },\n    \"closed\": {\n      \"action\": \"route\",\n      \"dest_type\": \"queue\",\n      \"queue_id\": \"{{inbound_queue_dest_id}}\"\n    }\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 422\", function () { pm.response.to.have.status(422); });",
                  "const e = pm.response.json().error;",
                  "pm.test(\"shape: unprocessable, nombra hours\", function () {",
                  "  pm.expect(e.code).to.eql(\"unprocessable\");",
                  "  pm.expect(e.message).to.include(\"hours\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/inbound-routes/:id (hours: null lo quita -> 200)",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-routes/{{inbound_queue_route_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-routes",
                "{{inbound_queue_route_id}}"
              ]
            },
            "description": "core#2530: `hours: null` quita el horario (la ruta atiende a toda hora, como antes); el GET devuelve `hours: null` en una ruta sin horario.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"hours\": null\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const r = pm.response.json().inbound_route;",
                  "pm.test(\"shape: sin horario\", function () {",
                  "  pm.expect(r).to.have.property(\"hours\");",
                  "  pm.expect(r.hours).to.eql(null);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/inbound-queues/:id/retire (una ruta activa la usa -> 409)",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-queues/{{inbound_queue_id}}/retire",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-queues",
                "{{inbound_queue_id}}",
                "retire"
              ]
            },
            "description": "Una cola que nombra una ruta queue ACTIVA no se retira: 409 nombrando la ruta."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 409\", function () { pm.response.to.have.status(409); });",
                  "pm.test(\"nombra la ruta\", function () { pm.expect(pm.response.json().error.message).to.include(pm.collectionVariables.get(\"inbound_queue_route_id\")); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/inbound-routes/:id/disable (libera la cola)",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-routes/{{inbound_queue_route_id}}/disable",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-routes",
                "{{inbound_queue_route_id}}",
                "disable"
              ]
            },
            "description": "Desactivar la ruta suelta la cola."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/inbound-queues/:id/retire",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-queues/{{inbound_queue_id}}/retire",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-queues",
                "{{inbound_queue_id}}",
                "retire"
              ]
            },
            "description": "Retirada terminal: sale de la lista y ya no se nombra desde una ruta, un menú o un desborde."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const q = pm.response.json().inbound_queue;",
                  "pm.test(\"shape: retirada\", function () {",
                  "  pm.expect(q.status).to.eql(\"retired\");",
                  "  pm.expect(q.retired_at).to.be.a(\"string\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/inbound-routes (queue con una cola retirada -> 400)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-routes",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-routes"
              ]
            },
            "description": "core#1347: una cola retirada no vale como destino; 400 invalid sobre queue_id y la ruta no se escribe.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"e164\": \"{{postman_queue_e164}}\",\n  \"dest_type\": \"queue\",\n  \"queue_id\": \"{{inbound_queue_id}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const nxx = 200 + Math.floor(Math.random() * 800);",
                  "const line = String(Math.floor(Math.random() * 10000)).padStart(4, \"0\");",
                  "const e164 = \"+1774\" + nxx + line;",
                  "pm.collectionVariables.set(\"postman_queue_e164\", e164);",
                  "// core#2414/#2429: una ruta activa exige un DID activo del tenant; se da de alta antes por la API.",
                  "pm.sendRequest({",
                  "  url: pm.variables.get(\"baseUrl\") + \"/v1/dids\",",
                  "  method: \"POST\",",
                  "  header: { \"Content-Type\": \"application/json\", \"Authorization\": \"Bearer \" + pm.variables.get(\"api_token\") },",
                  "  body: { mode: \"raw\", raw: JSON.stringify({ e164: e164, us_state: \"MA\" }) }",
                  "}, function (err, res) {",
                  "  pm.test(\"alta previa del DID de la ruta -> 201\", function () { pm.expect(err, \"sendRequest\").to.equal(null); pm.expect(res.code, \"POST /v1/dids\").to.equal(201); });",
                  "});"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "pm.test(\"nombra queue_id\", function () { pm.expect(pm.response.json().error.message).to.include(\"queue_id\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/inbound-queues/:id/retire (el destino, limpieza)",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/inbound-queues/{{inbound_queue_dest_id}}/retire",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "inbound-queues",
                "{{inbound_queue_dest_id}}",
                "retire"
              ]
            },
            "description": "Retira la cola destino: ya nada la nombra."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "39 · dispositions",
      "description": "Catalogo de `dialer.v1.Disposition` en la ortografia REST (core#1456): solo lectura, igual para todo tenant, derivado de la tabla unica `Dialer.Dispositions.Catalog`. El vocabulario de `call_attempts.disposition` es abierto y puede llevar codigos que no estan aqui (`abandoned`, `delivered`...).",
      "item": [
        {
          "name": "GET /v1/dispositions",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/dispositions",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "dispositions"
              ]
            },
            "description": "Lista cerrada del contrato: valor REST, nombre del enum, numero y descripcion, ordenada por numero. No incluye `unspecified`."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const rows = pm.response.json().dispositions;",
                  "pm.test(\"el catalogo es la lista cerrada del contrato, sin `unspecified`\", function () {",
                  "  pm.expect(rows).to.be.an(\"array\").that.is.not.empty;",
                  "  pm.expect(rows.map(r => r.value)).to.not.include(\"unspecified\");",
                  "});",
                  "pm.test(\"cada fila trae valor REST, nombre del enum, numero y descripcion\", function () {",
                  "  rows.forEach(r => {",
                  "    pm.expect(r.value).to.match(/^[a-z_]+$/);",
                  "    pm.expect(r.enum).to.eql(\"DISPOSITION_\" + r.value.toUpperCase());",
                  "    pm.expect(r.number).to.be.a(\"number\").above(0);",
                  "    pm.expect(r.description).to.be.a(\"string\").that.is.not.empty;",
                  "  });",
                  "});",
                  "pm.test(\"ordenado por numero de enum, sin valores ni numeros repetidos\", function () {",
                  "  const nums = rows.map(r => r.number);",
                  "  pm.expect(nums).to.eql(nums.slice().sort((a, b) => a - b));",
                  "  pm.expect(new Set(nums).size).to.eql(nums.length);",
                  "  pm.expect(new Set(rows.map(r => r.value)).size).to.eql(rows.length);",
                  "});",
                  "pm.test(\"answered_human es DISPOSITION_ANSWERED_HUMAN = 1\", function () {",
                  "  const h = rows.find(r => r.value === \"answered_human\");",
                  "  pm.expect(h.enum).to.eql(\"DISPOSITION_ANSWERED_HUMAN\");",
                  "  pm.expect(h.number).to.eql(1);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/dispositions (sin bearer -> 401)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/dispositions",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "dispositions"
              ]
            },
            "description": "Sin credencial el catalogo no se sirve.",
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 401: el catalogo exige bearer como el resto de /v1\", function () { pm.response.to.have.status(401); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "40 · import-mappings",
      "item": [
        {
          "name": "POST /v1/import-mappings",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/import-mappings",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "import-mappings"
              ]
            },
            "description": "Crea una plantilla con nombre unico por corrida: nace como version 1, con el mapeo EN ORDEN y el separador explicito (comma por defecto).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"{{import_mapping_name}}\",\n  \"mapping\": [\n    {\n      \"column\": \"Cuenta\",\n      \"field\": \"external_ref\"\n    },\n    {\n      \"column\": \"Saldo\",\n      \"field\": \"amount_cents\"\n    },\n    {\n      \"column\": \"Notas\",\n      \"field\": \"skip\"\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set(\"import_mapping_name\", \"postman-mapping-\" + Date.now());",
                  "pm.collectionVariables.set(\"import_mapping_ref\", \"postman-mapping-debt-\" + Date.now());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const t = pm.response.json().import_mapping;",
                  "pm.test(\"shape ImportMapping: version 1, mapeo en orden, opciones explicitas\", function () {",
                  "  pm.expect(t.id).to.be.a(\"string\");",
                  "  pm.expect(t.name).to.eql(pm.collectionVariables.get(\"import_mapping_name\"));",
                  "  pm.expect(t.archived).to.eql(false);",
                  "  pm.expect(t.version).to.eql(1);",
                  "  pm.expect(t.mapping.map(c => c.column)).to.eql([\"Cuenta\", \"Saldo\", \"Notas\"]);",
                  "  pm.expect(t.options.separator).to.eql(\"comma\");",
                  "});",
                  "pm.collectionVariables.set(\"import_mapping_id\", t.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/import-mappings — nombre repetido (409)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/import-mappings",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "import-mappings"
              ]
            },
            "description": "El nombre es unico entre las plantillas vivas del tenant: repetirlo es un 409 y no escribe otra.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"{{import_mapping_name}}\",\n  \"mapping\": [\n    {\n      \"column\": \"Cuenta\",\n      \"field\": \"external_ref\"\n    },\n    {\n      \"column\": \"Saldo\",\n      \"field\": \"amount_cents\"\n    },\n    {\n      \"column\": \"Notas\",\n      \"field\": \"skip\"\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 409\", function () { pm.response.to.have.status(409); });",
                  "pm.test(\"conflict\", function () { pm.expect(pm.response.json().error.code).to.eql(\"conflict\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/import-mappings — mapeo vacio (400)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/import-mappings",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "import-mappings"
              ]
            },
            "description": "Un cuerpo que no es una plantilla (mapeo vacio) es un 400 y no escribe nada.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"{{import_mapping_name}}-vacia\",\n  \"mapping\": []\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "pm.test(\"bad_request\", function () { pm.expect(pm.response.json().error.code).to.eql(\"bad_request\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/import-mappings",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/import-mappings",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "import-mappings"
              ]
            },
            "description": "Las plantillas vivas del tenant con su version vigente; la de esta corrida esta."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: import_mappings[] y la plantilla de esta corrida esta\", function () {",
                  "  pm.expect(j.import_mappings).to.be.an(\"array\");",
                  "  const mine = j.import_mappings.find(t => t.id === pm.collectionVariables.get(\"import_mapping_id\"));",
                  "  pm.expect(mine).to.be.an(\"object\");",
                  "  pm.expect(mine.version).to.eql(1);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/import-mappings/:id",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/import-mappings/{{import_mapping_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "import-mappings",
                "{{import_mapping_id}}"
              ]
            },
            "description": "La cabecera y la version vigente de la plantilla."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const t = pm.response.json().import_mapping;",
                  "pm.test(\"es la plantilla de esta corrida, en la version 1\", function () {",
                  "  pm.expect(t.id).to.eql(pm.collectionVariables.get(\"import_mapping_id\"));",
                  "  pm.expect(t.version).to.eql(1);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/import-mappings/:id — id desconocido (404)",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/import-mappings/{{import_mapping_unknown_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "import-mappings",
                "{{import_mapping_unknown_id}}"
              ]
            },
            "description": "Un id que no es del tenant (aqui, un UUID al azar) es un 404 del recurso, no un `no such route`: el router casa la ruta."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set(\"import_mapping_unknown_id\", \"00000000-0000-4000-8000-\" + String(Date.now()).padStart(12, \"0\").slice(-12));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "pm.test(\"not_found del recurso\", function () { pm.expect(pm.response.json().error.code).to.eql(\"not_found\"); pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/import-mappings/:id — nueva version",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/import-mappings/{{import_mapping_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "import-mappings",
                "{{import_mapping_id}}"
              ]
            },
            "description": "Editar las opciones escribe la version 2 y deja la 1 intacta: una importacion pudo usarla. `base_version` es concurrencia optimista.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"options\": {\n    \"separator\": \"semicolon\"\n  },\n  \"base_version\": 1\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const t = pm.response.json().import_mapping;",
                  "pm.test(\"version 2 con el separador nuevo y el mismo mapeo\", function () {",
                  "  pm.expect(t.version).to.eql(2);",
                  "  pm.expect(t.options.separator).to.eql(\"semicolon\");",
                  "  pm.expect(t.mapping.length).to.eql(3);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/import-mappings/:id — base_version vieja (409)",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/import-mappings/{{import_mapping_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "import-mappings",
                "{{import_mapping_id}}"
              ]
            },
            "description": "Con una base_version que ya no es la vigente el PATCH es un 409 y no escribe una version 3.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"options\": {\n    \"separator\": \"tab\"\n  },\n  \"base_version\": 1\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 409\", function () { pm.response.to.have.status(409); });",
                  "pm.test(\"conflict\", function () { pm.expect(pm.response.json().error.code).to.eql(\"conflict\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/import-mappings/:id/versions",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/import-mappings/{{import_mapping_id}}/versions",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "import-mappings",
                "{{import_mapping_id}}",
                "versions"
              ]
            },
            "description": "Las versiones inmutables, de la 1 a la vigente."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const v = pm.response.json().versions;",
                  "pm.test(\"dos versiones en orden; la 1 conserva su separador\", function () {",
                  "  pm.expect(v.map(x => x.version)).to.eql([1, 2]);",
                  "  pm.expect(v[0].options.separator).to.eql(\"comma\");",
                  "  pm.expect(v[1].options.separator).to.eql(\"semicolon\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/import-mappings/:id/versions/:version",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/import-mappings/{{import_mapping_id}}/versions/{{import_mapping_version_first}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "import-mappings",
                "{{import_mapping_id}}",
                "versions",
                "{{import_mapping_version_first}}"
              ]
            },
            "description": "Una version concreta, tal como se escribio."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set(\"import_mapping_version_first\", \"1\");"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const v = pm.response.json().version;",
                  "pm.test(\"version 1 intacta\", function () {",
                  "  pm.expect(v.version).to.eql(1);",
                  "  pm.expect(v.mapping_id).to.eql(pm.collectionVariables.get(\"import_mapping_id\"));",
                  "  pm.expect(v.options.separator).to.eql(\"comma\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/import-mappings/:id/versions/:version — inexistente (404)",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/import-mappings/{{import_mapping_id}}/versions/{{import_mapping_version_unknown}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "import-mappings",
                "{{import_mapping_id}}",
                "versions",
                "{{import_mapping_version_unknown}}"
              ]
            },
            "description": "Una version que no existe es un 404 del recurso."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set(\"import_mapping_version_unknown\", \"9\");"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "pm.test(\"not_found del recurso\", function () { pm.expect(pm.response.json().error.code).to.eql(\"not_found\"); pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/debts/import?mapping_id= — registra la version usada",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/debts/import?mapping_id={{import_mapping_id}}&mapping_version=1",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debts",
                "import"
              ],
              "query": [
                {
                  "key": "mapping_id",
                  "value": "{{import_mapping_id}}"
                },
                {
                  "key": "mapping_version",
                  "value": "1"
                }
              ]
            },
            "description": "La plantilla se resuelve ANTES de importar y la version usada se anexa DESPUES: la respuesta trae `mapping {id, version}`. `mapping_version=1` fija la version antigua.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"debts\": [\n    {\n      \"external_ref\": \"{{import_mapping_ref}}\",\n      \"consumer_ref\": \"{{import_mapping_ref}}-c\",\n      \"amount_cents\": 1000,\n      \"currency\": \"USD\"\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"entra 1 y la respuesta dice que version de la plantilla uso\", function () {",
                  "  pm.expect(j.imported).to.eql(1);",
                  "  pm.expect(j.failed).to.eql([]);",
                  "  pm.expect(j.mapping).to.eql({ id: pm.collectionVariables.get(\"import_mapping_id\"), version: 1 });",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/import-mappings/:id/uses",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/import-mappings/{{import_mapping_id}}/uses",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "import-mappings",
                "{{import_mapping_id}}",
                "uses"
              ]
            },
            "description": "Una fila por importacion que declaro la plantilla: version, cuantos items entraron y cuantos fallaron."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const u = pm.response.json().uses;",
                  "pm.test(\"una importacion con la version 1, 1 entrada y 0 fallos\", function () {",
                  "  pm.expect(u.length).to.eql(1);",
                  "  pm.expect(u[0].version).to.eql(1);",
                  "  pm.expect(u[0].imported).to.eql(1);",
                  "  pm.expect(u[0].failed).to.eql(0);",
                  "  pm.expect(u[0].list_id).to.eql(null);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/import-mappings/:id — archivar",
          "request": {
            "method": "DELETE",
            "url": {
              "raw": "{{baseUrl}}/v1/import-mappings/{{import_mapping_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "import-mappings",
                "{{import_mapping_id}}"
              ]
            },
            "description": "Borrar es archivar: libera el nombre y deja el historial. Idempotente."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 204\", function () { pm.response.to.have.status(204); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/import-mappings/:id — archivada sigue legible",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/v1/import-mappings/{{import_mapping_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "import-mappings",
                "{{import_mapping_id}}"
              ]
            },
            "description": "La plantilla archivada conserva su cabecera y sus versiones."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"archived true\", function () { pm.expect(pm.response.json().import_mapping.archived).to.eql(true); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/debts/import?mapping_id= — plantilla archivada (409)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/debts/import?mapping_id={{import_mapping_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debts",
                "import"
              ],
              "query": [
                {
                  "key": "mapping_id",
                  "value": "{{import_mapping_id}}"
                }
              ]
            },
            "description": "Una plantilla archivada no se usa para importar: 409 y no entra nada.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"debts\": [\n    {\n      \"external_ref\": \"{{import_mapping_ref}}-archivada\",\n      \"consumer_ref\": \"{{import_mapping_ref}}-c2\"\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 409\", function () { pm.response.to.have.status(409); });",
                  "pm.test(\"conflict\", function () { pm.expect(pm.response.json().error.code).to.eql(\"conflict\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/import-mappings/:id — archivada no se edita (409)",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/import-mappings/{{import_mapping_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "import-mappings",
                "{{import_mapping_id}}"
              ]
            },
            "description": "Una plantilla archivada no admite editar el mapeo.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"options\": {\n    \"separator\": \"tab\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 409\", function () { pm.response.to.have.status(409); });",
                  "pm.test(\"conflict\", function () { pm.expect(pm.response.json().error.code).to.eql(\"conflict\"); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "41 · sso",
      "item": [
        {
          "name": "GET v1/auth/sso/:tenant_id/metadata (publica: EntityDescriptor)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/sso/{{tenant_id}}/metadata",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "sso",
                "{{tenant_id}}",
                "metadata"
              ]
            },
            "description": "Metadata SAML del SP del tenant: publica, sin sesion, derivada solo del tenant id (core#1473).",
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"EntityDescriptor con ACS del tenant\", function () { const b = pm.response.text(); pm.expect(b).to.include(\"EntityDescriptor\"); pm.expect(b).to.include(pm.collectionVariables.get(\"tenant_id\")); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST v1/auth/sso/start (tenant sin IdP -> 404)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/sso/start",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "sso",
                "start"
              ]
            },
            "description": "Un tenant sin IdP habilitado no inicia login SSO: un unico 404, sin oraculo entre 'no existe' y 'sin SSO'.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"tenant_id\": \"{{tenant_id}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "pm.test(\"codigo not_found\", function () { pm.expect(pm.response.json().error.code).to.eql(\"not_found\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST v1/auth/sso/start (sin tenant_id -> 400)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/sso/start",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "sso",
                "start"
              ]
            },
            "description": "Sin tenant_id uuid -> 400.",
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST v1/auth/sso/:tenant_id/acs (respuesta basura -> 303 sso_failed)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/x-www-form-urlencoded"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/sso/{{tenant_id}}/acs",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "sso",
                "{{tenant_id}}",
                "acs"
              ]
            },
            "description": "Una SAMLResponse basura nunca abre sesion: 303 de vuelta al dashboard con error generico, sin code.",
            "body": {
              "mode": "urlencoded",
              "urlencoded": [
                {
                  "key": "SAMLResponse",
                  "value": "bm8tZXMtdW5hLXJlc3B1ZXN0YQ=="
                },
                {
                  "key": "RelayState",
                  "value": "00000000-0000-4000-8000-000000000000"
                }
              ]
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 303\", function () { pm.response.to.have.status(303); });",
                  "pm.test(\"location lleva error generico y ningun code\", function () { const l = pm.response.headers.get(\"Location\") || \"\"; pm.expect(l).to.include(\"error=sso_failed\"); pm.expect(l).to.not.include(\"code=\"); });"
                ]
              }
            }
          ],
          "protocolProfileBehavior": {
            "followRedirects": false
          }
        },
        {
          "name": "POST v1/auth/sso/:tenant_id/slo (LogoutRequest basura -> 400 uniforme)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/x-www-form-urlencoded"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/sso/{{tenant_id}}/slo",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "sso",
                "{{tenant_id}}",
                "slo"
              ]
            },
            "description": "Single Logout del IdP por el binding HTTP-POST: una SAMLRequest que no es un LogoutRequest firmado por el IdP del tenant no revoca ninguna sesion y se rechaza con el mismo 400 uniforme (fail-closed, nunca 500). El caso feliz (LogoutRequest firmado, 303 de vuelta al IdP) necesita la clave del IdP y vive en los tests de core (core#1581).",
            "auth": {
              "type": "noauth"
            },
            "body": {
              "mode": "urlencoded",
              "urlencoded": [
                {
                  "key": "SAMLRequest",
                  "value": "bm8tZXMtdW4tbG9nb3V0cmVxdWVzdA=="
                },
                {
                  "key": "RelayState",
                  "value": "postman-slo"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "pm.test(\"rechazo uniforme, sin revocar ni filtrar el motivo\", function () { const e = pm.response.json().error; pm.expect(e.code).to.eql(\"bad_request\"); pm.expect(e.message).to.eql(\"logout request rejected\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET v1/auth/sso/:tenant_id/slo (binding Redirect sin firma valida -> 400 uniforme)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/sso/{{tenant_id}}/slo?SAMLRequest=bm8tZXMtdW4tbG9nb3V0cmVxdWVzdA%3D%3D&SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256&Signature=AAAA",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "sso",
                "{{tenant_id}}",
                "slo"
              ],
              "query": [
                {
                  "key": "SAMLRequest",
                  "value": "bm8tZXMtdW4tbG9nb3V0cmVxdWVzdA=="
                },
                {
                  "key": "SigAlg",
                  "value": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
                },
                {
                  "key": "Signature",
                  "value": "AAAA"
                }
              ]
            },
            "description": "Single Logout del IdP por el binding HTTP-Redirect: el tenant_id del stack no tiene IdP configurado (sso/start da 404 en esta misma carpeta), asi que la SAMLRequest se rechaza antes de verificar la Signature, no revoca nada y da el mismo 400 uniforme que el binding POST. La Signature de otra clave con IdP configurado la cubren los tests de core (core#1581).",
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "pm.test(\"rechazo uniforme, sin revocar ni filtrar el motivo\", function () { const e = pm.response.json().error; pm.expect(e.code).to.eql(\"bad_request\"); pm.expect(e.message).to.eql(\"logout request rejected\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST v1/auth/sso/exchange (code desconocido -> 401)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/sso/exchange",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "sso",
                "exchange"
              ]
            },
            "description": "Un code desconocido -> 401 uniforme.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"code\": \"codigo-que-no-existe\",\n  \"browser_secret\": \"secreto-que-no-existe\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 401\", function () { pm.response.to.have.status(401); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST v1/auth/sso/exchange (sin browser_secret -> 400)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/auth/sso/exchange",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "auth",
                "sso",
                "exchange"
              ]
            },
            "description": "Sin browser_secret -> 400.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"code\": \"x\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET v1/sso/idp-config (dd_ -> 403)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/sso/idp-config",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "sso",
                "idp-config"
              ]
            },
            "description": "Lectura: solo account_admin con sesion ddu_."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403 role_forbidden: una llave dd_ no administra el SSO\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"codigo role_forbidden\", function () { pm.expect(pm.response.json().error.code).to.eql(\"role_forbidden\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT v1/sso/idp-config (dd_ -> 403)",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/sso/idp-config",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "sso",
                "idp-config"
              ]
            },
            "description": "Escritura: una llave dd_ filtrada no puede apuntar el login a un IdP del atacante.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"idp_entity_id\": \"https://idp.invalid/e\",\n  \"sso_url\": \"https://idp.invalid/sso\",\n  \"certificates\": [],\n  \"enabled\": false\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403 role_forbidden: una llave dd_ no administra el SSO\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"codigo role_forbidden\", function () { pm.expect(pm.response.json().error.code).to.eql(\"role_forbidden\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH v1/sso/idp-config (dd_ -> 403)",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/sso/idp-config",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "sso",
                "idp-config"
              ]
            },
            "description": "Habilitar/deshabilitar: solo account_admin.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"enabled\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403 role_forbidden: una llave dd_ no administra el SSO\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"codigo role_forbidden\", function () { pm.expect(pm.response.json().error.code).to.eql(\"role_forbidden\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE v1/sso/idp-config (dd_ -> 403)",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/sso/idp-config",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "sso",
                "idp-config"
              ]
            },
            "description": "Borrado: solo account_admin."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403 role_forbidden: una llave dd_ no administra el SSO\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"codigo role_forbidden\", function () { pm.expect(pm.response.json().error.code).to.eql(\"role_forbidden\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET v1/sso/idp-config/events (dd_ -> 403)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/sso/idp-config/events",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "sso",
                "idp-config",
                "events"
              ]
            },
            "description": "Historial de cambios: solo account_admin."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403 role_forbidden: una llave dd_ no administra el SSO\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"codigo role_forbidden\", function () { pm.expect(pm.response.json().error.code).to.eql(\"role_forbidden\"); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "42 · tenant-calling-window",
      "description": "Horario de llamadas del tenant (R19 `tenant_calling_window`, core#1527): una tercera ventana semanal sobre todas las campañas que SÓLO estrecha — la ventana efectiva es R18 (legal, hora local del destino) ∩ ventana de la campaña ∩ ventana del tenant. Malformado => 422 con el motivo y nada se guarda; un valor guardado que no se puede evaluar no deja salir la llamada (regla propia en gate_blocks). Escribir: llave dd_ o account_admin (un login `member` recibe 403 role_forbidden). La carpeta termina con DELETE para no dejar horario en el stack.",
      "item": [
        {
          "name": "GET /v1/tenant/calling-window (sin horario propio)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/tenant/calling-window",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "tenant",
                "calling-window"
              ]
            },
            "description": "Un tenant que nunca escribió horario lee `null`: el comportamiento es el de hoy (R18 ∩ ventana de la campaña). Es el estado limpio del que parte la carpeta y al que vuelve."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: calling_window null\", function () { pm.expect(j).to.have.property(\"calling_window\", null); });"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/tenant/calling-window (malformado: 422 y no se guarda)",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/tenant/calling-window",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "tenant",
                "calling-window"
              ]
            },
            "description": "Fail-closed al guardar: un día que no existe es 422 con el motivo y NO se almacena nada (lo prueba core en `test/db/tenant_calling_window_api_test.exs`; esta carpeta sólo comprueba el 422 y su motivo).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"timezone\": \"America/Chicago\",\n  \"weekly\": {\n    \"funday\": {\n      \"start\": \"09:00\",\n      \"end\": \"20:00\"\n    }\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 422\", function () { pm.response.to.have.status(422); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: error unprocessable con motivo\", function () {",
                  "  pm.expect(j.error.code).to.eql(\"unprocessable\");",
                  "  pm.expect(j.error.message).to.be.a(\"string\").and.not.eql(\"\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/tenant/calling-window",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/tenant/calling-window",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "tenant",
                "calling-window"
              ]
            },
            "description": "Guarda el horario semanal del tenant (zona IANA explícita + día->{start,end}; un día sin entrada está CERRADO). Sólo estrecha: la ventana efectiva es R18 ∩ campaña ∩ tenant. Responde la forma normalizada (HH:MM:SS). Llave dd_ o account_admin.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"timezone\": \"America/Chicago\",\n  \"weekly\": {\n    \"mon\": {\n      \"start\": \"09:00\",\n      \"end\": \"20:00\"\n    },\n    \"tue\": {\n      \"start\": \"09:00\",\n      \"end\": \"20:00\"\n    },\n    \"wed\": {\n      \"start\": \"09:00\",\n      \"end\": \"20:00\"\n    },\n    \"thu\": {\n      \"start\": \"09:00\",\n      \"end\": \"20:00\"\n    },\n    \"fri\": {\n      \"start\": \"09:00\",\n      \"end\": \"20:00\"\n    }\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: ventana normalizada\", function () {",
                  "  pm.expect(j.calling_window.timezone).to.eql(\"America/Chicago\");",
                  "  pm.expect(j.calling_window.weekly.mon).to.eql({ start: \"09:00:00\", end: \"20:00:00\" });",
                  "  pm.expect(j.calling_window.weekly).to.not.have.property(\"sat\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/tenant/calling-window",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/tenant/calling-window",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "tenant",
                "calling-window"
              ]
            },
            "description": "Lee lo guardado, tal cual lo normalizó el PUT."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: lo que se guardó\", function () {",
                  "  pm.expect(j.calling_window.timezone).to.eql(\"America/Chicago\");",
                  "  pm.expect(j.calling_window.weekly.fri).to.eql({ start: \"09:00:00\", end: \"20:00:00\" });",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/tenant/calling-window",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/tenant/calling-window",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "tenant",
                "calling-window"
              ]
            },
            "description": "Quita el horario del tenant (vuelve a R18 ∩ campaña). Deja el stack sin horario propio: otras carpetas marcan sin que esta les cierre la jornada."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: calling_window null\", function () { pm.expect(pm.response.json()).to.have.property(\"calling_window\", null); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/tenant/calling-window (tras DELETE)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/tenant/calling-window",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "tenant",
                "calling-window"
              ]
            },
            "description": "Control: tras el DELETE el tenant vuelve a no tener horario propio."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: calling_window null\", function () { pm.expect(pm.response.json()).to.have.property(\"calling_window\", null); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "43 · skip-trace",
      "item": [
        {
          "name": "GET /v1/skip-trace/cap (sin tope)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/skip-trace/cap",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "skip-trace",
                "cap"
              ]
            },
            "description": "Un tenant sin tope lee los dos límites en null y uso 0 (core#1627, ADR #213)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: limites null y uso 0\", function () {",
                  "  pm.expect(j.cap).to.have.property(\"max_queries_per_month\", null);",
                  "  pm.expect(j.cap).to.have.property(\"max_usd_per_month\", null);",
                  "  pm.expect(j.usage.queries).to.eql(0);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/skip-trace/cap (fija el tope)",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/skip-trace/cap",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "skip-trace",
                "cap"
              ]
            },
            "description": "Fija los dos límites mensuales; la respuesta es el valor guardado.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"max_queries_per_month\": 5,\n  \"max_usd_per_month\": \"2.5\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: tope guardado\", function () {",
                  "  pm.expect(j.cap.max_queries_per_month).to.eql(5);",
                  "  pm.expect(Number(j.cap.max_usd_per_month)).to.eql(2.5);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/skip-trace/cap (invalido: 422)",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/skip-trace/cap",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "skip-trace",
                "cap"
              ]
            },
            "description": "Un límite inválido es 422 con el motivo y no se guarda nada.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"max_queries_per_month\": -1\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 422\", function () { pm.response.to.have.status(422); });",
                  "pm.test(\"shape: error unprocessable\", function () { pm.expect(pm.response.json().error.code).to.eql(\"unprocessable\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/skip-trace/lookups (sin proveedor: 503)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/skip-trace/lookups",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "skip-trace",
                "lookups"
              ]
            },
            "description": "El stack e2e no configura proveedor de skip tracing: la consulta es 503 not_configured, no llama a nada ni mide.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"request_key\": \"newman-skip-trace-0001\",\n  \"subject\": {\n    \"name\": \"Jane Doe\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 503\", function () { pm.response.to.have.status(503); });",
                  "pm.test(\"shape: not_configured\", function () { pm.expect(pm.response.json().error.code).to.eql(\"not_configured\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/skip-trace/lookups (cuerpo invalido: 422)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/skip-trace/lookups",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "skip-trace",
                "lookups"
              ]
            },
            "description": "Una clave de petición demasiado corta es 422 antes de tocar nada.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"request_key\": \"x\",\n  \"subject\": {\n    \"name\": \"Jane Doe\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 422\", function () { pm.response.to.have.status(422); });",
                  "pm.test(\"shape: error unprocessable\", function () { pm.expect(pm.response.json().error.code).to.eql(\"unprocessable\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/skip-trace/cap (limpia el tope)",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/skip-trace/cap",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "skip-trace",
                "cap"
              ]
            },
            "description": "Devuelve la carpeta al estado limpio del que parte: sin límites.",
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: limites null\", function () {",
                  "  pm.expect(j.cap).to.have.property(\"max_queries_per_month\", null);",
                  "  pm.expect(j.cap).to.have.property(\"max_usd_per_month\", null);",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "44 · click-to-call publico",
      "description": "Carril publico completo: 202 uniforme sin oraculo de cuentas, oscuridad 404 por tenant y topes durables 429. El bloqueo por compuerta se prueba en tests/smoke/test_click_to_call_e2e.py porque ocurre asincronamente en el Runner.",
      "item": [
        {
          "name": "2. acunar sitio para uniformidad",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/click-to-call/sites",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "click-to-call",
                "sites"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"campaign_id\": \"{{golden_campaign_id}}\",\n  \"label\": \"postman-uniform\",\n  \"consent_text\": \"Acepto recibir una llamada sobre mi cuenta.\",\n  \"tenant_daily_cap\": 200,\n  \"number_daily_cap\": 10,\n  \"site_hourly_cap\": 20\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", () => pm.response.to.have.status(201));",
                  "const j = pm.response.json();",
                  "pm.test(\"plaintext ddc_ una vez y sin hash\", () => { pm.expect(j.token).to.match(/^ddc_/); pm.expect(j.site).to.not.have.property(\"token_hash\"); });",
                  "pm.collectionVariables.set(\"click_to_call_site_id\", j.site.id);",
                  "pm.collectionVariables.set(\"click_to_call_token\", j.token);",
                  "pm.collectionVariables.set(\"click_to_call_consent_version\", j.site.consent_version);",
                  "pm.collectionVariables.set(\"click_to_call_consent_sha256\", j.site.consent_sha256);",
                  "pm.collectionVariables.set(\"click_to_call_unknown_phone\", \"+1312\" + String(Date.now()).slice(-7));"
                ]
              }
            }
          ]
        },
        {
          "name": "3. GET configuracion publica",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Origin",
                "value": "{{click_to_call_origin}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/public/call-request-config",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "public",
                "call-request-config"
              ]
            },
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{click_to_call_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", () => pm.response.to.have.status(200));",
                  "pm.test(\"config pertenece al sitio\", () => pm.expect(pm.response.json().config.site_id).to.eql(pm.collectionVariables.get(\"click_to_call_site_id\")));",
                  "pm.test(\"config publica consentimiento vigente\", () => { const c = pm.response.json().config; pm.expect(c.consent_version).to.eql(pm.collectionVariables.get(\"click_to_call_consent_version\")); pm.expect(c.consent_sha256).to.eql(pm.collectionVariables.get(\"click_to_call_consent_sha256\")); });"
                ]
              }
            }
          ]
        },
        {
          "name": "4. 202 para numero con cuenta",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Origin",
                "value": "{{click_to_call_origin}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/public/call-requests",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "public",
                "call-requests"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"mode\": \"immediate\",\n  \"phone\": \"{{golden_contact_phone}}\",\n  \"consent_text_version\": \"{{click_to_call_consent_version}}\",\n  \"consent_text_sha256\": \"{{click_to_call_consent_sha256}}\",\n  \"consent_checked\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{click_to_call_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 202\", () => pm.response.to.have.status(202));",
                  "const j = pm.response.json();",
                  "pm.test(\"respuesta minima\", () => { pm.expect(Object.keys(j)).to.eql([\"request_id\"]); pm.expect(j.request_id).to.be.a(\"string\"); });",
                  "pm.collectionVariables.set(\"click_to_call_uniform_keys\", JSON.stringify(Object.keys(j)));"
                ]
              }
            }
          ]
        },
        {
          "name": "5. 202 uniforme para numero sin cuenta",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Origin",
                "value": "{{click_to_call_origin}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/public/call-requests",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "public",
                "call-requests"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"mode\": \"immediate\",\n  \"phone\": \"{{click_to_call_unknown_phone}}\",\n  \"consent_text_version\": \"{{click_to_call_consent_version}}\",\n  \"consent_text_sha256\": \"{{click_to_call_consent_sha256}}\",\n  \"consent_checked\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{click_to_call_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 202\", () => pm.response.to.have.status(202));",
                  "const j = pm.response.json();",
                  "pm.test(\"misma forma para cuenta y numero desconocido\", () => pm.expect(JSON.stringify(Object.keys(j))).to.eql(pm.collectionVariables.get(\"click_to_call_uniform_keys\")));"
                ]
              }
            }
          ]
        },
        {
          "name": "7. carril apagado responde 404",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Origin",
                "value": "{{click_to_call_origin}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/public/call-requests",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "public",
                "call-requests"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"mode\": \"immediate\",\n  \"phone\": \"{{click_to_call_unknown_phone}}\",\n  \"consent_text_version\": \"{{click_to_call_consent_version}}\",\n  \"consent_text_sha256\": \"{{click_to_call_consent_sha256}}\",\n  \"consent_checked\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{click_to_call_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", () => pm.response.to.have.status(404));",
                  "pm.test(\"404 indistinguible del catch-all\", () => { const e = pm.response.json().error; pm.expect(e.code).to.eql(\"not_found\"); pm.expect(e.message).to.eql(\"no such route\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "9. acunar sitio con tope horario dos",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/click-to-call/sites",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "click-to-call",
                "sites"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"campaign_id\": \"{{golden_campaign_id}}\",\n  \"label\": \"postman-cap\",\n  \"consent_text\": \"Acepto recibir una llamada sobre mi cuenta.\",\n  \"tenant_daily_cap\": 200,\n  \"number_daily_cap\": 10,\n  \"site_hourly_cap\": 2\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", () => pm.response.to.have.status(201));",
                  "const j = pm.response.json(); pm.collectionVariables.set(\"click_to_call_cap_site_id\", j.site.id); pm.collectionVariables.set(\"click_to_call_cap_token\", j.token); pm.collectionVariables.set(\"click_to_call_cap_consent_version\", j.site.consent_version); pm.collectionVariables.set(\"click_to_call_cap_consent_sha256\", j.site.consent_sha256);"
                ]
              }
            }
          ]
        },
        {
          "name": "10. tope — primera solicitud admitida",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Origin",
                "value": "{{click_to_call_origin}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/public/call-requests",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "public",
                "call-requests"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"mode\": \"immediate\",\n  \"phone\": \"+13125550181\",\n  \"consent_text_version\": \"{{click_to_call_cap_consent_version}}\",\n  \"consent_text_sha256\": \"{{click_to_call_cap_consent_sha256}}\",\n  \"consent_checked\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{click_to_call_cap_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 202\", () => pm.response.to.have.status(202));"
                ]
              }
            }
          ]
        },
        {
          "name": "11. tope — segunda solicitud admitida",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Origin",
                "value": "{{click_to_call_origin}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/public/call-requests",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "public",
                "call-requests"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"mode\": \"immediate\",\n  \"phone\": \"+13125550182\",\n  \"consent_text_version\": \"{{click_to_call_cap_consent_version}}\",\n  \"consent_text_sha256\": \"{{click_to_call_cap_consent_sha256}}\",\n  \"consent_checked\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{click_to_call_cap_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 202\", () => pm.response.to.have.status(202));"
                ]
              }
            }
          ]
        },
        {
          "name": "12. tope — tercera solicitud rechazada",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Origin",
                "value": "{{click_to_call_origin}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/public/call-requests",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "public",
                "call-requests"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"mode\": \"immediate\",\n  \"phone\": \"+13125550183\",\n  \"consent_text_version\": \"{{click_to_call_cap_consent_version}}\",\n  \"consent_text_sha256\": \"{{click_to_call_cap_consent_sha256}}\",\n  \"consent_checked\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{click_to_call_cap_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 429\", () => pm.response.to.have.status(429));",
                  "pm.test(\"rechazo uniforme sin revelar el tope\", () => { const e = pm.response.json().error; pm.expect(e.code).to.eql(\"rate_limited\"); pm.expect(pm.response.headers.has(\"Retry-After\")).to.eql(false); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "45 · email y mensajes por deuda",
      "description": "Canal de email (ADR 224) y lectura unificada por deuda (core#1772): proveedores, direcciones con procedencia, supresiones (solo lectura), mensajes y GET /v1/debts/{id}/messages. Los ids que solo nacen de estado vivo usan UUID aleatorio + aserciones anti-catch-all.",
      "item": [
        {
          "name": "POST /v1/email/providers",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/email/providers",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "email",
                "providers"
              ]
            },
            "description": "Alta de proveedor SMTP con host IP publico literal (un literal IP no pasa por DNS en el guardia de egress; nunca se conecta: no hay envio en esta carpeta). El secreto es de solo escritura: la respuesta solo dice has_secret. Se retira al final de la carpeta.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"adapter\": \"smtp\",\n  \"region\": \"us\",\n  \"name\": \"postman-email-{{$timestamp}}\",\n  \"from_address\": \"cobranza@sede.test\",\n  \"host\": \"203.0.113.25\",\n  \"port\": 587,\n  \"username\": \"u\",\n  \"secret\": \"postman-no-sale\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201 (proveedor creado)\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: email_provider sin secreto\", function () {",
                  "  pm.expect(j.email_provider.id).to.be.a(\"string\");",
                  "  pm.expect(j.email_provider.has_secret).to.eql(true);",
                  "  pm.expect(j.email_provider).to.not.have.property(\"secret\");",
                  "  pm.expect(pm.response.text()).to.not.include(\"postman-no-sale\");",
                  "});",
                  "pm.collectionVariables.set(\"email_provider_id\", j.email_provider.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/email/providers",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/email/providers",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "email",
                "providers"
              ]
            },
            "description": "Registro de proveedores de la sede."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200 (lista)\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: email_providers[] con el proveedor recien creado\", function () {",
                  "  pm.expect(j.email_providers).to.be.an(\"array\");",
                  "  pm.expect(j.email_providers.map(function (p) { return p.id; })).to.include(pm.collectionVariables.get(\"email_provider_id\"));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/email/providers/{id}",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/email/providers/{{email_provider_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "email",
                "providers",
                "{{email_provider_id}}"
              ]
            },
            "description": "Un proveedor por id."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200 (proveedor)\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: email_provider, has_secret y sin secret\", function () {",
                  "  pm.expect(j.email_provider.id).to.eql(pm.collectionVariables.get(\"email_provider_id\"));",
                  "  pm.expect(j.email_provider.has_secret).to.eql(true);",
                  "  pm.expect(j.email_provider).to.not.have.property(\"secret\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/email/providers/{id}",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/email/providers/{{email_provider_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "email",
                "providers",
                "{{email_provider_id}}"
              ]
            },
            "description": "Edicion de campos del proveedor activo.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"postman-email-renombrado\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200 (editado)\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: nombre cambiado\", function () { pm.expect(j.email_provider.name).to.eql(\"postman-email-renombrado\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/email/providers/{id}/retire",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/email/providers/{{email_provider_id}}/retire",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "email",
                "providers",
                "{{email_provider_id}}",
                "retire"
              ]
            },
            "description": "Retiro (solo retiro, nunca borrado).",
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200 (retirado)\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: status retired\", function () { pm.expect(j.email_provider.status).to.eql(\"retired\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/email/providers/{id} retirado",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/email/providers/{{email_provider_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "email",
                "providers",
                "{{email_provider_id}}"
              ]
            },
            "description": "Un proveedor retirado no se edita: 409 conflict, no 500.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"x\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 409 (retirado no editable)\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(409); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: error.code conflict\", function () { pm.expect(j.error.code).to.eql(\"conflict\"); });",
                  "pm.test(\"el 404/400 es del endpoint real (no catch-all)\", function () { pm.expect(j.error.message).to.not.eql(\"no such route\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/email/addresses",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/email/addresses",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "email",
                "addresses"
              ]
            },
            "description": "Alta de direccion con procedencia. Un contact_id inexistente es 400 fail-closed (la direccion no se crea).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"contact_id\": \"{{email_contact_id}}\",\n  \"address\": \"postman-{{$timestamp}}@gmail.com\",\n  \"provenance\": \"imported_unverified\",\n  \"provenance_evidence\": \"postman probe\",\n  \"obtained_at\": \"2026-01-01T00:00:00Z\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set(\"email_contact_id\", pm.variables.replaceIn(\"{{$guid}}\"));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400 (contacto inexistente)\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(400); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: error.code invalid\", function () { pm.expect(j.error.code).to.eql(\"invalid\"); });",
                  "pm.test(\"el 404/400 es del endpoint real (no catch-all)\", function () { pm.expect(j.error.message).to.not.eql(\"no such route\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/email/addresses",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/email/addresses?contact_id={{email_contact_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "email",
                "addresses"
              ],
              "query": [
                {
                  "key": "contact_id",
                  "value": "{{email_contact_id}}"
                }
              ]
            },
            "description": "Direcciones de un contacto, con sendable/ineligible_reason calculados al leer."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200 (lista)\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: email_addresses[] vacio para un contacto sin direcciones\", function () {",
                  "  pm.expect(j.email_addresses).to.be.an(\"array\").that.is.empty;",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/email/addresses/{id}",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/email/addresses/{{email_address_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "email",
                "addresses",
                "{{email_address_id}}"
              ]
            },
            "description": "Una direccion por id; un id inexistente es 404 del endpoint real."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set(\"email_address_id\", pm.variables.replaceIn(\"{{$guid}}\"));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404 (direccion inexistente)\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(404); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: error.code not_found\", function () { pm.expect(j.error.code).to.eql(\"not_found\"); });",
                  "pm.test(\"el 404/400 es del endpoint real (no catch-all)\", function () { pm.expect(j.error.message).to.not.eql(\"no such route\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/email/suppressions",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/email/suppressions?address=sin-historial-{{email_suppression_probe}}@gmail.com",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "email",
                "suppressions"
              ],
              "query": [
                {
                  "key": "address",
                  "value": "sin-historial-{{email_suppression_probe}}@gmail.com"
                }
              ]
            },
            "description": "Historial de supresion de UNA direccion (solo lectura; la ultima fila gana)."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set(\"email_suppression_probe\", pm.variables.replaceIn(\"{{$guid}}\"));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200 (historial)\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: status clear y email_suppressions[] vacio\", function () {",
                  "  pm.expect(j.status).to.eql(\"clear\");",
                  "  pm.expect(j.email_suppressions).to.be.an(\"array\").that.is.empty;",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/email/suppressions sin address",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/email/suppressions",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "email",
                "suppressions"
              ]
            },
            "description": "Sin address es 400, no una lista de todo el tenant."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400 (address obligatoria)\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(400); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: error.code bad_request\", function () { pm.expect(j.error.code).to.eql(\"bad_request\"); });",
                  "pm.test(\"el 404/400 es del endpoint real (no catch-all)\", function () { pm.expect(j.error.message).to.not.eql(\"no such route\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/email/messages",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/email/messages?limit=20",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "email",
                "messages"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "20"
                }
              ]
            },
            "description": "Libro de mensajes de email (originales + transiciones, keyset)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200 (lista)\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: email_messages[] + next_cursor, body_preview nunca body\", function () {",
                  "  pm.expect(j.email_messages).to.be.an(\"array\");",
                  "  pm.expect(j).to.have.property(\"next_cursor\");",
                  "  j.email_messages.forEach(function (m) {",
                  "    pm.expect(m).to.have.property(\"body_preview\");",
                  "    pm.expect(m).to.not.have.property(\"body\");",
                  "  });",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/email/messages/{id}",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/email/messages/{{email_message_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "email",
                "messages",
                "{{email_message_id}}"
              ]
            },
            "description": "Un mensaje (vista colapsada); un id inexistente es 404 del endpoint real."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set(\"email_message_id\", pm.variables.replaceIn(\"{{$guid}}\"));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404 (mensaje inexistente)\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(404); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: error.code not_found\", function () { pm.expect(j.error.code).to.eql(\"not_found\"); });",
                  "pm.test(\"el 404/400 es del endpoint real (no catch-all)\", function () { pm.expect(j.error.message).to.not.eql(\"no such route\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/debts/{id}/messages",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/debts/{{golden_debt_id}}/messages?limit=20",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debts",
                "{{golden_debt_id}}",
                "messages"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "20"
                }
              ]
            },
            "description": "Lectura unificada por deuda: que se le envio y por que canal (SMS + email), derivada al leer (ADR 218), ventana <=31 dias."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200 (lectura unificada)\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: messages[] + next_cursor; cada item lleva channel sms|email y body_preview\", function () {",
                  "  pm.expect(j.messages).to.be.an(\"array\");",
                  "  pm.expect(j).to.have.property(\"next_cursor\");",
                  "  j.messages.forEach(function (m) {",
                  "    pm.expect([\"sms\", \"email\"]).to.include(m.channel);",
                  "    pm.expect(m).to.not.have.property(\"body\");",
                  "  });",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/debts/{id}/messages deuda inexistente",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/debts/{{email_debt_id}}/messages",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debts",
                "{{email_debt_id}}",
                "messages"
              ]
            },
            "description": "Una deuda ajena o inexistente es 404, no una lista vacia."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set(\"email_debt_id\", pm.variables.replaceIn(\"{{$guid}}\"));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404 (deuda inexistente)\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(404); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: error.code not_found\", function () { pm.expect(j.error.code).to.eql(\"not_found\"); });",
                  "pm.test(\"el 404/400 es del endpoint real (no catch-all)\", function () { pm.expect(j.error.message).to.not.eql(\"no such route\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/debts/{id}/messages ventana invalida",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/debts/{{golden_debt_id}}/messages?from=2020-01-01T00:00:00Z&to=2026-01-01T00:00:00Z",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "debts",
                "{{golden_debt_id}}",
                "messages"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "2020-01-01T00:00:00Z"
                },
                {
                  "key": "to",
                  "value": "2026-01-01T00:00:00Z"
                }
              ]
            },
            "description": "Una ventana mayor que el tope es 400."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400 (ventana excesiva)\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(400); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: error.code bad_request\", function () { pm.expect(j.error.code).to.eql(\"bad_request\"); });",
                  "pm.test(\"el 404/400 es del endpoint real (no catch-all)\", function () { pm.expect(j.error.message).to.not.eql(\"no such route\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/campaigns/{id}/email-skips",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{golden_campaign_id}}/email-skips",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{golden_campaign_id}}",
                "email-skips"
              ]
            },
            "description": "Por que una campana de email NO envio a un lead (core#2098, ADR 260): contadores por motivo derivados de la tabla append-only email_lead_skips (ceros incluidos) y las ultimas filas. Sobre la campana golden (sin descartes) todos los motivos salen en 0."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: campaign_id, total_leads, reasons[11] con ceros, samples[]\", function () {",
                  "  pm.expect(j.campaign_id).to.eql(pm.collectionVariables.get(\"golden_campaign_id\"));",
                  "  pm.expect(j.total_leads).to.be.a(\"number\");",
                  "  pm.expect(j.samples).to.be.an(\"array\");",
                  "  pm.expect(j.reasons.map(function (r) { return r.reason; })).to.eql([\"debt_not_found\", \"debt_not_dialable\", \"no_sendable_address\", \"email_address_invalid\", \"email_debt_unresolved\", \"throttled_plan_inactive\", \"throttled_rate_limit\", \"throttled_provider_open\", \"throttled_spend_cap\", \"throttled_no_rate\", \"throttled_synthetic_tenant\"]);",
                  "  j.reasons.forEach(function (r) { pm.expect(r).to.have.keys([\"reason\", \"leads\", \"days\", \"first_at\", \"last_at\"]); });",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/campaigns/{id}/email-skips campana inexistente",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/campaigns/{{email_skips_ghost_campaign_id}}/email-skips",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "campaigns",
                "{{email_skips_ghost_campaign_id}}",
                "email-skips"
              ]
            },
            "description": "Una campana ajena o inexistente es 404, nunca un 200 vacio."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set(\"email_skips_ghost_campaign_id\", pm.variables.replaceIn(\"{{$guid}}\"));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404 (campana inexistente)\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(404); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: error.code not_found\", function () { pm.expect(j.error.code).to.eql(\"not_found\"); });",
                  "pm.test(\"el 404 es del endpoint real (no catch-all)\", function () { pm.expect(j.error.message).to.not.eql(\"no such route\"); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "46 · segment-filters",
      "description": "Filtros de segmentación guardados y materialización puntual a listas. El flujo enciende el flag del tenant, crea un filtro y conserva su id real, recorre las siete operaciones públicas y restaura el flag apagado.",
      "item": [
        {
          "name": "POST /v1/segment-filters",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/segment-filters",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"{{segment_filter_name}}\",\n  \"criteria\": {}\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "description": "Crea el filtro que consume el resto de la carpeta; el id se toma de la respuesta, no de una constante."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const run = Date.now();",
                  "pm.collectionVariables.set(\"segment_filter_name\", \"postman-segment-filter-\" + run);",
                  "pm.collectionVariables.set(\"segment_filter_saved_list_name\", \"postman-segment-saved-\" + run);",
                  "pm.collectionVariables.set(\"segment_filter_inline_list_name\", \"postman-segment-inline-\" + run);"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const f = pm.response.json().segment_filter;",
                  "pm.test(\"shape: filtro creado en versión 1\", function () {",
                  "  pm.expect(f.id).to.be.a(\"string\").and.not.empty;",
                  "  pm.expect(f.name).to.eql(pm.collectionVariables.get(\"segment_filter_name\"));",
                  "  pm.expect(f.version).to.eql(1);",
                  "  pm.expect(f.criteria).to.eql({});",
                  "  pm.expect(f.archived).to.eql(false);",
                  "});",
                  "pm.collectionVariables.set(\"segment_filter_id\", f.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/segment-filters",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/segment-filters",
            "description": "Lista los filtros vigentes e identifica el creado en esta corrida."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const rows = pm.response.json().segment_filters;",
                  "pm.test(\"shape: segment_filters[] contiene el filtro del flujo\", function () {",
                  "  pm.expect(rows).to.be.an(\"array\");",
                  "  const mine = rows.find(function (f) { return f.id === pm.collectionVariables.get(\"segment_filter_id\"); });",
                  "  pm.expect(mine).to.be.an(\"object\");",
                  "  pm.expect(mine.version).to.eql(1);",
                  "  pm.expect(mine.archived).to.eql(false);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/segment-filters/:id",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/segment-filters/{{segment_filter_id}}",
            "description": "Lee por el id devuelto por el POST de esta carpeta."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const f = pm.response.json().segment_filter;",
                  "pm.test(\"shape: filtro vigente pedido por id\", function () {",
                  "  pm.expect(f.id).to.eql(pm.collectionVariables.get(\"segment_filter_id\"));",
                  "  pm.expect(f.name).to.eql(pm.collectionVariables.get(\"segment_filter_name\"));",
                  "  pm.expect(f.version).to.eql(1);",
                  "  pm.expect(f.criteria).to.eql({});",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/segment-filters/:id",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/segment-filters/{{segment_filter_id}}",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"base_version\": 1,\n  \"criteria\": {\n    \"state\": \"open\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "description": "Reemplaza los criterios y exige la versión observada por el flujo."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const f = pm.response.json().segment_filter;",
                  "pm.test(\"shape: nueva versión con criterios reemplazados\", function () {",
                  "  pm.expect(f.id).to.eql(pm.collectionVariables.get(\"segment_filter_id\"));",
                  "  pm.expect(f.version).to.eql(2);",
                  "  pm.expect(f.criteria).to.eql({state: [\"open\"]});",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/segment-filters/:id/to-list",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/segment-filters/{{segment_filter_id}}/to-list",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"new_list_name\": \"{{segment_filter_saved_list_name}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "description": "Congela la última versión del filtro en una lista nueva de esta corrida."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: materialización desde filtro guardado\", function () {",
                  "  const m = j.segment_materialization;",
                  "  pm.expect(m.filter_id).to.eql(pm.collectionVariables.get(\"segment_filter_id\"));",
                  "  pm.expect(m.filter_version).to.eql(2);",
                  "  pm.expect(m.criteria).to.eql({state: [\"open\"]});",
                  "  pm.expect(m.matched).to.be.a(\"number\");",
                  "  pm.expect(m.added).to.be.a(\"number\");",
                  "  pm.expect(m.truncated).to.be.a(\"boolean\");",
                  "  pm.expect(j.debt_list.id).to.eql(m.list_id);",
                  "  pm.expect(j.debt_list.name).to.eql(pm.collectionVariables.get(\"segment_filter_saved_list_name\"));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/segment-filters/to-list",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/segment-filters/to-list",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"criteria\": {\n    \"state\": \"open\"\n  },\n  \"new_list_name\": \"{{segment_filter_inline_list_name}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "description": "Congela criterios enviados en línea sin asociarlos a un filtro guardado."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: materialización desde criterios en línea\", function () {",
                  "  const m = j.segment_materialization;",
                  "  pm.expect(m.filter_id).to.eql(null);",
                  "  pm.expect(m.filter_version).to.eql(null);",
                  "  pm.expect(m.criteria).to.eql({state: [\"open\"]});",
                  "  pm.expect(m.matched).to.be.a(\"number\");",
                  "  pm.expect(m.added).to.be.a(\"number\");",
                  "  pm.expect(m.truncated).to.be.a(\"boolean\");",
                  "  pm.expect(j.debt_list.id).to.eql(m.list_id);",
                  "  pm.expect(j.debt_list.name).to.eql(pm.collectionVariables.get(\"segment_filter_inline_list_name\"));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/segment-filters/:id",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": "{{baseUrl}}/v1/segment-filters/{{segment_filter_id}}",
            "description": "Archiva el filtro creado por esta carpeta después de materializarlo."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 204\", function () { pm.response.to.have.status(204); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "47 · plantillas de email",
      "description": "API de plantillas de email (core#1925, ADR #224 D9): alta y versionado append-only, lista, detalle, edicion de borrador y las puertas de aprobar y retirar (solo account_admin; con la llave dd_ de la coleccion ambas son 403). La coleccion no tiene account_admin (la promocion es operator-only), asi que aqui no se aprueba: el camino feliz y el 403 de un member viven en test/db/email_templates_api_test.exs de core.",
      "item": [
        {
          "name": "POST /v1/email-templates (alta: draft v1)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/email-templates",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "email-templates"
              ]
            },
            "description": "Alta de una plantilla de email (core#1925, ADR #224 D9). Nace `draft` en la version 1; el asunto es texto estatico (sin variables) y el cuerpo lleva el aviso obligatorio. Sin DELETE: descartar un borrador es retirarlo, y retirar es de account_admin, que esta coleccion no tiene (la promocion es solo del operador), asi que la plantilla queda como borrador del tenant de la corrida.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"postman-emlt-{{$timestamp}}\",\n  \"subject\": \"Un mensaje importante para usted\",\n  \"body\": \"Hola {{nombre}}, tiene un mensaje pendiente. Esta comunicación es de un cobrador de deudas (debt collector). Para darse de baja (unsubscribe) responda BAJA.\",\n  \"variables\": [\n    {\n      \"name\": \"nombre\",\n      \"type\": \"string\",\n      \"required\": true\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: email_template en draft v1 sin aprobar\", function () {",
                  "  const t = j.email_template;",
                  "  pm.expect(t.id).to.be.a(\"string\");",
                  "  pm.expect(t.status).to.eql(\"draft\");",
                  "  pm.expect(t.version).to.eql(1);",
                  "  pm.expect(t.variables).to.be.an(\"array\");",
                  "  pm.expect(t.creditor_names).to.eql([]);",
                  "  pm.expect(t.approved_by).to.eql(null);",
                  "  pm.expect(t.approved_at).to.eql(null);",
                  "  pm.expect(t.retired_at).to.eql(null);",
                  "});",
                  "pm.collectionVariables.set(\"email_template_id\", j.email_template.id);",
                  "pm.collectionVariables.set(\"email_template_name\", j.email_template.name);"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/email-templates (mismo name -> version 2 en draft)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/email-templates",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "email-templates"
              ]
            },
            "description": "Un `name` existente abre la version siguiente como draft (append-only): la v1 no se toca.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"{{email_template_name}}\",\n  \"subject\": \"Un mensaje importante para usted\",\n  \"body\": \"Hola {{nombre}}, tiene un mensaje pendiente. Esta comunicación es de un cobrador de deudas (debt collector). Para darse de baja (unsubscribe) responda BAJA.\",\n  \"variables\": [\n    {\n      \"name\": \"nombre\",\n      \"type\": \"string\",\n      \"required\": true\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(201); });",
                  "const j = pm.response.json();",
                  "pm.test(\"version 2, draft y otro id\", function () {",
                  "  pm.expect(j.email_template.version).to.eql(2);",
                  "  pm.expect(j.email_template.status).to.eql(\"draft\");",
                  "  pm.expect(j.email_template.id).to.not.eql(pm.collectionVariables.get(\"email_template_id\"));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/email-templates (lista con ?name=)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/email-templates?name={{email_template_name}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "email-templates"
              ],
              "query": [
                {
                  "key": "name",
                  "value": "{{email_template_name}}"
                }
              ]
            },
            "description": "Lista las versiones del tenant, mas reciente primero por nombre. El filtro va como query en array (newman ignora el ?query del raw)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: email_templates[] con las dos versiones, v2 antes que v1\", function () {",
                  "  pm.expect(j.email_templates).to.be.an(\"array\");",
                  "  pm.expect(j.email_templates.map(function (t) { return t.version; })).to.eql([2, 1]);",
                  "  pm.expect(j.email_templates.map(function (t) { return t.id; })).to.include(pm.collectionVariables.get(\"email_template_id\"));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/email-templates (status fuera de catalogo -> 400)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/email-templates?status=bogus",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "email-templates"
              ],
              "query": [
                {
                  "key": "status",
                  "value": "bogus"
                }
              ]
            },
            "description": "Un `status` fuera de draft|approved|retired es 400, no una lista vacia."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(400); });",
                  "pm.test(\"sobre bad_request y no catch-all\", function () {",
                  "  const e = pm.response.json().error;",
                  "  pm.expect(e.code).to.eql(\"bad_request\");",
                  "  pm.expect(e.message).to.not.include(\"no such route\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/email-templates/{id}",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/email-templates/{{email_template_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "email-templates",
                "{{email_template_id}}"
              ]
            },
            "description": "Detalle de la version creada."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: el mismo id y nombre\", function () {",
                  "  pm.expect(j.email_template.id).to.eql(pm.collectionVariables.get(\"email_template_id\"));",
                  "  pm.expect(j.email_template.name).to.eql(pm.collectionVariables.get(\"email_template_name\"));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/email-templates/{id} (edita el borrador)",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/email-templates/{{email_template_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "email-templates",
                "{{email_template_id}}"
              ]
            },
            "description": "Un draft edita asunto, cuerpo, variables y acreedores; el nombre no se edita (otra version).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"subject\": \"Otro asunto neutro\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"asunto nuevo y sigue en draft\", function () {",
                  "  pm.expect(j.email_template.subject).to.eql(\"Otro asunto neutro\");",
                  "  pm.expect(j.email_template.status).to.eql(\"draft\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/email-templates/{id} (campo desconocido -> 400)",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/email-templates/{{email_template_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "email-templates",
                "{{email_template_id}}"
              ]
            },
            "description": "La lista de campos es cerrada: `status` no entra por el cuerpo (se cambia aprobando o retirando).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"status\": \"approved\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(400); });",
                  "pm.test(\"sobre bad_request\", function () { pm.expect(pm.response.json().error.code).to.eql(\"bad_request\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/email-templates/{id}/approve (llave dd_ -> 403)",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/email-templates/{{email_template_id}}/approve",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "email-templates",
                "{{email_template_id}}",
                "approve"
              ]
            },
            "description": "Aprobar es solo de un login account_admin: una llave dd_ no tiene usuario y lee 403 forbidden. (El 403 de un member y el camino feliz del admin se ejercen en la suite :db de core: la coleccion no tiene account_admin.)"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(403); });",
                  "pm.test(\"sobre forbidden\", function () { pm.expect(pm.response.json().error.code).to.eql(\"forbidden\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/email-templates/{id}/retire (llave dd_ -> 403)",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/email-templates/{{email_template_id}}/retire",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "email-templates",
                "{{email_template_id}}",
                "retire"
              ]
            },
            "description": "Retirar tambien es solo de account_admin (lo comprueba la ruta; el contexto no lo exige)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(403); });",
                  "pm.test(\"sobre forbidden\", function () { pm.expect(pm.response.json().error.code).to.eql(\"forbidden\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/email-templates/{id} (tras los 403 sigue draft)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/email-templates/{{email_template_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "email-templates",
                "{{email_template_id}}"
              ]
            },
            "description": "El 403 no movio la fila."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(200); });",
                  "pm.test(\"sigue en draft, sin aprobador\", function () {",
                  "  const t = pm.response.json().email_template;",
                  "  pm.expect(t.status).to.eql(\"draft\");",
                  "  pm.expect(t.approved_by).to.eql(null);",
                  "  pm.expect(t.retired_at).to.eql(null);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/email-templates/{ghost} (id desconocido -> 404)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/email-templates/{{ghost_uuid}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "email-templates",
                "{{ghost_uuid}}"
              ]
            },
            "description": "UUID bien formado que no existe: 404 not_found del endpoint real (no el `no such route` del catch-all)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(404); });",
                  "pm.test(\"sobre not_found y no catch-all\", function () {",
                  "  const e = pm.response.json().error;",
                  "  pm.expect(e.code).to.eql(\"not_found\");",
                  "  pm.expect(e.message).to.not.include(\"no such route\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/email-templates/{id} (id no UUID -> 404)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/email-templates/{{email_template_name}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "email-templates",
                "{{email_template_name}}"
              ]
            },
            "description": "Un id que no es UUID (aqui el nombre de la plantilla) es 404 not_found, como en /v1/sms-templates."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(404); });",
                  "pm.test(\"sobre not_found\", function () { pm.expect(pm.response.json().error.code).to.eql(\"not_found\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/email-templates (sin credenciales -> 401)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/email-templates",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "email-templates"
              ]
            },
            "description": "Sin bearer: 401.",
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 401\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(401); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "48 · qm y mis evaluaciones",
      "description": "Cobertura del contrato QM sin sembrar privilegios artificiales: las lecturas usan la llave del tenant o la sesion personal ligada que deja 32 · me-scorecard; las escrituras exclusivas de account_admin/evaluador prueban su negativa determinista. La carpeta enciende qm_enabled y siempre lo restaura al final.",
      "item": [
        {
          "name": "POST /v1/qm/forms — llave de maquina no suplanta account_admin",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/qm/forms",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Postman QM\",\n  \"pass_threshold\": 80,\n  \"criteria\": [{\"key\": \"saludo\", \"label\": \"Saluda\", \"kind\": \"yes_no\", \"weight\": 1}]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"shape: role_forbidden\", function () { const e = pm.response.json().error; pm.expect(e.code).to.eql(\"role_forbidden\"); pm.expect(e.message).to.be.a(\"string\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/qm/forms",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/qm/forms",
            "description": "Las lecturas siguen disponibles y devuelven la lista tipada aunque no haya formularios."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: forms[]\", function () { pm.expect(pm.response.json().forms).to.be.an(\"array\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/qm/forms/:id — id desconocido",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/qm/forms/{{ghost_uuid}}"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "pm.test(\"shape: not_found\", function () { const e = pm.response.json().error; pm.expect(e.code).to.eql(\"not_found\"); pm.expect(e.message).to.be.a(\"string\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/qm/forms/:id — llave de maquina no suplanta account_admin",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/qm/forms/{{ghost_uuid}}",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"pass_threshold\": 90,\n  \"base_version\": 1\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"shape: role_forbidden\", function () { const e = pm.response.json().error; pm.expect(e.code).to.eql(\"role_forbidden\"); pm.expect(e.message).to.be.a(\"string\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/qm/forms/:id — llave de maquina no suplanta account_admin",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": "{{baseUrl}}/v1/qm/forms/{{ghost_uuid}}"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"shape: role_forbidden\", function () { const e = pm.response.json().error; pm.expect(e.code).to.eql(\"role_forbidden\"); pm.expect(e.message).to.be.a(\"string\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/qm/forms/:id/versions — id desconocido",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/qm/forms/{{ghost_uuid}}/versions"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "pm.test(\"shape: not_found\", function () { pm.expect(pm.response.json().error.code).to.eql(\"not_found\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/qm/forms/:id/versions/:version — id desconocido",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/qm/forms/{{ghost_uuid}}/versions/{{qm_form_version}}"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "pm.test(\"shape: not_found\", function () { pm.expect(pm.response.json().error.code).to.eql(\"not_found\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/qm/reports/agents",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/qm/reports/agents?from={{qm_report_from}}&to={{qm_report_to}}"
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const to = new Date();",
                  "const from = new Date(to.getTime() - 60 * 60 * 1000);",
                  "pm.collectionVariables.set(\"qm_report_from\", from.toISOString());",
                  "pm.collectionVariables.set(\"qm_report_to\", to.toISOString());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: ventana y rows[] por agente\", function () { pm.expect(j.from).to.be.a(\"string\"); pm.expect(j.to).to.be.a(\"string\"); pm.expect(j.rows).to.be.an(\"array\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/qm/reports/campaigns",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/qm/reports/campaigns?from={{qm_report_from}}&to={{qm_report_to}}"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: ventana y rows[] por campana\", function () { pm.expect(j.from).to.be.a(\"string\"); pm.expect(j.to).to.be.a(\"string\"); pm.expect(j.rows).to.be.an(\"array\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/qm/evaluations/:id/dispute/resolution — llave de maquina no es evaluador",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/qm/evaluations/{{ghost_uuid}}/dispute/resolution",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"decision\": \"upheld\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"shape: role_forbidden\", function () { const e = pm.response.json().error; pm.expect(e.code).to.eql(\"role_forbidden\"); pm.expect(e.message).to.be.a(\"string\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/me/evaluations",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/me/evaluations",
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{scorecard_ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: evaluaciones propias[]\", function () { pm.expect(pm.response.json().evaluations).to.be.an(\"array\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/me/evaluations/:id — evaluacion propia desconocida",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/me/evaluations/{{ghost_uuid}}",
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{scorecard_ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "pm.test(\"shape: not_found\", function () { pm.expect(pm.response.json().error.code).to.eql(\"not_found\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/me/evaluations/:id/dispute — evaluacion propia desconocida",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/me/evaluations/{{ghost_uuid}}/dispute",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"reason\": \"wrong_call\",\n  \"note\": \"Control Postman\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{scorecard_ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "pm.test(\"shape: not_found\", function () { pm.expect(pm.response.json().error.code).to.eql(\"not_found\"); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "49 · operaciones de producto pendientes",
      "description": "Cierra las operaciones de sms-templates, bandeja SMS, administracion click-to-call, evaluaciones de grabaciones y conferencia. Los ids que no nacen del flujo usan un UUID bien formado y cada 404 distingue la ruta real del catch-all. Las escrituras con rol humano prueban el 403 de la llave dd_ de la coleccion; los flags temporales siempre se restauran.",
      "item": [
        {
          "name": "POST /v1/calls/:call_id/conference/add — llamada desconocida",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/calls/{{ghost_uuid}}/conference/add",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"destination\": \"1000\",\n  \"consumer_agreed\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "pm.test(\"not_found de conferencia, no catch-all\", function () { const e = pm.response.json().error; pm.expect(e.code).to.eql(\"not_found\"); pm.expect(e.message).to.not.eql(\"no such route\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/calls/:call_id/conference/remove — llamada desconocida",
          "request": {
            "method": "POST",
            "header": [],
            "url": "{{baseUrl}}/v1/calls/{{ghost_uuid}}/conference/remove"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "pm.test(\"not_found de conferencia, no catch-all\", function () { const e = pm.response.json().error; pm.expect(e.code).to.eql(\"not_found\"); pm.expect(e.message).to.not.eql(\"no such route\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/click-to-call/sites",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/click-to-call/sites"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: sites[] y sitio sembrado sin llave\", function () { const site = j.sites.find(function (x) { return x.id === pm.collectionVariables.get(\"click_to_call_site_id\"); }); pm.expect(j.sites).to.be.an(\"array\"); pm.expect(site).to.be.an(\"object\"); pm.expect(site).to.not.have.property(\"token\"); pm.expect(site).to.not.have.property(\"token_hash\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/click-to-call/sites/:id",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/click-to-call/sites/{{click_to_call_site_id}}"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: site del tenant y consentimiento vigente\", function () { const s = pm.response.json().site; pm.expect(s.id).to.eql(pm.collectionVariables.get(\"click_to_call_site_id\")); pm.expect(s.consent_version).to.be.a(\"string\"); pm.expect(s.consent_sha256).to.match(/^[0-9a-f]{64}$/); pm.expect(s).to.not.have.property(\"token_hash\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/click-to-call/sites/:id",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/click-to-call/sites/{{click_to_call_site_id}}",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"label\": \"postman-uniform-editado\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"edita label sin exponer ni rotar la llave\", function () { const s = pm.response.json().site; pm.expect(s.id).to.eql(pm.collectionVariables.get(\"click_to_call_site_id\")); pm.expect(s.label).to.eql(\"postman-uniform-editado\"); pm.expect(s.consent_version).to.eql(pm.collectionVariables.get(\"click_to_call_consent_version\")); pm.expect(s).to.not.have.property(\"token\"); pm.expect(s).to.not.have.property(\"token_hash\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/click-to-call/sites/:id",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": "{{baseUrl}}/v1/click-to-call/sites/{{click_to_call_site_id}}"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"revoca el sitio y no devuelve secretos\", function () { const s = pm.response.json().site; pm.expect(s.id).to.eql(pm.collectionVariables.get(\"click_to_call_site_id\")); pm.expect(s.revoked_at).to.be.a(\"string\"); pm.expect(s).to.not.have.property(\"token\"); pm.expect(s).to.not.have.property(\"token_hash\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/recordings/:id/evaluations — llave de maquina no es evaluador",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/recordings/{{ghost_uuid}}/evaluations",
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"shape: role_forbidden antes de revelar la grabacion\", function () { const e = pm.response.json().error; pm.expect(e.code).to.eql(\"role_forbidden\"); pm.expect(e.message).to.be.a(\"string\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/recordings/:id/evaluations — grabacion desconocida",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/recordings/{{ghost_uuid}}/evaluations"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "pm.test(\"not_found del endpoint real\", function () { const e = pm.response.json().error; pm.expect(e.code).to.eql(\"not_found\"); pm.expect(e.message).to.not.eql(\"no such route\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/recordings/:id/evaluations/:evaluation_id — ids desconocidos",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/recordings/{{ghost_uuid}}/evaluations/{{ghost_uuid}}"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "pm.test(\"not_found del endpoint real\", function () { const e = pm.response.json().error; pm.expect(e.code).to.eql(\"not_found\"); pm.expect(e.message).to.not.eql(\"no such route\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/sms-templates — crear draft",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/sms-templates",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"postman-sms-template-{{$timestamp}}\",\n  \"body\": \"Hola {{nombre}}, tiene un pago pendiente. Responda STOP para no recibir mas mensajes.\",\n  \"variables\": [{\"name\": \"nombre\", \"type\": \"string\"}]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const t = pm.response.json().sms_template;",
                  "pm.test(\"shape: draft v1 sin aprobador\", function () { pm.expect(t.id).to.be.a(\"string\"); pm.expect(t.status).to.eql(\"draft\"); pm.expect(t.version).to.eql(1); pm.expect(t.variables).to.be.an(\"array\"); pm.expect(t.approved_by).to.eql(null); });",
                  "pm.collectionVariables.set(\"sms_template_id\", t.id);",
                  "pm.collectionVariables.set(\"sms_template_name\", t.name);"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/sms-templates",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/sms-templates"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: sms_templates[] contiene el draft\", function () { pm.expect(j.sms_templates).to.be.an(\"array\"); pm.expect(j.sms_templates.map(function (t) { return t.id; })).to.include(pm.collectionVariables.get(\"sms_template_id\")); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/sms-templates/:id",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/sms-templates/{{sms_template_id}}"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: version creada\", function () { const t = pm.response.json().sms_template; pm.expect(t.id).to.eql(pm.collectionVariables.get(\"sms_template_id\")); pm.expect(t.name).to.eql(pm.collectionVariables.get(\"sms_template_name\")); pm.expect(t.status).to.eql(\"draft\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/sms-templates/:id",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/sms-templates/{{sms_template_id}}",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"body\": \"Hola {{nombre}}, su pago vence hoy. Responda STOP para no recibir mas mensajes.\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"edita el draft sin versionarlo\", function () { const t = pm.response.json().sms_template; pm.expect(t.id).to.eql(pm.collectionVariables.get(\"sms_template_id\")); pm.expect(t.status).to.eql(\"draft\"); pm.expect(t.version).to.eql(1); pm.expect(t.body).to.include(\"vence hoy\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/sms-templates/:id/approve — llave dd_ no es account_admin",
          "request": {
            "method": "POST",
            "header": [],
            "url": "{{baseUrl}}/v1/sms-templates/{{sms_template_id}}/approve"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"shape: forbidden\", function () { const e = pm.response.json().error; pm.expect(e.code).to.eql(\"forbidden\"); pm.expect(e.message).to.be.a(\"string\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/sms-templates/:id/retire",
          "request": {
            "method": "POST",
            "header": [],
            "url": "{{baseUrl}}/v1/sms-templates/{{sms_template_id}}/retire"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"retira el draft de forma terminal\", function () { const t = pm.response.json().sms_template; pm.expect(t.id).to.eql(pm.collectionVariables.get(\"sms_template_id\")); pm.expect(t.status).to.eql(\"retired\"); pm.expect(t.retired_at).to.be.a(\"string\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/sms/template-policy",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/sms/template-policy"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: templates_only booleano\", function () { pm.expect(pm.response.json().sms_template_policy.templates_only).to.be.a(\"boolean\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/sms/template-policy — llave dd_ no es account_admin",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/sms/template-policy",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"templates_only\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"shape: forbidden\", function () { const e = pm.response.json().error; pm.expect(e.code).to.eql(\"forbidden\"); pm.expect(e.message).to.be.a(\"string\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/sms/threads",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/sms/threads"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: sms_threads[] + next_cursor\", function () { const j = pm.response.json(); pm.expect(j.sms_threads).to.be.an(\"array\"); pm.expect(j).to.have.property(\"next_cursor\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/sms/threads/:id — hilo desconocido",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/sms/threads/{{ghost_uuid}}"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "pm.test(\"not_found del endpoint real\", function () { const e = pm.response.json().error; pm.expect(e.code).to.eql(\"not_found\"); pm.expect(e.message).to.not.eql(\"no such route\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/sms/threads/:id/assign — llave dd_ no tiene persona",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/sms/threads/{{ghost_uuid}}/assign",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"user_id\": null\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"shape: role_forbidden sin evento\", function () { const e = pm.response.json().error; pm.expect(e.code).to.eql(\"role_forbidden\"); pm.expect(e.message).to.be.a(\"string\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/sms/threads/:id/read — llave dd_ no tiene persona",
          "request": {
            "method": "POST",
            "header": [],
            "url": "{{baseUrl}}/v1/sms/threads/{{ghost_uuid}}/read"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"shape: role_forbidden sin evento\", function () { const e = pm.response.json().error; pm.expect(e.code).to.eql(\"role_forbidden\"); pm.expect(e.message).to.be.a(\"string\"); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "50 · inbound-vip-lists",
      "description": "Listas de deudas designadas como VIP para la cola entrante. El flujo reutiliza la lista creada por la carpeta debt-lists y restaura el conjunto vacío al terminar.",
      "item": [
        {
          "name": "PUT /v1/inbound-vip-lists",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/inbound-vip-lists",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"list_ids\": [\"{{debt_list_id}}\", \"{{debt_list_id}}\"]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "description": "Reemplaza el conjunto con la lista creada por esta corrida. El id repetido prueba la deduplicación prometida por el contrato."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: una lista VIP designada\", function () {",
                  "  pm.expect(j.vip_lists).to.be.an(\"array\").and.to.have.length(1);",
                  "  const v = j.vip_lists[0];",
                  "  pm.expect(v.list_id).to.eql(pm.collectionVariables.get(\"debt_list_id\"));",
                  "  pm.expect(v.name).to.eql(pm.collectionVariables.get(\"debt_list_name\"));",
                  "  pm.expect(v.designated_at).to.be.a(\"string\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/inbound-vip-lists",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/inbound-vip-lists"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: lectura conserva la lista designada\", function () {",
                  "  pm.expect(j.vip_lists).to.be.an(\"array\").and.to.have.length(1);",
                  "  pm.expect(j.vip_lists[0].list_id).to.eql(pm.collectionVariables.get(\"debt_list_id\"));",
                  "  pm.expect(j.vip_lists[0].name).to.eql(pm.collectionVariables.get(\"debt_list_name\"));",
                  "  pm.expect(j.vip_lists[0].designated_at).to.be.a(\"string\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/inbound-vip-lists — sin bearer",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/inbound-vip-lists",
            "auth": {
              "type": "noauth"
            },
            "description": "Control negativo: sin bearer no se revela la configuración VIP del tenant."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 401\", function () { pm.response.to.have.status(401); });",
                  "pm.test(\"shape: unauthorized sin listas\", function () {",
                  "  const j = pm.response.json();",
                  "  pm.expect(j.error.code).to.eql(\"unauthorized\");",
                  "  pm.expect(j).to.not.have.property(\"vip_lists\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/inbound-vip-lists — restaurar conjunto vacío",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/inbound-vip-lists",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"list_ids\": []\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "description": "Limpieza de la configuración mutable compartida por la colección."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"conjunto VIP restaurado vacío\", function () { pm.expect(pm.response.json().vip_lists).to.eql([]); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "51 · support-access",
      "description": "Prueba el banner con una sesión humana member y las negativas account_admin de concesión, rastro y revocación. No fabrica privilegios: reutiliza la sesión personal creada por 32 · me-scorecard.",
      "item": [
        {
          "name": "GET /v1/support-access/active",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/support-access/active",
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{scorecard_ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: banner inactivo\", function () { pm.expect(j.active).to.eql(false); pm.expect(j.expires_at).to.eql(null); pm.expect(j.operator_name).to.eql(null); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/support-access/active — sin bearer",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/support-access/active",
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 401\", function () { pm.response.to.have.status(401); });",
                  "pm.test(\"shape: unauthorized\", function () { const e = pm.response.json().error; pm.expect(e.code).to.eql(\"unauthorized\"); pm.expect(e.message).to.be.a(\"string\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/support-access/grants — member no es account_admin",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/support-access/grants",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"reason\": \"Validación Postman de acceso de soporte\",\n  \"duration_minutes\": 15\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{scorecard_ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"shape: role_forbidden sin conceder acceso\", function () { const j = pm.response.json(); pm.expect(j.error.code).to.eql(\"role_forbidden\"); pm.expect(j.error.message).to.be.a(\"string\"); pm.expect(j).to.not.have.property(\"grant\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/support-access/events — member no es account_admin",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/support-access/events",
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{scorecard_ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"shape: role_forbidden sin filtrar el rastro\", function () { const j = pm.response.json(); pm.expect(j.error.code).to.eql(\"role_forbidden\"); pm.expect(j.error.message).to.be.a(\"string\"); pm.expect(j).to.not.have.property(\"events\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/support-access/grants/:id — member no es account_admin",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": "{{baseUrl}}/v1/support-access/grants/{{ghost_uuid}}",
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{scorecard_ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"shape: role_forbidden antes de revelar la concesión\", function () { const j = pm.response.json(); pm.expect(j.error.code).to.eql(\"role_forbidden\"); pm.expect(j.error.message).to.be.a(\"string\"); pm.expect(j).to.not.have.property(\"grant\"); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "52 · tenant login y SMS",
      "description": "Cubre la política de login y la configuración SMS del tenant. La llave dd_ lee la política pero no puede escribirla; para SMS ejerce las escrituras de administración y restaura business-hours al estado vacío. La sesión member prueba los rechazos de rol sin fabricar privilegios.",
      "item": [
        {
          "name": "GET /v1/tenant/login-policy",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/tenant/login-policy"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: política nullable y medición explícita\", function () { pm.expect(j).to.have.property(\"login_policy\"); pm.expect(j.client_address_measured).to.be.a(\"boolean\"); if (j.login_policy !== null) { pm.expect(j.login_policy.version).to.be.a(\"number\"); pm.expect(j.login_policy.mode).to.be.oneOf([\"off\", \"observe\", \"enforce\"]); pm.expect(j.login_policy.ip_allow).to.be.an(\"array\"); pm.expect(j.login_policy.windows).to.be.an(\"object\"); pm.expect(j.login_policy.inserted_at).to.be.a(\"string\"); } });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/tenant/login-policy/history",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/tenant/login-policy/history?limit=2"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: historial paginado\", function () { pm.expect(j.versions).to.be.an(\"array\"); pm.expect(j.versions.length).to.be.at.most(2); pm.expect(j.next_cursor === null || Number.isInteger(j.next_cursor)).to.eql(true); if (j.versions.length) { pm.expect(j.versions[0].version).to.be.a(\"number\"); pm.expect(j.versions[0].set_by).to.be.a(\"string\"); } });"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/tenant/login-policy — llave de máquina no suplanta account_admin",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/tenant/login-policy",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"mode\": \"off\",\n  \"reason\": \"Control Postman\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"shape: role_forbidden sin publicar versión\", function () { const j = pm.response.json(); pm.expect(j.error.code).to.eql(\"role_forbidden\"); pm.expect(j.error.message).to.be.a(\"string\"); pm.expect(j).to.not.have.property(\"login_policy\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/tenant/login-policy — member no es lector administrativo",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/tenant/login-policy",
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{scorecard_ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"shape: role_forbidden sin revelar la política\", function () { const j = pm.response.json(); pm.expect(j.error.code).to.eql(\"role_forbidden\"); pm.expect(j).to.not.have.property(\"login_policy\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/tenant/sms-business-hours — estado inicial",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/tenant/sms-business-hours"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: sin horario SMS\", function () { pm.expect(pm.response.json()).to.have.property(\"sms_business_hours\", null); });"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/tenant/sms-business-hours",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/tenant/sms-business-hours",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"timezone\": \"America/Chicago\",\n  \"weekly\": {\n    \"mon\": {\"start\": \"09:00\", \"end\": \"17:00\"},\n    \"tue\": {\"start\": \"10:00\", \"end\": \"18:00\"}\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: horario SMS normalizado\", function () { const h = pm.response.json().sms_business_hours; pm.expect(h.timezone).to.eql(\"America/Chicago\"); pm.expect(h.weekly.mon).to.eql({start: \"09:00:00\", end: \"17:00:00\"}); pm.expect(h.weekly.tue).to.eql({start: \"10:00:00\", end: \"18:00:00\"}); });"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/tenant/sms-business-hours — member no es administrador",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/tenant/sms-business-hours",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"timezone\": \"America/New_York\",\n  \"weekly\": {\n    \"fri\": {\"start\": \"08:00\", \"end\": \"12:00\"}\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{scorecard_ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"shape: role_forbidden sin reemplazar horario\", function () { const j = pm.response.json(); pm.expect(j.error.code).to.eql(\"role_forbidden\"); pm.expect(j).to.not.have.property(\"sms_business_hours\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/tenant/sms-business-hours — conserva escritura autorizada",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/tenant/sms-business-hours"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"el rechazo member no cambió el horario\", function () { const h = pm.response.json().sms_business_hours; pm.expect(h.timezone).to.eql(\"America/Chicago\"); pm.expect(h.weekly.mon).to.eql({start: \"09:00:00\", end: \"17:00:00\"}); pm.expect(h.weekly).to.not.have.property(\"fri\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/tenant/sms-business-hours — member no es administrador",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": "{{baseUrl}}/v1/tenant/sms-business-hours",
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{scorecard_ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"shape: role_forbidden sin borrar horario\", function () { const j = pm.response.json(); pm.expect(j.error.code).to.eql(\"role_forbidden\"); pm.expect(j).to.not.have.property(\"sms_business_hours\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/tenant/sms-business-hours",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": "{{baseUrl}}/v1/tenant/sms-business-hours"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: horario SMS eliminado\", function () { pm.expect(pm.response.json()).to.have.property(\"sms_business_hours\", null); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/tenant/sms-business-hours — tras DELETE",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/tenant/sms-business-hours"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"estado compartido restaurado\", function () { pm.expect(pm.response.json()).to.have.property(\"sms_business_hours\", null); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/tenant/sms-auto-reply — estado inicial",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/tenant/sms-auto-reply"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: sin respuesta automática guardada\", function () { pm.expect(pm.response.json()).to.have.property(\"sms_auto_reply\", null); });"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/tenant/sms-auto-reply",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/tenant/sms-auto-reply",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"body\": \"Estamos fuera de horario. Responda STOP para dejar de recibir mensajes.\",\n  \"language\": \"es-MX\",\n  \"cooldown_seconds\": 3600\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: versión guardada por API key\", function () { const r = pm.response.json().sms_auto_reply; pm.expect(r.body).to.include(\"STOP\"); pm.expect(r.language).to.eql(\"es-MX\"); pm.expect(r.cooldown_seconds).to.eql(3600); pm.expect(r.set_by).to.eql(null); pm.expect(r.updated_at).to.be.a(\"string\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/tenant/sms-auto-reply — member no es administrador",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/tenant/sms-auto-reply",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"body\": \"Texto que no debe guardarse. Responda STOP.\",\n  \"language\": \"es-MX\",\n  \"cooldown_seconds\": 7200\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{scorecard_ddu_token}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"shape: role_forbidden sin guardar versión\", function () { const j = pm.response.json(); pm.expect(j.error.code).to.eql(\"role_forbidden\"); pm.expect(j).to.not.have.property(\"sms_auto_reply\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/tenant/sms-auto-reply",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/tenant/sms-auto-reply"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"el rechazo member no cambió la versión\", function () { const r = pm.response.json().sms_auto_reply; pm.expect(r.body).to.eql(\"Estamos fuera de horario. Responda STOP para dejar de recibir mensajes.\"); pm.expect(r.language).to.eql(\"es-MX\"); pm.expect(r.cooldown_seconds).to.eql(3600); pm.expect(r.set_by).to.eql(null); pm.expect(r.updated_at).to.be.a(\"string\"); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "53 · insights",
      "description": "Evidencia de anomalías del evaluador insights (core#1983): series, umbrales y acuse por usuario.",
      "item": [
        {
          "name": "GET /v1/insights/anomalies",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/insights/anomalies",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "insights",
                "anomalies"
              ]
            },
            "description": "Anomalías detectadas por el evaluador (ordenadas por más recientes)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: lista de anomalías y next_cursor\", function () {",
                  "  pm.expect(j).to.have.property(\"anomalies\");",
                  "  pm.expect(j.anomalies).to.be.an(\"array\");",
                  "  pm.expect(j).to.have.property(\"next_cursor\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/insights/series",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/insights/series?metric=contact_rate",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "insights",
                "series"
              ],
              "query": [
                {
                  "key": "metric",
                  "value": "contact_rate"
                }
              ]
            },
            "description": "Serie temporal de una métrica en cubos de 1h con la banda del evaluador zscore."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: serie de puntos por hora\", function () {",
                  "  pm.expect(j).to.have.property(\"metric\", \"contact_rate\");",
                  "  pm.expect(j).to.have.property(\"buckets\");",
                  "  pm.expect(j.buckets).to.be.an(\"array\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/insights/anomalies/:id/ack",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/insights/anomalies/{{ghost_uuid}}/ack",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "insights",
                "anomalies",
                "{{ghost_uuid}}",
                "ack"
              ]
            },
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{scorecard_ddu_token}}",
                  "type": "string"
                }
              ]
            },
            "description": "Acuse de recibo de una anomalía por parte del usuario (requiere sesión ddu_). Con ghost_uuid o id inexistente retorna 404 por diseño."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404 para id inexistente\", function () { pm.response.to.have.status(404); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: error anomaly_not_found o not_found\", function () {",
                  "  pm.expect(j).to.have.property(\"error\");",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "54 · wfm-coverage",
      "description": "Cobertura WFM por intervalo (core#2020, ADR #241 de core). Ruta de lectura de tenant scope; el objetivo de servicio del tenant nace ausente (inerte), así que el request base acepta inerte o configurado y la simulación (`threshold_ms` + `target_percent`) es la que ejerce Erlang C de punta a punta.",
      "item": [
        {
          "name": "GET /v1/wfm/coverage?date",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/wfm/coverage?date={{wfm_date}}",
            "description": "Cobertura por intervalo del día (core#2020, ADR #241 de core): pronóstico, agentes requeridos por Erlang C, planificados R11 y brecha. El objetivo de servicio del tenant nace ausente: sin él `target` y cada `staffing` son `{inert: razón}` y `coverage` es null; con él, `target` lleva threshold_ms/target_percent."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Mañana en UTC: el día se pide por fecha ISO y el motor responde los 48 intervalos de 30 min.",
                  "pm.collectionVariables.set(\"wfm_date\", new Date(Date.now() + 24 * 3600 * 1000).toISOString().slice(0, 10));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: 48 intervalos de 30 min, sin simulación\", function () { pm.expect(j.date).to.eql(pm.collectionVariables.get(\"wfm_date\")); pm.expect(j.interval_minutes).to.eql(30); pm.expect(j.simulated).to.eql(false); pm.expect(j.intervals).to.be.an(\"array\").with.lengthOf(48); });",
                  "pm.test(\"shape: objetivo inerte o configurado, nunca inventado\", function () { const t = j.target; pm.expect(t.inert !== undefined || (t.threshold_ms > 0 && t.target_percent >= 1 && t.target_percent <= 100 && t.simulated === false)).to.be.true; });",
                  "pm.test(\"shape: cada intervalo trae forecast, staffing y planificados\", function () { j.intervals.forEach(function (i) { pm.expect(i.forecast.volume).to.be.a(\"number\"); pm.expect(i.forecast.aht_ms).to.be.a(\"number\"); pm.expect(i.forecast.aht_source).to.be.a(\"string\"); pm.expect(i.staffing).to.be.an(\"object\"); pm.expect(i.scheduled_agents).to.be.a(\"number\"); }); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/wfm/coverage?date&threshold_ms&target_percent (simulación)",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/wfm/coverage?date={{wfm_date}}&threshold_ms=20000&target_percent=80",
            "description": "Simula un objetivo 80/20 sólo en esta respuesta (no se persiste): `simulated: true` y cada intervalo trae `required_agents` por Erlang C."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Mañana en UTC: el día se pide por fecha ISO y el motor responde los 48 intervalos de 30 min.",
                  "pm.collectionVariables.set(\"wfm_date\", new Date(Date.now() + 24 * 3600 * 1000).toISOString().slice(0, 10));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const j = pm.response.json();",
                  "pm.test(\"shape: simulación declarada\", function () { pm.expect(j.simulated).to.eql(true); pm.expect(j.target).to.eql({threshold_ms: 20000, target_percent: 80, simulated: true}); });",
                  "pm.test(\"shape: cada intervalo pide agentes enteros >= 0\", function () { pm.expect(j.intervals).to.have.lengthOf(48); j.intervals.forEach(function (i) { pm.expect(Number.isInteger(i.staffing.required_agents)).to.be.true; pm.expect(i.staffing.required_agents).to.be.at.least(0); pm.expect(i.staffing.reachable).to.be.a(\"boolean\"); }); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/wfm/coverage (sin date -> 400)",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/wfm/coverage",
            "description": "El día es obligatorio: sin `date` responde 400."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "pm.test(\"shape: pide date\", function () { pm.expect(pm.response.json().error.message).to.eql(\"date is required\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/wfm/coverage?threshold_ms (sin target_percent -> 400)",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/wfm/coverage?date={{wfm_date}}&threshold_ms=20000",
            "description": "La simulación pide los dos parámetros juntos o ninguno."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Mañana en UTC: el día se pide por fecha ISO y el motor responde los 48 intervalos de 30 min.",
                  "pm.collectionVariables.set(\"wfm_date\", new Date(Date.now() + 24 * 3600 * 1000).toISOString().slice(0, 10));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "pm.test(\"shape: ambos o ninguno\", function () { pm.expect(pm.response.json().error.message).to.include(\"must be given together\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/wfm/coverage?threshold_ms (umbral sobre un día -> 400)",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/wfm/coverage?date={{wfm_date}}&threshold_ms=86400001&target_percent=80",
            "description": "El umbral simulado se acota a 86 400 000 ms (un día): por encima responde 400, no un 500."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Mañana en UTC: el día se pide por fecha ISO y el motor responde los 48 intervalos de 30 min.",
                  "pm.collectionVariables.set(\"wfm_date\", new Date(Date.now() + 24 * 3600 * 1000).toISOString().slice(0, 10));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "pm.test(\"shape: cota del umbral\", function () { pm.expect(pm.response.json().error.message).to.eql(\"threshold_ms must be at most 86400000\"); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "55 · experiments",
      "description": "A/B de campañas (core#1976, ADR #242 de core; hija E de core#1961, integration-tests#788): las ocho operaciones `/campaigns/:id/experiments`. Reusa la campaña `{{golden_campaign_id}}` de `03 · golden-flow`; crea un borrador, lo edita, lo arranca, lo termina y comprueba los 409 de lo congelado y los 422 de lo legal. Incluye `…/results` (core#1978).",
      "item": [
        {
          "name": "POST /v1/campaigns/:id/experiments (borrador de 2 variantes)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/campaigns/{{golden_campaign_id}}/experiments",
            "description": "Crea un experimento `draft` (core#1976, ADR #242 de core): 2 a 5 variantes con exactamente una de control. `min_gap_seconds` es un override de la lista cerrada que solo endurece la campaña.",
            "body": {
              "mode": "raw",
              "raw": "{\"name\": \"postman A/B\", \"primary_metric\": \"contact_rate\", \"variants\": [{\"name\": \"control\", \"weight\": 1, \"is_control\": true, \"overrides\": {}}, {\"name\": \"desafiante\", \"weight\": 1, \"is_control\": false, \"overrides\": {\"min_gap_seconds\": 7200}}]}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const e = pm.response.json().experiment;",
                  "pm.test(\"shape: borrador con 2 variantes, una de control, en orden de posición\", function () { pm.expect(e.state).to.eql(\"draft\"); pm.expect(e.primary_metric).to.eql(\"contact_rate\"); pm.expect(e.variants).to.have.lengthOf(2); pm.expect(e.variants.map(function (v) { return v.position; })).to.eql([0, 1]); pm.expect(e.variants.filter(function (v) { return v.is_control; })).to.have.lengthOf(1); });",
                  "pm.collectionVariables.set(\"experiment_id\", e.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/campaigns/:id/experiments — override fuera de la lista cerrada -> 422",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/campaigns/{{golden_campaign_id}}/experiments",
            "description": "`optout_digit`, `abandonment_threshold` y `max_dial_ratio` no se pueden variar: un A/B jamás toca lo legal.",
            "body": {
              "mode": "raw",
              "raw": "{\"name\": \"rechazado\", \"primary_metric\": \"contact_rate\", \"variants\": [{\"name\": \"control\", \"weight\": 1, \"is_control\": true, \"overrides\": {}}, {\"name\": \"x\", \"weight\": 1, \"is_control\": false, \"overrides\": {\"optout_digit\": \"5\"}}]}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 422\", function () { pm.response.to.have.status(422); });",
                  "pm.test(\"error con la ruta rechazada en details\", function () { const j = pm.response.json(); pm.expect(j.error).to.be.an(\"object\"); pm.expect(JSON.stringify(j.error.details)).to.include(\"optout_digit\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/campaigns/:id/experiments",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/campaigns/{{golden_campaign_id}}/experiments",
            "description": ""
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: lista con el experimento creado\", function () { const j = pm.response.json(); pm.expect(j.experiments).to.be.an(\"array\"); pm.expect(j.experiments.map(function (e) { return e.id; })).to.include(pm.collectionVariables.get(\"experiment_id\")); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/campaigns/:id/experiments/:experiment_id",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/campaigns/{{golden_campaign_id}}/experiments/{{experiment_id}}",
            "description": ""
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: el experimento con sus variantes\", function () { const e = pm.response.json().experiment; pm.expect(e.id).to.eql(pm.collectionVariables.get(\"experiment_id\")); pm.expect(e.variants).to.have.lengthOf(2); });"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/campaigns/:id/experiments/:experiment_id (borrador)",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/campaigns/{{golden_campaign_id}}/experiments/{{experiment_id}}",
            "description": "",
            "body": {
              "mode": "raw",
              "raw": "{\"name\": \"postman A/B editado\"}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: nombre editado, sigue en borrador\", function () { const e = pm.response.json().experiment; pm.expect(e.name).to.eql(\"postman A/B editado\"); pm.expect(e.state).to.eql(\"draft\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/campaigns/:id/experiments/:experiment_id/start",
          "request": {
            "method": "POST",
            "header": [],
            "url": "{{baseUrl}}/v1/campaigns/{{golden_campaign_id}}/experiments/{{experiment_id}}/start",
            "description": ""
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: running con started_at\", function () { const e = pm.response.json().experiment; pm.expect(e.state).to.eql(\"running\"); pm.expect(e.started_at).to.be.a(\"string\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/campaigns/:id/experiments/:experiment_id/results",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/campaigns/{{golden_campaign_id}}/experiments/{{experiment_id}}/results",
            "description": "Resultados y veredicto (core#1978): se recalculan del libro en cada lectura. Con el experimento recién arrancado y sin llamadas atribuidas, cada variante trae ceros y tasas nulas, y el veredicto es `insufficient_data` (suelo de 100 intentos colocados y 10 éxitos por brazo). Los tres veredictos con llamadas reales los ejerce el tier `e2e-ab`."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const r = pm.response.json().results;",
                  "pm.test(\"shape: experimento, una fila por variante y veredicto honesto\", function () { pm.expect(r.experiment.id).to.eql(pm.collectionVariables.get(\"experiment_id\")); pm.expect(r.variants).to.have.lengthOf(2); pm.expect(r.unattributed_attempts).to.eql(0); pm.expect(r.verdict.status).to.be.oneOf([\"insufficient_data\", \"no_significant_difference\", \"winner\"]); pm.expect(r.allocation_warning).to.be.a(\"boolean\"); });",
                  "pm.test(\"sin llamadas no hay ganador: insufficient_data\", function () { pm.expect(r.verdict.status).to.eql(\"insufficient_data\"); pm.expect(r.verdict.winner).to.eql(null); r.variants.forEach(function (v) { pm.expect(v.attempts).to.eql(0); pm.expect(v.contact_rate).to.eql(null); }); });"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/campaigns/:id/experiments/:experiment_id (en marcha -> 409)",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/campaigns/{{golden_campaign_id}}/experiments/{{experiment_id}}",
            "description": "Con el experimento en marcha sus variantes y su nombre quedan congelados.",
            "body": {
              "mode": "raw",
              "raw": "{\"name\": \"no se puede\"}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 409\", function () { pm.response.to.have.status(409); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/campaigns/:id/experiments/:experiment_id/stop",
          "request": {
            "method": "POST",
            "header": [],
            "url": "{{baseUrl}}/v1/campaigns/{{golden_campaign_id}}/experiments/{{experiment_id}}/stop",
            "description": ""
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: ended con ended_at\", function () { const e = pm.response.json().experiment; pm.expect(e.state).to.eql(\"ended\"); pm.expect(e.ended_at).to.be.a(\"string\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/campaigns/:id/experiments/:experiment_id (terminado -> 409)",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": "{{baseUrl}}/v1/campaigns/{{golden_campaign_id}}/experiments/{{experiment_id}}",
            "description": "Solo un borrador se borra."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 409\", function () { pm.response.to.have.status(409); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/campaigns/:id/experiments (segundo borrador, para borrar)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/campaigns/{{golden_campaign_id}}/experiments",
            "description": "",
            "body": {
              "mode": "raw",
              "raw": "{\"name\": \"postman A/B a borrar\", \"primary_metric\": \"promise_rate\", \"variants\": [{\"name\": \"control\", \"weight\": 1, \"is_control\": true, \"overrides\": {}}, {\"name\": \"desafiante\", \"weight\": 1, \"is_control\": false, \"overrides\": {\"min_gap_seconds\": 3600}}]}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "pm.collectionVariables.set(\"experiment_draft_id\", pm.response.json().experiment.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/campaigns/:id/experiments/:experiment_id (borrador -> 204)",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": "{{baseUrl}}/v1/campaigns/{{golden_campaign_id}}/experiments/{{experiment_draft_id}}",
            "description": ""
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 204\", function () { pm.response.to.have.status(204); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/campaigns/:id/experiments/:experiment_id (borrado -> 404)",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/campaigns/{{golden_campaign_id}}/experiments/{{experiment_draft_id}}",
            "description": ""
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "56 · recordings redaction y transcript",
      "description": "core#2016 (ADR #245) y core#1971: la copia redactada, el audio original con motivo y la transcripción. Sin grabación real que sembrar, la carpeta ejerce con la llave dd_ de la colección las puertas deterministas: id inexistente, motivo ausente, método inválido y la transcripción cerrada a una sesión de supervisor/admin.",
      "item": [
        {
          "name": "GET /v1/recordings/:id/redaction (inexistente -> 404)",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/recordings/{{ghost_uuid}}/redaction",
            "description": "Sin grabación (o sin redacción) el id es 404, igual que uno de otro tenant."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "pm.test(\"error.code not_found\", function () { pm.expect(pm.response.json().error.code).to.eql(\"not_found\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/recordings/:id/redaction (method invalido -> 400)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/recordings/{{ghost_uuid}}/redaction",
            "description": "`method` sólo admite silence o beep; se valida antes de tocar la grabación.",
            "body": {
              "mode": "raw",
              "raw": "{\"segments\": [], \"method\": \"mp3\"}"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "pm.test(\"error.code bad_request\", function () { pm.expect(pm.response.json().error.code).to.eql(\"bad_request\"); });",
                  "pm.test(\"mensaje del método\", function () { pm.expect(pm.response.json().error.message).to.eql(\"method must be silence or beep\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/recordings/:id/original-url (sin reason -> 400)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/recordings/{{ghost_uuid}}/original-url",
            "description": "El audio original exige un motivo no vacío, que queda en el registro append-only antes de emitir la URL.",
            "body": {
              "mode": "raw",
              "raw": "{}"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "pm.test(\"error.code bad_request\", function () { pm.expect(pm.response.json().error.code).to.eql(\"bad_request\"); });",
                  "pm.test(\"mensaje del motivo\", function () { pm.expect(pm.response.json().error.message).to.eql(\"reason is required (a non-empty string)\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/recordings/:id/original-url (inexistente -> 404)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/recordings/{{ghost_uuid}}/original-url",
            "description": "Con motivo válido, una grabación inexistente responde 404 sin firmar nada.",
            "body": {
              "mode": "raw",
              "raw": "{\"reason\": \"auditoria de la coleccion Postman\"}"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "pm.test(\"error.code not_found\", function () { pm.expect(pm.response.json().error.code).to.eql(\"not_found\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/recordings/:id/transcript (llave dd_ -> 403)",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/recordings/{{ghost_uuid}}/transcript",
            "description": "La transcripción es PII cruda: sólo la lee un login de dashboard con asiento supervisor/admin. Una llave `dd_` recibe 403 antes de buscar la grabación."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"error.code role_forbidden\", function () { pm.expect(pm.response.json().error.code).to.eql(\"role_forbidden\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/recordings/:id/transcript/original (sin reason -> 400)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/recordings/{{ghost_uuid}}/transcript/original",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "recordings",
                "{{ghost_uuid}}",
                "transcript",
                "original"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{}"
            },
            "description": "El texto sin redactar exige un motivo no vacio (llave dd_ o login account_admin); se valida antes de buscar la grabacion y nada se escribe en el registro de accesos."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(400); });",
                  "pm.test(\"error.code bad_request\", function () { pm.expect(pm.response.json().error.code).to.eql(\"bad_request\"); });",
                  "pm.test(\"mensaje del motivo\", function () { pm.expect(pm.response.json().error.message).to.eql(\"reason is required (a non-empty string)\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/recordings/:id/transcript/original (inexistente -> 404)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/recordings/{{ghost_uuid}}/transcript/original",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "recordings",
                "{{ghost_uuid}}",
                "transcript",
                "original"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"reason\": \"postman: comprobacion del contrato 404\"\n}"
            },
            "description": "Con motivo valido, una grabacion sin transcripcion (o de otro tenant) es 404 sin entregar texto ni escribir en el registro de accesos."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(404); });",
                  "pm.test(\"error.code not_found\", function () { pm.expect(pm.response.json().error.code).to.eql(\"not_found\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/transcripts/search (llave dd_ -> 403)",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/transcripts/search?q=hola&from=2026-01-01T00:00:00Z&to=2026-01-08T00:00:00Z",
            "description": "Busqueda de una palabra en las transcripciones de una ventana (core#1974). Es dato personal en bruto: la puerta es la de la transcripcion suelta (login de dashboard con asiento de supervisor o admin); una llave dd_ es 403 role_forbidden."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"error.code role_forbidden\", function () { pm.expect(pm.response.json().error.code).to.eql(\"role_forbidden\"); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "57 · screen-monitor",
      "description": "core#1989 (FASE 0 de dashboard#696): aviso vigente y acuses de la observación de pantalla. La bandera `screen_monitor_enabled` nace apagada y se enciende por segmento, así que las dos rutas que la consultan se aceptan en ambos estados; la lista de acuses no depende de la bandera.",
      "item": [
        {
          "name": "GET /v1/screen-monitor/notice",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/screen-monitor/notice",
            "description": "Bandera apagada: 404 not_found. Encendida: 200 con el aviso publicado, o 404 si aún no hay ninguno."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200 o 404\", function () { pm.expect(pm.response.code).to.be.oneOf([200, 404]); });",
                  "pm.test(\"shape\", function () { const j = pm.response.json(); if (pm.response.code === 200) { pm.expect(j.notice).to.be.an(\"object\"); } else { pm.expect(j.error.code).to.eql(\"not_found\"); } });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/screen-monitor/acks (llave dd_ sin asiento)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/screen-monitor/acks",
            "description": "Sólo el propio asiento acusa el aviso. Bandera apagada: 404 not_found. Encendida: la llave `dd_` no tiene asiento y recibe 403 forbidden.",
            "body": {
              "mode": "raw",
              "raw": "{}"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403 o 404\", function () { pm.expect(pm.response.code).to.be.oneOf([403, 404]); });",
                  "pm.test(\"error.code coherente\", function () { pm.expect(pm.response.json().error.code).to.eql(pm.response.code === 404 ? \"not_found\" : \"forbidden\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/screen-monitor/acks (llave dd_ sin asiento -> 403)",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/screen-monitor/acks",
            "description": "El registro de acuses lo lista un asiento supervisor/admin y se evalúa sin consultar la bandera: una llave `dd_` no tiene asiento y recibe 403 forbidden."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"error.code forbidden\", function () { pm.expect(pm.response.json().error.code).to.eql(\"forbidden\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/screen-monitor/armed (llave dd_ sin asiento)",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/screen-monitor/armed",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "screen-monitor",
                "armed"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"armed\": true,\n  \"tab_id\": \"postman-tab\"\n}"
            },
            "description": "Solo el asiento propio (login ddu_) arma la captura. Bandera screen_monitor_enabled apagada: 404 not_found. Encendida: la llave dd_ no tiene asiento y recibe 403 forbidden."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403 o 404\", function () { pm.expect(pm.response.code, pm.response.text()).to.be.oneOf([403, 404]); });",
                  "pm.test(\"error.code coherente\", function () { pm.expect(pm.response.json().error.code).to.eql(pm.response.code === 404 ? \"not_found\" : \"forbidden\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/screen-monitor/sessions (llave dd_ -> 403)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/screen-monitor/sessions",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "screen-monitor",
                "sessions"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{ghost_uuid}}\"\n}"
            },
            "description": "Abrir una sesion de observacion es de un login ddu_ con asiento supervisor/admin. El rol se evalua SIEMPRE, sin consultar la bandera: una llave dd_ no tiene persona detras y recibe 403 forbidden sin fila."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(403); });",
                  "pm.test(\"error.code forbidden\", function () { pm.expect(pm.response.json().error.code).to.eql(\"forbidden\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/screen-monitor/sessions/:id/signal (llave dd_ sin asiento)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/screen-monitor/sessions/{{ghost_uuid}}/signal",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "screen-monitor",
                "sessions",
                "{{ghost_uuid}}",
                "signal"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"kind\": \"candidate\",\n  \"payload\": {}\n}"
            },
            "description": "Senalizacion WebRTC entre las dos partes de una sesion viva. Bandera apagada: 404 not_found. Encendida: la llave dd_ no es parte (no tiene asiento) y recibe 403 forbidden."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403 o 404\", function () { pm.expect(pm.response.code, pm.response.text()).to.be.oneOf([403, 404]); });",
                  "pm.test(\"error.code coherente\", function () { pm.expect(pm.response.json().error.code).to.eql(pm.response.code === 404 ? \"not_found\" : \"forbidden\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/screen-monitor/sessions/:id/end (llave dd_ -> 403)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/screen-monitor/sessions/{{ghost_uuid}}/end",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "screen-monitor",
                "sessions",
                "{{ghost_uuid}}",
                "end"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{}"
            },
            "description": "Cerrar la sesion es de una de las dos partes y NO consulta la bandera (cerrar siempre debe poder): la llave dd_ no tiene asiento y recibe 403 forbidden."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(403); });",
                  "pm.test(\"error.code forbidden\", function () { pm.expect(pm.response.json().error.code).to.eql(\"forbidden\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/screen-monitor/sessions/:id/ice (llave dd_ sin asiento)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/screen-monitor/sessions/{{ghost_uuid}}/ice",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "screen-monitor",
                "sessions",
                "{{ghost_uuid}}",
                "ice"
              ]
            },
            "description": "Credencial TURN efimera de la sesion de observacion (core#2131): la piden las dos partes de una sesion abierta. Bandera screen_monitor_enabled apagada: 404 not_found. Encendida: la llave dd_ no tiene asiento y no es parte, recibe 403 forbidden."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403 o 404\", function () { pm.expect(pm.response.code, pm.response.text()).to.be.oneOf([403, 404]); });",
                  "pm.test(\"error.code coherente\", function () { pm.expect(pm.response.json().error.code).to.eql(pm.response.code === 404 ? \"not_found\" : \"forbidden\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/screen-recordings/gaps (llave dd_ sin asiento)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/screen-recordings/gaps",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "screen-recordings",
                "gaps"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"call_uuid\": \"postman-call\",\n  \"reason\": \"agent_stopped\"\n}"
            },
            "description": "El cliente del agente declara un hueco de pantalla. Bandera apagada: 404 not_found. Encendida: la llave dd_ no tiene asiento y recibe 403 forbidden."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403 o 404\", function () { pm.expect(pm.response.code, pm.response.text()).to.be.oneOf([403, 404]); });",
                  "pm.test(\"error.code coherente\", function () { pm.expect(pm.response.json().error.code).to.eql(pm.response.code === 404 ? \"not_found\" : \"forbidden\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/screen-recordings/:id/chunks/:n (llave dd_ sin asiento)",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "video/webm"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/screen-recordings/{{ghost_uuid}}/chunks/{{screen_chunk_seq}}?call_uuid=postman-call",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "screen-recordings",
                "{{ghost_uuid}}",
                "chunks",
                "{{screen_chunk_seq}}"
              ],
              "query": [
                {
                  "key": "call_uuid",
                  "value": "postman-call"
                }
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "postman-chunk",
              "options": {}
            },
            "description": "Un tramo de la grabacion de pantalla del asiento propio (video/webm). Bandera apagada: 404 not_found. Encendida: la llave dd_ no tiene asiento y recibe 403 forbidden; nada se almacena."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403 o 404\", function () { pm.expect(pm.response.code, pm.response.text()).to.be.oneOf([403, 404]); });",
                  "pm.test(\"error.code coherente\", function () { pm.expect(pm.response.json().error.code).to.eql(pm.response.code === 404 ? \"not_found\" : \"forbidden\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/screen-recordings/:id/pii-marks (llave dd_ sin asiento)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/screen-recordings/{{ghost_uuid}}/pii-marks",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "screen-recordings",
                "{{ghost_uuid}}",
                "pii-marks"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"marks\": [{\"start_ms\": 1000, \"end_ms\": 2000, \"zone\": \"payment\"}],\n  \"covered_through_ms\": 5000\n}"
            },
            "description": "Marcas de PII del asiento propio sobre su grabacion de pantalla (core#2124, ADR 266). Bandera apagada: 404 not_found. Encendida: la llave dd_ no tiene asiento y recibe 403 forbidden; nada se almacena. El 200 de un ddu_ exige una grabacion ya subida y lo cubre el e2e."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403 o 404\", function () { pm.expect(pm.response.code, pm.response.text()).to.be.oneOf([403, 404]); });",
                  "pm.test(\"error.code coherente\", function () { pm.expect(pm.response.json().error.code).to.eql(pm.response.code === 404 ? \"not_found\" : \"forbidden\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/screen-recordings/playback (llave dd_ -> 403)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/screen-recordings/playback",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "screen-recordings",
                "playback"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"call_uuid\": \"postman-call\"\n}"
            },
            "description": "Pedir ver la grabacion de pantalla de una llamada (core#2109). Solo un asiento supervisor o admin (ddu_); una llave dd_ no tiene asiento y recibe 403 forbidden, sin fila de acceso ni URL."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(403); });",
                  "pm.test(\"error.code forbidden\", function () { pm.expect(pm.response.json().error.code).to.eql(\"forbidden\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/screen-recordings/original-playback (llave dd_ -> 403 role_forbidden)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/screen-recordings/original-playback",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "screen-recordings",
                "original-playback"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"call_uuid\": \"postman-call\",\n  \"reason\": \"revision de una disputa del consumidor\"\n}"
            },
            "description": "Pedir el video ORIGINAL (sin tapar) de la pantalla de una llamada (core#2124, ADR 266). Solo un login ddu_ con rol account_admin; una llave dd_ no es una persona y recibe 403 role_forbidden, sin fila de acceso ni URL. La puerta de rol se evalua antes de mirar el cuerpo y no consulta la bandera screen_monitor_enabled."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.expect(pm.response.code, pm.response.text()).to.eql(403); });",
                  "pm.test(\"error.code role_forbidden y sin URL\", function () {",
                  "  pm.expect(pm.response.json().error.code).to.eql(\"role_forbidden\");",
                  "  pm.expect(pm.response.text()).to.not.include(\"/v1/screen-media/\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/screen-monitor/sessions (llave dd_ -> 403)",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/screen-monitor/sessions",
            "description": "Libro de sesiones de monitoreo de pantalla (core#1990). Solo un asiento supervisor o admin; una llave dd_ es 403 forbidden y el rol se evalua siempre, con o sin supervision_role_enforced."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"error.code forbidden\", function () { pm.expect(pm.response.json().error.code).to.eql(\"forbidden\"); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "58 · business-types",
      "description": "core#2065 (ADR #246): la declaración de tipos de negocio del tenant y el piso de retención de grabaciones que implica. La lectura es de la consola; la escritura es de un login account_admin y una llave `dd_` solo lee.",
      "item": [
        {
          "name": "GET /v1/business-types",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/business-types",
            "description": "Declaración, vocabulario cerrado con el piso de cada tipo y piso efectivo."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: business_types es un objeto\", function () { pm.expect(pm.response.json().business_types).to.be.an(\"object\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /v1/business-types (llave dd_ -> 403)",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/business-types",
            "description": "Bajar un piso es decisión de una persona que responde por ella: la llave `dd_` lee pero no declara, y el 403 llega sin escribir nada.",
            "body": {
              "mode": "raw",
              "raw": "{\"business_types\": []}"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"error.code role_forbidden\", function () { pm.expect(pm.response.json().error.code).to.eql(\"role_forbidden\"); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "59 · login-policy recovery",
      "description": "core#1761 (ADR #227, FD-1): `DELETE /v1/admin/tenants/{id}/login-policy` es la vía de soporte que apaga la política de login de un tenant. Es de /v1/admin, así que el gate de deriva no la cuenta; la colección fija la negativa por credencial que el stack de CI sí puede ejercer.",
      "item": []
    },
    {
      "name": "60 · product-analytics",
      "description": "Analitica de producto y NPS dentro de la consola (macro-tasks#876): eventos de uso como claves de un catalogo cerrado y la pregunta de NPS. Las tres rutas piden un login del dashboard; la colección fija la negativa por credencial que el stack de CI sí puede ejercer con la llave `dd_`.",
      "item": [
        {
          "name": "POST /v1/product-analytics/events (llave dd_ -> 403)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/product-analytics/events",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "product-analytics",
                "events"
              ]
            },
            "description": "Registra eventos de uso de la consola del usuario que llama. Usa la llave `dd_` del harness: la ruta pide un login del dashboard (una PERSONA), asi que la llave recibe 403. El camino feliz (202/201, el catalogo cerrado, el intervalo de 90 dias) lo ejerce `ui/specs/matrix/product-analytics.spec.ts` y `test/db/product_analytics_test.exs` de core.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"events\": [\n    \"screen.campaigns\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403: una llave dd_ no tiene a nadie detras\", function () { pm.response.to.have.status(403); });",
                  "const j = pm.response.json();",
                  "pm.test(\"error.code forbidden\", function () { pm.expect(j.error.code).to.eql(\"forbidden\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/product-analytics/nps (llave dd_ -> 403)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/product-analytics/nps",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "product-analytics",
                "nps"
              ]
            },
            "description": "Dice si toca preguntar el NPS a la persona que llama. Usa la llave `dd_` del harness: la ruta pide un login del dashboard (una PERSONA), asi que la llave recibe 403. El camino feliz (202/201, el catalogo cerrado, el intervalo de 90 dias) lo ejerce `ui/specs/matrix/product-analytics.spec.ts` y `test/db/product_analytics_test.exs` de core."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403: una llave dd_ no tiene a nadie detras\", function () { pm.response.to.have.status(403); });",
                  "const j = pm.response.json();",
                  "pm.test(\"error.code forbidden\", function () { pm.expect(j.error.code).to.eql(\"forbidden\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/product-analytics/nps (llave dd_ -> 403)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/product-analytics/nps",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "product-analytics",
                "nps"
              ]
            },
            "description": "Guarda la respuesta de NPS (0 a 10) o el «ahora no». Usa la llave `dd_` del harness: la ruta pide un login del dashboard (una PERSONA), asi que la llave recibe 403. El camino feliz (202/201, el catalogo cerrado, el intervalo de 90 dias) lo ejerce `ui/specs/matrix/product-analytics.spec.ts` y `test/db/product_analytics_test.exs` de core.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"dismissed\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403: una llave dd_ no tiene a nadie detras\", function () { pm.response.to.have.status(403); });",
                  "const j = pm.response.json();",
                  "pm.test(\"error.code forbidden\", function () { pm.expect(j.error.code).to.eql(\"forbidden\"); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "61 · ai-agent-experiments",
      "description": "A/B de agentes de voz-IA (core#2229, core#2242; hija F de macro-tasks#875, integration-tests#1117): las siete operaciones `/v1/ai/agents/:id/experiments` (lista, crear, leer, editar, borrar, `start`, `stop`). Crea su propio agente, lo pasa a `live` (requisito: sobre otro estado es 409), recorre el ciclo borrador -> en marcha -> terminado y comprueba el 409 de lo congelado, el 422 de lo legal y el 404. Retira el agente al final. Reusa `{{ai_voice_id}}` de `24 · voice-ai`.",
      "item": [
        {
          "name": "POST /v1/ai/agents (agente propio del A/B, borrador)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/ai/agents",
            "description": "Agente propio de esta carpeta (no toca el de `24 · voice-ai`). El voice_id sale de `GET /v1/ai/voices` (carpeta 24).",
            "body": {
              "mode": "raw",
              "raw": "{\"name\": \"postman-ab-{{$timestamp}}\", \"voice\": {\"language\": \"en-US\", \"voice_id\": \"{{ai_voice_id}}\", \"pace\": 0.5}, \"disclosure\": {\"text\": \"This call is from an automated assistant calling on behalf of the account holder. This call is recorded.\"}}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const a = pm.response.json().ai_agent;",
                  "pm.test(\"shape: borrador\", function () { pm.expect(a.status).to.eql(\"draft\"); });",
                  "pm.collectionVariables.set(\"ai_exp_agent_id\", a.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/ai/agents/:id/experiments — agente en borrador -> 409",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/ai/agents/{{ai_exp_agent_id}}/experiments",
            "description": "Solo un agente `live` admite experimentos (core#2229): sobre un borrador es 409.",
            "body": {
              "mode": "raw",
              "raw": "{\"name\": \"postman A/B voz\", \"primary_metric\": \"handoff_rate\", \"variants\": [{\"name\": \"control\", \"weight\": 50, \"is_control\": true, \"overrides\": {}}, {\"name\": \"modelo\", \"weight\": 50, \"is_control\": false, \"overrides\": {\"model\": \"openai/gpt-oss-120b\"}}]}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 409\", function () { pm.response.to.have.status(409); });",
                  "pm.test(\"error conflict\", function () { pm.expect(pm.response.json().error.code).to.eql(\"conflict\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/ai/agents/:id (a live)",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/ai/agents/{{ai_exp_agent_id}}",
            "description": "Pasa el agente propio a `live`, requisito de los experimentos.",
            "body": {
              "mode": "raw",
              "raw": "{\"status\": \"live\"}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: live\", function () { pm.expect(pm.response.json().ai_agent.status).to.eql(\"live\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/ai/agents/:id/experiments (borrador de 2 variantes)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/ai/agents/{{ai_exp_agent_id}}/experiments",
            "description": "Crea un experimento `draft` (core#2229, core#2242): 2 a 5 variantes con exactamente una de control.",
            "body": {
              "mode": "raw",
              "raw": "{\"name\": \"postman A/B voz\", \"primary_metric\": \"handoff_rate\", \"variants\": [{\"name\": \"control\", \"weight\": 50, \"is_control\": true, \"overrides\": {}}, {\"name\": \"modelo\", \"weight\": 50, \"is_control\": false, \"overrides\": {\"model\": \"openai/gpt-oss-120b\"}}]}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const e = pm.response.json().experiment;",
                  "pm.test(\"shape: borrador con 2 variantes, una de control, en orden de posición\", function () { pm.expect(e.state).to.eql(\"draft\"); pm.expect(e.primary_metric).to.eql(\"handoff_rate\"); pm.expect(e.variants).to.have.lengthOf(2); pm.expect(e.variants.map(function (v) { return v.position; })).to.eql([0, 1]); pm.expect(e.variants.filter(function (v) { return v.is_control; })).to.have.lengthOf(1); });",
                  "pm.collectionVariables.set(\"ai_experiment_id\", e.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/ai/agents/:id/experiments — override fuera de la lista cerrada -> 422",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/ai/agents/{{ai_exp_agent_id}}/experiments",
            "description": "`disclosure`, `policy`, `handoff_rules`, `tools`... no se pueden variar: un A/B jamás toca lo legal.",
            "body": {
              "mode": "raw",
              "raw": "{\"name\": \"rechazado\", \"primary_metric\": \"handoff_rate\", \"variants\": [{\"name\": \"control\", \"weight\": 50, \"is_control\": true, \"overrides\": {}}, {\"name\": \"x\", \"weight\": 50, \"is_control\": false, \"overrides\": {\"disclosure\": {\"text\": \"otro\"}}}]}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 422\", function () { pm.response.to.have.status(422); });",
                  "pm.test(\"error con la ruta rechazada en details\", function () { const j = pm.response.json(); pm.expect(j.error.code).to.eql(\"unprocessable\"); pm.expect(JSON.stringify(j.error.details)).to.include(\"disclosure\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/ai/agents/:id/experiments",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/ai/agents/{{ai_exp_agent_id}}/experiments",
            "description": ""
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: lista con el experimento creado\", function () { const j = pm.response.json(); pm.expect(j.experiments).to.be.an(\"array\"); pm.expect(j.experiments.map(function (e) { return e.id; })).to.include(pm.collectionVariables.get(\"ai_experiment_id\")); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/ai/agents/:id/experiments/:experiment_id",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/ai/agents/{{ai_exp_agent_id}}/experiments/{{ai_experiment_id}}",
            "description": ""
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: el experimento pedido\", function () { const e = pm.response.json().experiment; pm.expect(e.id).to.eql(pm.collectionVariables.get(\"ai_experiment_id\")); pm.expect(e.ai_agent_id).to.eql(pm.collectionVariables.get(\"ai_exp_agent_id\")); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/ai/agents/:id/experiments/:experiment_id — id desconocido -> 404",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/ai/agents/{{ai_exp_agent_id}}/experiments/{{$guid}}",
            "description": ""
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "pm.test(\"404 del endpoint real (no catch-all)\", function () { pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/ai/agents/:id/experiments/:experiment_id (borrador)",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/ai/agents/{{ai_exp_agent_id}}/experiments/{{ai_experiment_id}}",
            "description": "",
            "body": {
              "mode": "raw",
              "raw": "{\"name\": \"postman A/B voz editado\"}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: nombre editado, sigue en borrador\", function () { const e = pm.response.json().experiment; pm.expect(e.name).to.eql(\"postman A/B voz editado\"); pm.expect(e.state).to.eql(\"draft\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/ai/agents/:id/experiments/:experiment_id/start",
          "request": {
            "method": "POST",
            "header": [],
            "url": "{{baseUrl}}/v1/ai/agents/{{ai_exp_agent_id}}/experiments/{{ai_experiment_id}}/start",
            "description": ""
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: en marcha\", function () { const e = pm.response.json().experiment; pm.expect(e.state).to.eql(\"running\"); pm.expect(e.started_at).to.be.a(\"string\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/ai/agents/:id/experiments/:experiment_id/results",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/ai/agents/{{ai_exp_agent_id}}/experiments/{{ai_experiment_id}}/results",
            "description": "Resultados y veredicto honesto por variante (core#2230, integration-tests#1434): se recalculan en cada lectura. Con el experimento recién arrancado y sin conversaciones atribuidas, cada variante trae ceros y el veredicto no nombra ganador."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const r = pm.response.json().results;",
                  "pm.test(\"shape: experimento, una fila por variante, veredicto y avisos\", function () { pm.expect(r.experiment.id).to.eql(pm.collectionVariables.get(\"ai_experiment_id\")); pm.expect(r.variants).to.have.lengthOf(2); pm.expect(r.unattributed_conversations).to.eql(0); pm.expect(r.verdict.status).to.be.a(\"string\"); pm.expect(r.allocation_warning).to.be.a(\"boolean\"); });",
                  "pm.test(\"sin conversaciones no hay ganador\", function () { pm.expect(r.verdict.winner).to.eql(null); });"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /v1/ai/agents/:id/experiments/:experiment_id (en marcha -> 409)",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/ai/agents/{{ai_exp_agent_id}}/experiments/{{ai_experiment_id}}",
            "description": "Un experimento en marcha tiene las variantes congeladas.",
            "body": {
              "mode": "raw",
              "raw": "{\"name\": \"no debe cambiar\"}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 409\", function () { pm.response.to.have.status(409); });"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/ai/agents/:id/experiments/:experiment_id (en marcha -> 409)",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": "{{baseUrl}}/v1/ai/agents/{{ai_exp_agent_id}}/experiments/{{ai_experiment_id}}",
            "description": ""
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 409\", function () { pm.response.to.have.status(409); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/ai/agents/:id/experiments/:experiment_id/stop",
          "request": {
            "method": "POST",
            "header": [],
            "url": "{{baseUrl}}/v1/ai/agents/{{ai_exp_agent_id}}/experiments/{{ai_experiment_id}}/stop",
            "description": ""
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: terminado\", function () { const e = pm.response.json().experiment; pm.expect(e.state).to.eql(\"ended\"); pm.expect(e.ended_at).to.be.a(\"string\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/ai/agents/:id/experiments/:experiment_id/stop (terminado -> 409)",
          "request": {
            "method": "POST",
            "header": [],
            "url": "{{baseUrl}}/v1/ai/agents/{{ai_exp_agent_id}}/experiments/{{ai_experiment_id}}/stop",
            "description": ""
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 409\", function () { pm.response.to.have.status(409); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/ai/agents/:id/experiments/:experiment_id/promote (sin ganador -> 409)",
          "request": {
            "method": "POST",
            "header": [],
            "url": "{{baseUrl}}/v1/ai/agents/{{ai_exp_agent_id}}/experiments/{{ai_experiment_id}}/promote",
            "description": ""
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 409\", function () { pm.response.to.have.status(409); });"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/ai/agents/:id/experiments/:experiment_id (terminado -> 409)",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": "{{baseUrl}}/v1/ai/agents/{{ai_exp_agent_id}}/experiments/{{ai_experiment_id}}",
            "description": ""
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 409\", function () { pm.response.to.have.status(409); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/ai/agents/:id/experiments (segundo borrador, para borrar)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/ai/agents/{{ai_exp_agent_id}}/experiments",
            "description": "",
            "body": {
              "mode": "raw",
              "raw": "{\"name\": \"postman A/B voz\", \"primary_metric\": \"handoff_rate\", \"variants\": [{\"name\": \"control\", \"weight\": 50, \"is_control\": true, \"overrides\": {}}, {\"name\": \"modelo\", \"weight\": 50, \"is_control\": false, \"overrides\": {\"model\": \"openai/gpt-oss-120b\"}}]}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "pm.collectionVariables.set(\"ai_experiment_id\", pm.response.json().experiment.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/ai/agents/:id/experiments/:experiment_id (borrador -> 204)",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": "{{baseUrl}}/v1/ai/agents/{{ai_exp_agent_id}}/experiments/{{ai_experiment_id}}",
            "description": ""
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 204\", function () { pm.response.to.have.status(204); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/ai/agents/:id/experiments/:experiment_id (borrado -> 404)",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/ai/agents/{{ai_exp_agent_id}}/experiments/{{ai_experiment_id}}",
            "description": ""
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /v1/ai/agents/:id (retira el agente propio)",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": "{{baseUrl}}/v1/ai/agents/{{ai_exp_agent_id}}",
            "description": ""
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: retired\", function () { pm.expect(pm.response.json().ai_agent.status).to.eql(\"retired\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/ai/agents/drift",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/ai/agents/drift",
            "description": "Alertas de deriva de la voz IA y cobertura de lo vigilado (core#2231, integration-tests#1434). Ruta estática antes de `/ai/agents/:id`: no se confunde con un id de agente."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "const b = pm.response.json();",
                  "pm.test(\"shape: drift[], coverage[] y next_cursor\", function () { pm.expect(b.drift).to.be.an(\"array\"); pm.expect(b.coverage).to.be.an(\"array\"); pm.expect(b).to.have.property(\"next_cursor\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/ai/agents/drift — model junto a agent_id -> 400",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/ai/agents/drift?agent_id={{ai_exp_agent_id}}&model=x",
            "description": "`model` es otro ámbito y no se combina con `agent_id`."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 400\", function () { pm.response.to.have.status(400); });",
                  "pm.test(\"400 del endpoint real (no catch-all)\", function () { pm.expect(pm.response.json().error.message).to.not.eql(\"no such route\"); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "62 · agent-monitoring-notice",
      "description": "core#2323 (MT-CLI-37): el aviso de grabación y supervisión que el agente acepta, y la constancia por agente y fecha. El catálogo de textos NACE VACÍO (core no publica texto legal), así que el aviso se acepta en los dos estados (200 con texto publicado, 404 sin él). La aceptación sólo sale del propio asiento: la llave `dd_` no tiene asiento y recibe 403. La puerta del `Ready` que la exigirá (core#2339, PR 3/5, detrás de la bandera `agent_monitoring_ack_enforced`, que nace apagada) no se ejercita aquí.",
      "item": [
        {
          "name": "GET /v1/agent-monitoring-notice",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/agent-monitoring-notice",
            "description": "Los textos de la versión vigente, uno por idioma. 404 not_found mientras no haya ninguna versión publicada."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200 o 404\", function () { pm.expect(pm.response.code).to.be.oneOf([200, 404]); });",
                  "pm.test(\"shape\", function () { const j = pm.response.json(); if (pm.response.code === 200) { pm.expect(j.notice.version).to.be.a(\"number\"); pm.expect(j.notice.texts).to.be.an(\"array\").that.is.not.empty; pm.expect(j.notice.texts[0].text_sha256).to.match(/^[0-9a-f]{64}$/); } else { pm.expect(j.error.code).to.eql(\"not_found\"); } });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/agent-monitoring-acknowledgements (llave dd_ sin asiento -> 403)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{baseUrl}}/v1/agent-monitoring-acknowledgements",
            "description": "Sólo el propio asiento acepta el aviso: la llave `dd_` es una máquina sin persona detrás y recibe 403 forbidden, antes de mirar el cuerpo.",
            "body": {
              "mode": "raw",
              "raw": "{\"notice_version\": 1, \"lang\": \"es\"}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 403\", function () { pm.response.to.have.status(403); });",
                  "pm.test(\"error.code forbidden\", function () { pm.expect(pm.response.json().error.code).to.eql(\"forbidden\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/agents/:id/monitoring-acknowledgement (la llave dd_ lee el estado de un agente)",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/agents/{{postman_agent_id}}/monitoring-acknowledgement",
            "description": "La última aceptación del agente (null si no hay) y si cubre la versión vigente. Un agente que nunca aceptó: `current` false y `acknowledgement` null."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: sin aceptación\", function () { const m = pm.response.json().monitoring; pm.expect(m.current).to.eql(false); pm.expect(m.acknowledgement).to.eql(null); pm.expect(m).to.have.property(\"notice_version\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/agents/:id/monitoring-acknowledgement (id inexistente -> 404)",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{baseUrl}}/v1/agents/{{ghost_uuid}}/monitoring-acknowledgement",
            "description": "Un id que no es de este tenant es 404 not_found, y NO el 404 del enrutador (la ruta existe)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 404\", function () { pm.response.to.have.status(404); });",
                  "pm.test(\"es el 404 del recurso, no del enrutador\", function () { const j = pm.response.json(); pm.expect(j.error.code).to.eql(\"not_found\"); pm.expect(j.error.message).to.not.include(\"no such route\"); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "63 · pricing (lista de precios publica)",
      "description": "core#2834 (SS-C1 de macro-tasks#1077): la lista de precios vigente por unidad, sin autenticación de tenant. Cada unidad de la tabla aparece siempre (`rates: []` mientras no tenga precio cargado). Las rutas de carga y retirada (`/v1/admin/unit-rates`) son de operador y no entran en esta colección.",
      "item": [
        {
          "name": "GET /v1/pricing",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/pricing",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "pricing"
              ]
            },
            "description": "Lista vigente hoy (UTC), sin bearer. `price` es un decimal en texto (USD por unidad); la voz lleva `billing_increment_secs`.",
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape\", function () { const j = pm.response.json(); pm.expect(j.currency).to.eql(\"USD\"); pm.expect(j.units.map(function (u) { return u.unit; })).to.include.members([\"voice_out\", \"voice_in\", \"sms_segment\", \"email_message\", \"did_rent\", \"ai_minute\", \"recording_minute\", \"storage_gb_month\"]); j.units.forEach(function (u) { pm.expect(u.rates).to.be.an(\"array\"); u.rates.forEach(function (r) { pm.expect(r.price).to.match(/^[0-9]+(\\.[0-9]+)?$/); }); }); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "64 · support tickets",
      "description": "core#2854: el tenant abre un ticket de soporte, lo lista, lo lee, anade un mensaje y lo cierra. La respuesta del operador vive en `/v1/admin/support/...` y no entra en esta coleccion.",
      "item": [
        {
          "name": "POST /v1/support/tickets",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/support/tickets",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "support",
                "tickets"
              ]
            },
            "description": "Abre un ticket de soporte (core#2854): devuelve el ticket con su primer mensaje. Guarda el id en `support_ticket_id` para los requests siguientes.",
            "body": {
              "mode": "raw",
              "raw": "{\"subject\": \"newman: ticket de prueba\", \"body\": \"Ticket abierto por la coleccion Postman contra el stack real.\", \"priority\": \"low\"}"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "const t = pm.response.json().ticket;",
                  "pm.test(\"shape: ticket abierto con id, estado y asunto\", function () { pm.expect(t.id).to.be.a(\"string\"); pm.expect(t.status).to.eql(\"open\"); pm.expect(t.subject).to.eql(\"newman: ticket de prueba\"); });",
                  "pm.collectionVariables.set(\"support_ticket_id\", t.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/support/tickets",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/support/tickets?status=open",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "support",
                "tickets"
              ],
              "query": [
                {
                  "key": "status",
                  "value": "open"
                }
              ]
            },
            "description": "Lista los tickets del tenant, mas nuevos primero; `?status=open` filtra. Sin hilo de mensajes."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"el ticket abierto esta en la lista\", function () { const ts = pm.response.json().tickets; pm.expect(ts).to.be.an(\"array\"); pm.expect(ts.map(function (t) { return t.id; })).to.include(pm.collectionVariables.get(\"support_ticket_id\")); });"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /v1/support/tickets/:id",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/support/tickets/{{support_ticket_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "support",
                "tickets",
                "{{support_ticket_id}}"
              ]
            },
            "description": "El ticket con su hilo."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: ticket con id\", function () { const t = pm.response.json().ticket; pm.expect(t.id).to.be.a(\"string\"); pm.expect(t.id).to.eql(pm.collectionVariables.get(\"support_ticket_id\")); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/support/tickets/:id/messages",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/support/tickets/{{support_ticket_id}}/messages",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "support",
                "tickets",
                "{{support_ticket_id}}",
                "messages"
              ]
            },
            "description": "Anade un mensaje del tenant; 201 con el ticket. Un ticket cerrado responde 409.",
            "body": {
              "mode": "raw",
              "raw": "{\"body\": \"Segundo mensaje de la coleccion Postman.\"}"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 201\", function () { pm.response.to.have.status(201); });",
                  "pm.test(\"shape: ticket con id\", function () { const t = pm.response.json().ticket; pm.expect(t.id).to.be.a(\"string\"); pm.expect(t.id).to.eql(pm.collectionVariables.get(\"support_ticket_id\")); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/support/tickets/:id/close",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/support/tickets/{{support_ticket_id}}/close",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "support",
                "tickets",
                "{{support_ticket_id}}",
                "close"
              ]
            },
            "description": "Cierra el ticket (200)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: ticket con id\", function () { const t = pm.response.json().ticket; pm.expect(t.id).to.be.a(\"string\"); pm.expect(t.id).to.eql(pm.collectionVariables.get(\"support_ticket_id\")); });",
                  "pm.test(\"status closed\", function () { pm.expect(pm.response.json().ticket.status).to.eql(\"closed\"); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "65 · signups (alta publica)",
      "description": "core#2838: alta publica de un desarrollador, sin bearer. Solo la lectura de configuracion y el rechazo fail-closed de un cuerpo invalido; el registro real no se ejerce desde newman para no mandar correo.",
      "item": [
        {
          "name": "GET /v1/signups/config",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/signups/config",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "signups",
                "config"
              ]
            },
            "description": "Lo que el formulario de alta necesita (core#2838): si la celda admite altas, la version de terminos vigente y la clave publica del anti-bot. Publica, sin bearer."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"status 200\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"shape: available, terms_version y captcha\", function () { const j = pm.response.json(); pm.expect(j.available).to.be.a(\"boolean\"); pm.expect(j.captcha.provider).to.eql(\"turnstile\"); pm.expect(j).to.have.property(\"terms_version\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /v1/signups",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/signups",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "signups"
              ]
            },
            "description": "Alta publica (core#2838). Este request manda un campo desconocido: la ruta lo rechaza con 400 `bad_request` ANTES de registrar nada, asi que no se crea cuenta ni sale correo. Si la celda no tiene terminos o anti-bot configurados responde antes 503 `signup_unavailable`; ambos prueban que la ruta existe y cierra fail-closed. El camino 202 lo cubren los tests de core.",
            "body": {
              "mode": "raw",
              "raw": "{\"email\": \"newman-signup@example.test\", \"name\": \"Newman\", \"terms_version\": \"x\", \"campo_inexistente\": true}"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"rechazo fail-closed: 400 bad_request o 503 signup_unavailable\", function () { const e = pm.response.json().error; if (pm.response.code === 400) { pm.expect(e.code).to.eql(\"bad_request\"); } else { pm.response.to.have.status(503); pm.expect(e.code).to.eql(\"signup_unavailable\"); } });"
                ]
              }
            }
          ]
        }
      ]
    }
  ],
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://api.usa.dialerdigital.com"
    },
    {
      "key": "tenant_id",
      "value": ""
    },
    {
      "key": "tenant_name",
      "value": ""
    },
    {
      "key": "api_key_id",
      "value": ""
    },
    {
      "key": "golden_tz",
      "value": ""
    },
    {
      "key": "golden_state",
      "value": ""
    },
    {
      "key": "golden_city",
      "value": ""
    },
    {
      "key": "golden_campaign_id",
      "value": ""
    },
    {
      "key": "golden_import_body",
      "value": ""
    },
    {
      "key": "golden_debt_id",
      "value": ""
    },
    {
      "key": "golden_archive_id",
      "value": "00000000-0000-0000-0000-000000000000"
    },
    {
      "key": "golden_consumer_ref",
      "value": ""
    },
    {
      "key": "golden_contact_phone",
      "value": ""
    },
    {
      "key": "golden_typed_unknown",
      "value": ""
    },
    {
      "key": "golden_agent_id",
      "value": ""
    },
    {
      "key": "golden_agent_ext",
      "value": ""
    },
    {
      "key": "golden_call_id",
      "value": ""
    },
    {
      "key": "campaign_id",
      "value": ""
    },
    {
      "key": "attempt_id",
      "value": ""
    },
    {
      "key": "callback_when",
      "value": ""
    },
    {
      "key": "callback_id_complete",
      "value": ""
    },
    {
      "key": "callback_id_cancel",
      "value": ""
    },
    {
      "key": "callback_id_miss",
      "value": ""
    },
    {
      "key": "promise_id",
      "value": ""
    },
    {
      "key": "postman_agent_ext",
      "value": ""
    },
    {
      "key": "postman_agent_id",
      "value": ""
    },
    {
      "key": "did_id",
      "value": ""
    },
    {
      "key": "postman_did_e164",
      "value": ""
    },
    {
      "key": "carrier_id",
      "value": ""
    },
    {
      "key": "inbound_route_id",
      "value": ""
    },
    {
      "key": "postman_inbound_e164",
      "value": ""
    },
    {
      "key": "postman_inbound_foreign_e164",
      "value": ""
    },
    {
      "key": "sms_provider_id",
      "value": ""
    },
    {
      "key": "sms_send_provider_id",
      "value": ""
    },
    {
      "key": "sms_message_id",
      "value": ""
    },
    {
      "key": "decision_id",
      "value": "",
      "type": "string"
    },
    {
      "key": "ghost_uuid",
      "value": "00000000-0000-0000-0000-000000000000",
      "type": "string"
    },
    {
      "key": "postman_user_id",
      "value": "",
      "type": "string"
    },
    {
      "key": "postman_user_email",
      "value": "",
      "type": "string"
    },
    {
      "key": "ddu_token",
      "value": "",
      "type": "string"
    },
    {
      "key": "widget_agent_id",
      "value": "",
      "type": "string"
    },
    {
      "key": "widget_agent_email",
      "value": "",
      "type": "string"
    },
    {
      "key": "recording_id",
      "value": ""
    },
    {
      "key": "recording_export_job_id",
      "value": ""
    },
    {
      "key": "ai_agent_id",
      "value": ""
    },
    {
      "key": "ai_agent_name",
      "value": ""
    },
    {
      "key": "ai_voice_id",
      "value": "b24f41fd-00a3-4cd8-992a-a0c9f13f3ef1"
    },
    {
      "key": "ai_voice_language",
      "value": "en"
    },
    {
      "key": "ai_conversation_id",
      "value": ""
    },
    {
      "key": "export_from",
      "value": ""
    },
    {
      "key": "export_to",
      "value": ""
    },
    {
      "key": "export_from_oversized",
      "value": ""
    },
    {
      "key": "export_to_oversized",
      "value": ""
    },
    {
      "key": "reset_202_body",
      "value": ""
    },
    {
      "key": "reset_token",
      "value": ""
    },
    {
      "key": "invite_token",
      "value": ""
    },
    {
      "key": "webhook_id",
      "value": ""
    },
    {
      "key": "webhook_secret_hint",
      "value": ""
    },
    {
      "key": "academy_completion_id",
      "value": ""
    },
    {
      "key": "academy_completed_at",
      "value": ""
    },
    {
      "key": "occupancy_from",
      "value": ""
    },
    {
      "key": "occupancy_to",
      "value": ""
    },
    {
      "key": "scorecard_user_id",
      "value": ""
    },
    {
      "key": "scorecard_user_email",
      "value": ""
    },
    {
      "key": "scorecard_invite_token",
      "value": ""
    },
    {
      "key": "scorecard_ddu_token",
      "value": ""
    },
    {
      "key": "scorecard_agent_id",
      "value": ""
    },
    {
      "key": "scorecard_from",
      "value": ""
    },
    {
      "key": "scorecard_to",
      "value": ""
    },
    {
      "key": "debt_list_name",
      "value": "",
      "type": "string"
    },
    {
      "key": "debt_list_ref",
      "value": "",
      "type": "string"
    },
    {
      "key": "debt_list_id",
      "value": "",
      "type": "string"
    },
    {
      "key": "debt_list_debt_id",
      "value": "",
      "type": "string"
    },
    {
      "key": "inbound_ivr_menu_id",
      "value": ""
    },
    {
      "key": "postman_ivr_e164",
      "value": ""
    },
    {
      "key": "inbound_ivr_route_id",
      "value": ""
    },
    {
      "key": "inbound_queue_name",
      "value": ""
    },
    {
      "key": "inbound_queue_name_upper",
      "value": ""
    },
    {
      "key": "inbound_queue_dest_name",
      "value": ""
    },
    {
      "key": "inbound_queue_id",
      "value": ""
    },
    {
      "key": "inbound_queue_dest_id",
      "value": ""
    },
    {
      "key": "postman_queue_e164",
      "value": ""
    },
    {
      "key": "inbound_queue_route_id",
      "value": ""
    },
    {
      "key": "segment_filter_id",
      "value": ""
    },
    {
      "key": "segment_filter_name",
      "value": ""
    },
    {
      "key": "segment_filter_saved_list_name",
      "value": ""
    },
    {
      "key": "segment_filter_inline_list_name",
      "value": ""
    },
    {
      "key": "import_mapping_id",
      "value": ""
    },
    {
      "key": "import_mapping_name",
      "value": ""
    },
    {
      "key": "import_mapping_ref",
      "value": ""
    },
    {
      "key": "import_mapping_unknown_id",
      "value": ""
    },
    {
      "key": "import_mapping_version_first",
      "value": ""
    },
    {
      "key": "import_mapping_version_unknown",
      "value": ""
    },
    {
      "key": "operator_token",
      "value": "",
      "type": "string"
    },
    {
      "key": "qm_form_version",
      "value": "1",
      "type": "string"
    },
    {
      "key": "sms_template_id",
      "value": "",
      "type": "string"
    },
    {
      "key": "sms_template_name",
      "value": "",
      "type": "string"
    },
    {
      "key": "mfa_enroll_challenge",
      "value": "",
      "type": "string"
    },
    {
      "key": "mfa_enroll_secret",
      "value": "",
      "type": "string"
    },
    {
      "key": "mfa_enroll_code",
      "value": "",
      "type": "string"
    },
    {
      "key": "mfa_enroll_recovery_code",
      "value": "",
      "type": "string"
    },
    {
      "key": "rewind_segment_id",
      "value": "",
      "type": "string"
    },
    {
      "key": "disposition_correction_id",
      "value": "",
      "type": "string"
    },
    {
      "key": "disclosure_foreign_attempt_id",
      "value": "",
      "type": "string"
    },
    {
      "key": "click_to_call_origin",
      "value": "https://widget.example.test"
    },
    {
      "key": "click_to_call_site_id",
      "value": ""
    },
    {
      "key": "click_to_call_token",
      "value": "",
      "type": "string"
    },
    {
      "key": "click_to_call_consent_version",
      "value": ""
    },
    {
      "key": "click_to_call_consent_sha256",
      "value": ""
    },
    {
      "key": "click_to_call_cap_site_id",
      "value": ""
    },
    {
      "key": "click_to_call_cap_token",
      "value": "",
      "type": "string"
    },
    {
      "key": "click_to_call_cap_consent_version",
      "value": ""
    },
    {
      "key": "click_to_call_cap_consent_sha256",
      "value": ""
    },
    {
      "key": "click_to_call_unknown_phone",
      "value": ""
    },
    {
      "key": "click_to_call_uniform_keys",
      "value": ""
    },
    {
      "key": "report_run_from",
      "value": ""
    },
    {
      "key": "report_run_to",
      "value": ""
    },
    {
      "key": "screen_chunk_seq",
      "value": "0"
    },
    {
      "key": "report_definition_id",
      "value": ""
    },
    {
      "key": "report_definition_name",
      "value": ""
    },
    {
      "key": "ai_exp_agent_id",
      "value": ""
    },
    {
      "key": "ai_experiment_id",
      "value": ""
    },
    {
      "key": "support_ticket_id",
      "value": ""
    }
  ],
  "event": [
    {
      "listen": "test",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// core#1471 — cuota por API key: contrato de cabeceras en cada respuesta.",
          "var limit = pm.response.headers.get('X-RateLimit-Limit');",
          "if (limit !== undefined && limit !== null) {",
          "  pm.test('X-RateLimit-* coherentes', function () {",
          "    var lim = Number(limit);",
          "    var rem = Number(pm.response.headers.get('X-RateLimit-Remaining'));",
          "    var reset = Number(pm.response.headers.get('X-RateLimit-Reset'));",
          "    pm.expect(Number.isInteger(lim) && lim > 0, 'X-RateLimit-Limit entero > 0').to.be.true;",
          "    pm.expect(Number.isInteger(rem) && rem >= 0 && rem <= lim, 'X-RateLimit-Remaining en [0, Limit]').to.be.true;",
          "    pm.expect(Number.isInteger(reset) && reset >= 1, 'X-RateLimit-Reset entero >= 1').to.be.true;",
          "  });",
          "}",
          "if (pm.response.code === 429) {",
          "  var body = {};",
          "  try { body = pm.response.json(); } catch (e) { body = {}; }",
          "  const isClickToCall = pm.request.url.getPath() === '/v1/public/call-requests';",
          "  // core#2704: el tope de 5 envios de prueba/min es del WEBHOOK, no de la key: su 429 lleva Retry-After pero no agota X-RateLimit-Remaining.",
          "  const isWebhookTest = /^\\/v1\\/webhooks\\/[^/]+\\/test$/.test(pm.request.url.getPath());",
          "  if (body && body.error && body.error.code === 'rate_limited' && !isClickToCall) {",
          "    pm.test(isWebhookTest ? '429 rate_limited (tope por webhook) trae Retry-After entero >= 1' : '429 rate_limited trae Retry-After entero >= 1 y Remaining 0', function () {",
          "      var ra = Number(pm.response.headers.get('Retry-After'));",
          "      pm.expect(Number.isInteger(ra) && ra >= 1, 'Retry-After entero >= 1').to.be.true;",
          "      if (!isWebhookTest) { pm.expect(pm.response.headers.get('X-RateLimit-Remaining')).to.eql('0'); }",
          "    });",
          "  }",
          "}"
        ]
      }
    }
  ]
}
