{
  "info": {
    "name": "tagger",
    "description": "# tagger API\n\nServicio de categorización automática de movimientos bancarios.\n\n## Pipeline\nCascada en orden, primer hit retorna:\n1. **Catálogo** — lookup directo `bancard_id + codigo_comercio` (más rápido + confiable)\n2. **Patrones** — `regex` / `literal` / `contiene` / `prefijo` sobre descripción\n3. **MCC** — código MCC oficial mapeado a categoría\n4. **IA fallback** — Ollama (Gemma) async, response inmediato es null + revisión\n\n## Auth\nHeader `x-api-key: <apiKey>` excepto `/health*` y `/ui/*`.\n\n## Variables collection\n- `baseUrl` — URL del servicio (default `http://localhost:3000`)\n- `apiKey` — valor del header `x-api-key`\n- `movimientoId` — auto-seteado tras POST `/categorizar-movimiento`\n- `categoriaId` — auto-seteado tras GET `/categorias`\n\n## Workflow sugerido para explorar\n1. `health → GET /health` (verificar servicio vivo)\n2. `categorias → GET /categorias` (cachea `categoriaId`)\n3. `categorizar → POST regex hit` (cachea `movimientoId`)\n4. `movimientos → GET /movimientos/:id` (ver detalle + evidencia)\n5. `movimientos → POST correccion` (usa `categoriaId` cacheado)\n\n## Docs adicionales\n- `docs/integration-guide.md` — guía detallada para devs mobile\n- `openapi.yaml` — spec OpenAPI 3.1 (importable a Swagger UI / generadores TS)\n- `docs/runbook.md` — operación + troubleshooting",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
    "_postman_id": "11111111-1111-1111-1111-111111111111"
  },
  "auth": {
    "type": "apikey",
    "apikey": [
      {
        "key": "key",
        "value": "x-api-key",
        "type": "string"
      },
      {
        "key": "value",
        "value": "{{apiKey}}",
        "type": "string"
      },
      {
        "key": "in",
        "value": "header",
        "type": "string"
      }
    ]
  },
  "variable": [
    {
      "key": "baseUrl",
      "value": "http://localhost:3000"
    },
    {
      "key": "apiKey",
      "value": "replace-with-strong-random-key-min-16-chars"
    },
    {
      "key": "movimientoId",
      "value": ""
    },
    {
      "key": "categoriaId",
      "value": ""
    },
    {
      "key": "usuario",
      "value": "demo"
    },
    {
      "key": "subcategoriaUsuarioId",
      "value": ""
    }
  ],
  "item": [
    {
      "name": "health",
      "description": "Endpoints sin auth para liveness/readiness probes.\n\n- **`/health`** — liveness: ¿servicio respondiendo? Usar para k8s livenessProbe / ALB target health.\n- **`/health/ready`** — readiness: ¿DB + Ollama OK? Usar para k8s readinessProbe (no recibir tráfico hasta listo).",
      "item": [
        {
          "name": "GET /health",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/health",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "health"
              ]
            },
            "description": "Liveness probe. Sin auth.\n\n**Response 200:** `{\"status\":\"ok\"}`"
          }
        },
        {
          "name": "GET /health/ready",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/health/ready",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "health",
                "ready"
              ]
            },
            "description": "Readiness probe. Verifica DB + Ollama.\n\n**Response:** `{\"status\":\"ok|degraded\", \"db\":bool, \"ollama\":bool}`\n\nOllama puede estar `false` (servicio igual operativo, IA fallback no disponible)."
          }
        }
      ]
    },
    {
      "name": "categorias",
      "description": "Catálogo de categorías disponibles.\n\nCliente mobile lo usa para:\n- Poblar dropdown UI al mostrar correcciones\n- Mapear `categoria_id` de response → slug/nombre para render\n\n**Cachear en cliente** — cambian raro. 35 categorías por defecto en seed.",
      "item": [
        {
          "name": "GET /categorias",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const res = pm.response.json();",
                  "if (res.items && res.items.length > 0) {",
                  "  pm.collectionVariables.set('categoriaId', res.items[0].id);",
                  "  console.log('categoriaId =', res.items[0].id);",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/categorias",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "categorias"
              ]
            },
            "description": "Lista todas las categorías activas.\n\n**Response 200:**\n```json\n{\"items\":[{\"id\":\"uuid\",\"slug\":\"transporte\",\"nombre\":\"Transporte\",\"descripcion\":\"...\",\"activo\":true}, ...]}\n```\n\nTest script: setea `categoriaId` con primer item (uso en correccion)."
          }
        },
        {
          "name": "GET /categorias/:identificador/usage",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/categorias/:identificador/usage",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "categorias",
                ":identificador",
                "usage"
              ],
              "variable": [
                {
                  "key": "identificador",
                  "value": "transporte",
                  "description": "slug actual, alias antiguo o UUID"
                }
              ]
            },
            "description": "Counts de refs en otras tablas (movimientos / mcc_por_nombre / mcc_catalogo). Útil antes de borrar cat."
          }
        },
        {
          "name": "GET /categorias/:identificador/similares",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/categorias/:identificador/similares?limit=5",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "categorias",
                ":identificador",
                "similares"
              ],
              "variable": [
                {
                  "key": "identificador",
                  "value": "hogar"
                }
              ],
              "query": [
                {
                  "key": "q",
                  "value": "",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "5"
                },
                {
                  "key": "offset",
                  "value": "0",
                  "disabled": true
                },
                {
                  "key": "umbral",
                  "value": "0",
                  "disabled": true
                }
              ]
            },
            "description": "Categorías similares por similitud trigram (sin la origen). Si pasás `q`, busca contra ese texto."
          }
        }
      ]
    },
    {
      "name": "reglas",
      "description": "CRUD reglas (literal/contiene/regex) con scope global o usuario:X. Unifica memoria + patrones globales + patrones-usuario.",
      "item": [
        {
          "name": "GET /reglas?scope=global",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{API_KEY}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/reglas?scope=global",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "reglas"
              ],
              "query": [
                {
                  "key": "scope",
                  "value": "global"
                }
              ]
            }
          }
        },
        {
          "name": "GET /reglas?scope=usuario:X",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{API_KEY}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/reglas?scope=usuario:{{usuario}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "reglas"
              ],
              "query": [
                {
                  "key": "scope",
                  "value": "usuario:{{usuario}}"
                }
              ]
            }
          }
        },
        {
          "name": "GET /reglas/sugerencias?usuario=X",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{API_KEY}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/reglas/sugerencias?usuario={{usuario}}&umbral=2",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "reglas",
                "sugerencias"
              ],
              "query": [
                {
                  "key": "usuario",
                  "value": "{{usuario}}"
                },
                {
                  "key": "umbral",
                  "value": "2"
                }
              ]
            }
          }
        },
        {
          "name": "GET /reglas/sugerencias-globales",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{API_KEY}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/reglas/sugerencias-globales?min_usuarios=3&min_total=5",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "reglas",
                "sugerencias-globales"
              ],
              "query": [
                {
                  "key": "min_usuarios",
                  "value": "3"
                },
                {
                  "key": "min_total",
                  "value": "5"
                }
              ]
            },
            "description": "Nombres normalizados que ≥`min_usuarios` users distintos corrigieron a la misma cat. Excluye los que ya tienen regla global activa. Candidatos a promover a regla global."
          }
        },
        {
          "name": "POST /reglas (regla global)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{API_KEY}}"
              },
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"scope\": \"global\",\n  \"tipo\": \"contiene\",\n  \"valor\": \"BIGGIE\",\n  \"categoria_slug\": \"supermercado\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/reglas",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "reglas"
              ]
            }
          }
        },
        {
          "name": "POST /reglas (regla personal)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{API_KEY}}"
              },
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"scope\": \"usuario:{{usuario}}\",\n  \"tipo\": \"contiene\",\n  \"valor\": \"STRIPE\",\n  \"categoria_slug\": \"software\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/reglas",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "reglas"
              ]
            }
          }
        },
        {
          "name": "PATCH /reglas/:id",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{API_KEY}}"
              },
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"activo\": false\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/reglas/{{regla_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "reglas",
                "{{regla_id}}"
              ]
            }
          }
        },
        {
          "name": "DELETE /reglas/:id",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{API_KEY}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/reglas/{{regla_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "reglas",
                "{{regla_id}}"
              ]
            }
          }
        }
      ]
    },
    {
      "name": "categorizar",
      "description": "**Endpoint principal del servicio.** Cliente mobile manda movimientos aquí y recibe categoría.\n\n## Cascada\nOrden de resolución (primer hit retorna):\n1. Catálogo `bancard_id+codigo_comercio` — confianza heredada\n2. Patrones `regex` / `literal` (0.95) / `contiene` / `prefijo` (0.90)\n3. MCC mapeado (0.75)\n4. IA fallback Gemma async (cap 0.70) — response inmediato es `null`, cliente debe poll `/movimientos/:id`\n\n## Confianza < 0.70\n→ `requiere_revision: true` — mobile debería mostrar badge \"Verificar\".\n\n## Body\nAl menos uno requerido: `descripcion`, `nombre_comercio`, `nombre_bancard`, `mcc`.\nOpcionales: `bancard_id`, `codigo_comercio`, `monto`, `origen`, `batch_id`, `bypass_catalogo`.\n\n## Ejemplos abajo\n- regex hit — pega un patrón conocido (BIGGIE → supermercado)\n- solo MCC — sin descripción, solo código MCC\n- combustible — todos los campos (caso ideal con catálogo)\n- desconocido → IA — dispara fallback async\n- input inválido — 400 con detalle Zod",
      "item": [
        {
          "name": "POST /categorizar-movimiento (regex hit)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const res = pm.response.json();",
                  "if (res.movimiento_id) {",
                  "  pm.collectionVariables.set('movimientoId', res.movimiento_id);",
                  "  console.log('movimientoId =', res.movimiento_id);",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"descripcion\": \"COMPRA BIGGIE EXPRESS SUC. CENTRO\",\n  \"monto\": 50000\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/categorizar-movimiento",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "categorizar-movimiento"
              ]
            },
            "description": "**Caso típico:** descripción matchea patrón (`BIGGIE` → supermercado).\n\n**Espera:** `fuente: 'contiene'` o `'regex'`, confianza 0.90-0.95.\n\nTest script: cachea `movimiento_id` para usar en GET detalle + correccion."
          }
        },
        {
          "name": "POST /categorizar-movimiento (solo MCC)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"mcc\": \"5411\",\n  \"monto\": 12000\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/categorizar-movimiento",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "categorizar-movimiento"
              ]
            },
            "description": "**Caso:** banco solo envía MCC, sin descripción.\n\n5411 = Grocery Stores. Capa MCC matchea → categoría supermercado.\n\n**Espera:** `fuente: 'mcc'`, confianza 0.75."
          }
        },
        {
          "name": "POST /categorizar-movimiento (combustible)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"descripcion\": \"COPETROL ESTACION RUTA 2\",\n  \"nombre_bancard\": \"COPETROL\",\n  \"mcc\": \"5541\",\n  \"monto\": 250000\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/categorizar-movimiento",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "categorizar-movimiento"
              ]
            },
            "description": "**Caso ideal:** todos los campos disponibles. Pipeline elige el primer hit (patrón `COPETROL` o MCC 5541 → combustible)."
          }
        },
        {
          "name": "POST /categorizar-movimiento (manual con categoria_id)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"nombre_bancard\": \"GASTO MANUAL DESDE APP\",\n  \"monto\": 50000,\n  \"origen\": \"user123\",\n  \"categoria_id\": \"{{categoriaId}}\",\n  \"aprender\": false\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/categorizar-movimiento",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "categorizar-movimiento"
              ]
            },
            "description": "**Caso:** usuario crea un gasto manualmente en la app y ya elige la categoría al cargarlo. El backend NO corre la cascada — guarda el movimiento con la categoría dada como `fuente='manual'`, `confianza=1.0`.\n\n**Body:**\n- `categoria_id` (UUID, opcional pero key acá) — si presente, saltea pipeline.\n- `aprender` (boolean, opcional, default `false` cuando hay categoria_id):\n  - `true` (junto con `origen`): además del mov, guarda regla user-scope para que próximos movs con el mismo nombre devuelvan esta categoría automático.\n  - `false`: sólo este movimiento.\n\n**Espera:** `fuente: 'manual'`, `confianza: 1.0`."
          }
        },
        {
          "name": "POST /categorizar-movimiento (con subcategoria personal)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"nombre_comercio\": \"NETFLIX MAYO\",\n  \"monto\": 50000,\n  \"origen\": \"{{usuario}}\",\n  \"subcategoria_usuario_id\": \"{{subcategoriaUsuarioId}}\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/categorizar-movimiento",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "categorizar-movimiento"
              ]
            },
            "description": "**Caso:** mov con subcategoría personal del usuario (ej user creó \"Streaming\" anclada al rubro \"Entretenimiento\").\n\n**Body:**\n- `subcategoria_usuario_id` (opt, UUID): backend valida pertenencia al `origen` y resuelve la canónica padre. Override silencioso de `categoria_id` si se pasaran ambos.\n\n**Resultado en DB:**\n- `movimientos.categoria_id` = canónica padre (Entretenimiento)\n- `movimientos.subcategoria_usuario_id` = subcat user (Streaming)\n\nReports Mango siguen rolando al rubro canónico."
          }
        },
        {
          "name": "POST /categorizar-movimiento (desconocido → IA async)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"descripcion\": \"XYZ COMERCIO RANDOM\",\n  \"monto\": 30000\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/categorizar-movimiento",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "categorizar-movimiento"
              ]
            },
            "description": "**Caso:** descripción no matchea nada. Cascada agotada → response inmediato `categoria_id: null`, IA dispara async.\n\n**Mobile UX:** mostrar \"categorizando...\", poll `GET /movimientos/:id` cada 2-3s hasta `categoria_id !== null` (timeout 15s)."
          }
        },
        {
          "name": "POST /categorizar-movimiento (input inválido → 400)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"monto\": 100\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/categorizar-movimiento",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "categorizar-movimiento"
              ]
            },
            "description": "**Caso error:** body sin ningún campo de texto/MCC requerido.\n\n**Espera 400:** `{\"error\":\"invalid_input\", \"issues\":{...}}` con detalle Zod."
          }
        }
      ]
    },
    {
      "name": "movimientos",
      "description": "Detalle, corrección y reprocesamiento de movimientos previamente categorizados.\n\n- **GET /movimientos/:id** — polling de IA fallback. Devuelve evidencia completa (prompt IA, response).\n- **POST /movimientos/:id/correccion** — usuario corrige categoría desde mobile. Audit trail en `correcciones_usuario`.\n- **POST /movimientos/:id/reprocesar** — re-ejecuta cascada + IA sobre movimiento existente. Útil para movimientos viejos sin categoría o cuando cambian patrones/MCCs.",
      "item": [
        {
          "name": "GET /movimientos (lista paginada)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/movimientos?limit=50&offset=0",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "movimientos"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "50"
                },
                {
                  "key": "offset",
                  "value": "0"
                },
                {
                  "key": "origen",
                  "value": "demo",
                  "disabled": true,
                  "description": "Filtro por origen (ej demo, mobile, api)."
                }
              ]
            },
            "description": "Lista paginada de movs. Cada item incluye `categoria` embebida y `subcategoria` (si aplica)."
          }
        },
        {
          "name": "GET /movimientos/:id",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/movimientos/{{movimientoId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "movimientos",
                "{{movimientoId}}"
              ]
            },
            "description": "Detalle del movimiento.\n\nUsar para:\n- Polling tras IA fallback (categoria_id pasa de null → uuid cuando Ollama termina)\n- Inspeccionar `evidencia` (prompt IA, candidatos, scores)\n- Ver `subcategoria` poblada (si el user asignó subcat personal)\n\n`movimientoId` se setea automáticamente al ejecutar el primer POST /categorizar-movimiento."
          }
        },
        {
          "name": "GET /movimientos/:id/categorias-sugeridas",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/movimientos/{{movimientoId}}/categorias-sugeridas?limit=5",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "movimientos",
                "{{movimientoId}}",
                "categorias-sugeridas"
              ],
              "query": [
                {
                  "key": "q",
                  "value": "",
                  "disabled": true,
                  "description": "Texto libre; si presente, usa este en vez del enriquecido de la cat origen."
                },
                {
                  "key": "limit",
                  "value": "5"
                },
                {
                  "key": "offset",
                  "value": "0",
                  "disabled": true
                },
                {
                  "key": "umbral",
                  "value": "0",
                  "disabled": true
                }
              ]
            },
            "description": "Top-K categorías similares (trigram). Útil para UI \"¿quisiste decir X?\" antes de aplicar corrección."
          }
        },
        {
          "name": "POST /movimientos/importar",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"rows\": [\n    { \"nombre\": \"PETROBRAS CENTRO\", \"mcc\": \"5541\", \"monto\": \"50000\" }\n  ],\n  \"batchId\": \"import-001\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/movimientos/importar",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "movimientos",
                "importar"
              ]
            },
            "description": "Import async (chunked). Devuelve `importId` inmediato; worker corre cascada por row. Status via `/movimientos/importar/status`."
          }
        },
        {
          "name": "GET /movimientos/importar/status",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/movimientos/importar/status",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "movimientos",
                "importar",
                "status"
              ]
            }
          }
        },
        {
          "name": "POST /movimientos/:id/correccion",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"categoria_id_nueva\": \"{{categoriaId}}\",\n  \"motivo\": \"corrección manual de prueba\",\n  \"usuario\": \"{{usuario}}\",\n  \"aprender\": true,\n  \"subcategoria_usuario_id\": null\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/movimientos/{{movimientoId}}/correccion",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "movimientos",
                "{{movimientoId}}",
                "correccion"
              ]
            },
            "description": "Corrección manual desde mobile.\n\n**Body:**\n- `categoria_id_nueva` (UUID, required) — id de la categoría correcta. Cliente lo obtiene del cache de `GET /categorias`.\n- `motivo` (string, opcional) — feedback opcional.\n- `usuario` (string, opcional) — id del usuario que corrige. Determina el scope de la regla aprendida.\n- `aprender` (boolean, opcional, default `true`) —\n  - `true` (default): crea/actualiza regla user-scope con prio 1. Próximos movs del mismo nombre devuelven la categoría corregida automáticamente.\n  - `false`: sólo modifica este movimiento. Excepción puntual sin contaminar memoria del usuario. Útil cuando el comercio en general tiene categoría correcta pero este mov específico es distinto (ej estación de servicio donde sólo compraste shop, no combustible).\n\nEl audit en `correcciones_usuario` se inserta siempre, así que las sugerencias cross-user (`GET /reglas/sugerencias-globales`) siguen funcionando.\n\n**Response 200:**\n```json\n{\n  \"correccion_id\": \"uuid\",\n  \"categoria_anterior_id\": \"uuid|null\",\n  \"categoria_anterior\": { \"id\": \"uuid\", \"slug\": \"...\", \"nombre\": \"...\" },\n  \"categoria_nueva_id\": \"uuid\",\n  \"categoria_nueva\": { \"id\": \"uuid\", \"slug\": \"...\", \"nombre\": \"...\" }\n}\n```\n\nVariables: usa `{{categoriaId}}` (auto-seteada de GET /categorias) y `{{movimientoId}}` (auto-seteada de POST categorizar)."
          }
        },
        {
          "name": "POST /movimientos/:id/reprocesar",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{}"
            },
            "url": {
              "raw": "{{baseUrl}}/movimientos/{{movimientoId}}/reprocesar",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "movimientos",
                "{{movimientoId}}",
                "reprocesar"
              ]
            },
            "description": "Re-ejecuta cascada (catalogo → patrones → MCC) + IA fallback sobre un movimiento existente.\n\n**Casos de uso:**\n- Movimientos viejos creados antes de agregar patrones/MCCs nuevos\n- Movimientos que quedaron con `categoria_id: null` por IA fallida (ej. Ollama down al momento de POST inicial)\n- Cliente mobile permite \"re-categorizar este movimiento\"\n\n**Body (opcional):**\n```json\n{\n  \"bypass_catalogo\": false\n}\n```\n- `bypass_catalogo` (bool) — si true, salta capa 1 → fuerza cascada pura (testing).\n\n**Response 200:**\n```json\n{\n  \"movimiento_id\": \"uuid\",\n  \"categoria_id\": \"uuid | null\",\n  \"categoria\": { \"id\": \"...\", \"slug\": \"azar\", \"nombre\": \"Azar y apuestas\" } | null,\n  \"fuente\": \"regex | contiene | ... | null\",\n  \"confianza\": 0.95,\n  \"requiere_revision\": false,\n  \"ia_disparada\": true\n}\n```\n\n**`ia_disparada: true`** → cascada agotada, IA corriendo async. Cliente debe poll `GET /movimientos/:id` para ver categoría final.\n\n**Errores:**\n- 400 `invalid_id` — id no es UUID\n- 404 `no_existe` — movimiento no encontrado"
          }
        }
      ]
    },
    {
      "name": "categorias-usuario",
      "description": "Subcategorías personales del usuario. Cada subcat ancla a un rubro canónico (categoría global Mango) y agrupa movimientos del usuario bajo un nombre custom. Reports internos siguen agrupando por canónica.\n\nModelo:\n- `categorias` (canónicas): curadas por Mango, ~30-100 entries, inmutables para users.\n- `categorias_usuario`: cada user crea las suyas (cap 200), siempre con `canonica_id` no-null.\n\n**FK semantics:**\n- `movimientos.categoria_id` → canónica (rubro). Siempre presente cuando el mov está categorizado.\n- `movimientos.subcategoria_usuario_id` → subcat user (opcional). ON DELETE SET NULL.\n- `correcciones_usuario.subcategoria_usuario_id` → preserva elección de subcat en historial.",
      "item": [
        {
          "name": "GET /categorias-usuario?usuario=demo",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const res = pm.response.json();",
                  "if (res.items && res.items.length > 0) {",
                  "  pm.collectionVariables.set('subcategoriaUsuarioId', res.items[0].id);",
                  "  console.log('subcategoriaUsuarioId =', res.items[0].id);",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/categorias-usuario?usuario={{usuario}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "categorias-usuario"
              ],
              "query": [
                {
                  "key": "usuario",
                  "value": "{{usuario}}"
                }
              ]
            },
            "description": "Lista subcategorías activas del usuario. Cada item incluye datos de la canónica padre (rubro):\n\n**Response 200:**\n```json\n{\"items\":[{\n  \"id\":\"uuid\",\n  \"usuario_id\":\"demo\",\n  \"canonica_id\":\"uuid\",\n  \"canonica_slug\":\"entretenimiento\",\n  \"canonica_nombre\":\"Entretenimiento\",\n  \"nombre\":\"Streaming\",\n  \"slug\":\"streaming\",\n  \"emoji\":\"🎬\",\n  \"color\":null,\n  \"activo\":true,\n  \"origen\":\"manual\",\n  \"created_at\":\"...\"\n}]}\n```"
          }
        },
        {
          "name": "POST /categorias-usuario (crear subcat)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const res = pm.response.json();",
                  "if (res.id) pm.collectionVariables.set('subcategoriaUsuarioId', res.id);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"usuario\": \"{{usuario}}\",\n  \"canonica_id\": \"{{categoriaId}}\",\n  \"nombre\": \"Streaming\",\n  \"emoji\": \"🎬\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/categorias-usuario",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "categorias-usuario"
              ]
            },
            "description": "Crea subcategoría personal anclada a una canónica.\n\n**Body:**\n- `usuario` (req): id del user.\n- `canonica_id` (req, UUID): rubro padre. Debe ser activa y no reemplazada.\n- `nombre` (req, 1-80): no puede ser igual al de la canónica.\n- `slug` (opt): auto-generado del nombre si no se provee.\n- `emoji` (opt): icono mostrado en UI.\n- `color` (opt): color hex/css.\n\n**Errores:**\n- 400 `canonica_inactiva` — canónica inactiva o reemplazada.\n- 400 `nombre_igual_canonica` — nombre coincide con la canónica padre.\n- 409 `slug_duplicado` — ya existe subcat con ese slug para el user.\n- 429 `cap_alcanzado` — el user llegó al máx (200) de subcats activas."
          }
        },
        {
          "name": "PATCH /categorias-usuario/:id",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"nombre\": \"Streaming \\u00b7 video\",\n  \"emoji\": \"📺\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/categorias-usuario/{{subcategoriaUsuarioId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "categorias-usuario",
                "{{subcategoriaUsuarioId}}"
              ]
            },
            "description": "Edita campos mutables: `nombre`, `emoji`, `color`, `activo`. No permite cambiar `usuario_id` ni `canonica_id` (para mover de rubro, borrar y recrear).\n\n`activo=false` = soft hide. Movs que la usan no se pierden — quedan asignados pero la subcat no aparece en listados."
          }
        },
        {
          "name": "DELETE /categorias-usuario/:id",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/categorias-usuario/{{subcategoriaUsuarioId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "categorias-usuario",
                "{{subcategoriaUsuarioId}}"
              ]
            },
            "description": "Hard delete. FK `movimientos.subcategoria_usuario_id ON DELETE SET NULL`: movs que la usaban quedan sólo con la canónica padre, sin perder historial. El display en la UI cae al chip de la canónica.\n\nAlternativa: PATCH `activo=false` para hide sin borrar (preserva subcat para reuso si después cambian de opinión)."
          }
        }
      ]
    },
    {
      "name": "descripciones",
      "description": "Autocomplete per-user de descripciones. Lookup btree prefix, scope estricto user. Latencia objetivo p99 < 50ms.",
      "item": [
        {
          "name": "GET /descripciones/sugerencias",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{API_KEY}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/descripciones/sugerencias?usuario={{usuario}}&q=alq&limit=10",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "descripciones",
                "sugerencias"
              ],
              "query": [
                {
                  "key": "usuario",
                  "value": "{{usuario}}"
                },
                {
                  "key": "q",
                  "value": "alq"
                },
                {
                  "key": "limit",
                  "value": "10"
                },
                {
                  "key": "categoria_id",
                  "value": "",
                  "disabled": true,
                  "description": "Opcional. UUID de cat para boostear ranking."
                }
              ]
            },
            "description": "Autocomplete: devuelve top-K descripciones que el usuario tipeó antes y empiezan con `q`.\n\n**Params:**\n- `usuario` (req) — id del usuario final\n- `q` (req) — prefix mín 2 chars, máx 200\n- `limit` (opt, 1-20, default 10)\n- `categoria_id` (opt UUID) — boost descripciones con cat top igual\n\n**Response 200:**\n```json\n{\n  \"usuario\": \"user_123\",\n  \"q\": \"alq\",\n  \"limit\": 10,\n  \"items\": [\n    { \"descripcion\": \"alquiler\", \"freq\": 8, \"categoria_slug\": \"hogar\" },\n    { \"descripcion\": \"alquiler departamento\", \"freq\": 5, \"categoria_slug\": \"hogar\" }\n  ]\n}\n```\n\nScope estricto per-user: otro usuario nunca ve estas descripciones. Items se generan automáticamente cada vez que el user llama a `/categorizar-movimiento` con `descripcion`."
          }
        }
      ]
    },
    {
      "name": "presupuestos",
      "description": "Topes mensuales por categoría (canónica). Modelo versionado: editar = INSERT nueva versión `vigente_desde`. Baja = INSERT monto 0 (preserva histórico).",
      "item": [
        {
          "name": "GET /presupuestos?usuario=demo",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/presupuestos?usuario={{usuario}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "presupuestos"
              ],
              "query": [
                {
                  "key": "usuario",
                  "value": "{{usuario}}"
                }
              ]
            },
            "description": "Lista presupuestos vigentes del usuario. Excluye los dados de baja (monto=0)."
          }
        },
        {
          "name": "POST /presupuestos",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"usuario\": \"{{usuario}}\",\n  \"categoria_id\": \"{{categoriaId}}\",\n  \"monto_mensual\": 1500000\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/presupuestos",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "presupuestos"
              ]
            },
            "description": "Crea presupuesto mensual.\n\n**Errores:**\n- 400 `sin_categoria_no_admite_presupuesto`\n- 404 `categoria_no_encontrada`\n- 409 `presupuesto_ya_existe` — para cambiar el monto, usar PATCH."
          }
        },
        {
          "name": "PATCH /presupuestos/:id",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"monto_mensual\": 2000000\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/presupuestos/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "presupuestos",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": ""
                }
              ]
            },
            "description": "Editar monto = INSERT nueva versión `vigente_desde=hoy`. Versiones previas se preservan para reports pasados."
          }
        },
        {
          "name": "DELETE /presupuestos/:id",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/presupuestos/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "presupuestos",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": ""
                }
              ]
            },
            "description": "Baja = INSERT versión con `monto_mensual=0`. No borra histórico — meses pasados siguen mostrando tope real."
          }
        },
        {
          "name": "GET /presupuestos/estado?mes=YYYY-MM",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/presupuestos/estado?usuario={{usuario}}&mes=2026-05",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "presupuestos",
                "estado"
              ],
              "query": [
                {
                  "key": "usuario",
                  "value": "{{usuario}}"
                },
                {
                  "key": "mes",
                  "value": "2026-05",
                  "description": "default = mes actual (YYYY-MM)"
                }
              ]
            },
            "description": "Combina tope vigente del mes con gastos reales. Devuelve `items` con `{categoria_id, categoria_slug, presupuesto, gastado, restante, pct, movs}`. Útil para UI tipo \"X% usado · restan Y\"."
          }
        }
      ]
    },
    {
      "name": "chat",
      "description": "Chat IA contextual. Proxy a OpenRouter (modelos free, fallback chain). Backend monta prompt con resumen de movs + historial. Si server no tiene `OPENROUTER_API_KEY`, devuelve 503.",
      "item": [
        {
          "name": "POST /chat",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"usuario\": \"{{usuario}}\",\n  \"messages\": [\n    { \"role\": \"user\", \"content\": \"¿En qué categorías gasto más este mes?\" }\n  ],\n  \"movs\": [\n    { \"id\": \"m1\", \"nombre\": \"NETFLIX\", \"monto\": -50000, \"fecha\": \"2026-05-01\", \"categoria\": \"entretenimiento\" }\n  ]\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/chat",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "chat"
              ]
            },
            "description": "Body:\n- `messages`: array de `{role, content}` (user/assistant/system). Last 10 turns recommended.\n- `movs`: contexto de movs (top ~60). Cada uno `{id, nombre, monto, fecha, categoria}`.\n- `usuario`: id user.\n\nResponse: `{text: string}`."
          }
        }
      ]
    },
    {
      "name": "mia",
      "description": "Agente conversacional MIA. La lógica determinística (resolver + orquestador + guardrail) vive en `@mango/mia-core`; estos endpoints son el LlmPort (transporte LLM). Requieren OPENAI_API_KEY/OPENROUTER_API_KEY en el server. Ver HANDOFF_MIA.md.",
      "item": [
        {
          "name": "POST /mia/entender",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"messages\": [\n    { \"role\": \"user\", \"content\": \"poné un tope de 500 mil en supermercado\" }\n  ]\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/mia/entender",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "mia",
                "entender"
              ]
            },
            "description": "Paso 1 (ENTENDER). Body: `{messages}`. Response: `{intent, widget, categoria, comercio, accion, monto, model}`. 503 sin LLM key."
          }
        },
        {
          "name": "POST /mia/redactar",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"messages\": [\n    { \"role\": \"user\", \"content\": \"¿cuánto gasté este mes?\" }\n  ],\n  \"hechos\": \"Gasto por categoría: Supermercado=Gs 1.200.000, Servicios=Gs 150.000. Total Gs 1.350.000.\",\n  \"conTarjeta\": true\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/mia/redactar",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "mia",
                "redactar"
              ]
            },
            "description": "Paso 3 (REDACTAR). Body: `{messages, hechos, conTarjeta}`. La prosa usa SOLO `hechos` (anti-alucinación). Response: `{text, model}`."
          }
        },
        {
          "name": "POST /mia-agent",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"usuario\": \"{{usuario}}\",\n  \"messages\": [\n    { \"role\": \"user\", \"content\": \"¿cuánto gasté este mes?\" }\n  ],\n  \"movs\": [\n    { \"id\": \"m1\", \"nombre\": \"SUPERSEIS\", \"monto\": -1200000, \"fecha\": \"2026-06-01\", \"categoria\": \"supermercado\" }\n  ],\n  \"hechos\": \"Total Gs 1.200.000 en Supermercado.\",\n  \"conTarjeta\": true\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/mia-agent",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "mia-agent"
              ]
            },
            "description": "Combinado legacy (ENTENDER+REDACTAR). Body: `{messages, movs?, usuario?, contexto?, hechos?, conTarjeta?}`. Response: `{text, intent, widget, action, model}`."
          }
        },
        {
          "name": "GET /mia/bandeja",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/mia/bandeja?usuario={{usuario}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "mia",
                "bandeja"
              ],
              "query": [
                {
                  "key": "usuario",
                  "value": "{{usuario}}"
                }
              ]
            },
            "description": "Tarjetas que esperan acción. Response: `{resumen, tarjetas[]}`."
          }
        },
        {
          "name": "POST /mia/confirmar",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"movimiento_ids\": [\"{{movimientoId}}\"],\n  \"categoria_id\": \"{{categoriaId}}\",\n  \"usuario\": \"{{usuario}}\",\n  \"aprender\": true\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/mia/confirmar",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "mia",
                "confirmar"
              ]
            },
            "description": "Confirma categoría sobre 1+ movs (aprende regla user-scope salvo `aprender:false`)."
          }
        },
        {
          "name": "POST /mia/descartar",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"movimiento_ids\": [\"{{movimientoId}}\"]\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/mia/descartar",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "mia",
                "descartar"
              ]
            },
            "description": "Descarta sugerencia / revierte auto-categorización. Response: `{descartados}`."
          }
        },
        {
          "name": "GET /mia/auto-recientes",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/mia/auto-recientes?usuario={{usuario}}&desde_horas=24",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "mia",
                "auto-recientes"
              ],
              "query": [
                {
                  "key": "usuario",
                  "value": "{{usuario}}"
                },
                {
                  "key": "desde_horas",
                  "value": "24"
                }
              ]
            },
            "description": "Lo que MIA auto-categorizó (aviso 'categoricé N')."
          }
        },
        {
          "name": "GET /mia/insights",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/mia/insights?usuario={{usuario}}&max=3",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "mia",
                "insights"
              ],
              "query": [
                {
                  "key": "usuario",
                  "value": "{{usuario}}"
                },
                {
                  "key": "max",
                  "value": "3"
                }
              ]
            },
            "description": "Insights por movimiento (gasto alto, recurrente, ingreso, cerca/exceso de tope)."
          }
        },
        {
          "name": "POST /mia/resumen",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"balance\": 3520000,\n  \"gastos_mes\": 1480000,\n  \"ingresos_mes\": 5000000,\n  \"meta_mensual\": 1000000,\n  \"ahorrado\": 520000,\n  \"meta_pct\": 52\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/mia/resumen",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "mia",
                "resumen"
              ]
            },
            "description": "Saludo de asesor para la home. Response: `{titulo, frase}` (null = sin LLM)."
          }
        },
        {
          "name": "POST /mia/conversar",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"usuario\": \"{{usuario}}\",\n  \"mensaje\": \"confirmá la categoría de SUPERSEIS\",\n  \"historial\": []\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/mia/conversar",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "mia",
                "conversar"
              ]
            },
            "description": "Lenguaje natural → acción sobre la bandeja (confirmar/descartar) o aclaración."
          }
        }
      ]
    },
    {
      "name": "stats",
      "description": "Estadísticas agregadas del pipeline.",
      "item": [
        {
          "name": "GET /stats/pipeline?ventana=24h",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/stats/pipeline?ventana=24h",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "stats",
                "pipeline"
              ],
              "query": [
                {
                  "key": "ventana",
                  "value": "24h",
                  "description": "1h | 24h | 7d | 30d | all"
                }
              ]
            },
            "description": "Distribución por capa pipeline, agreement IA, latencias p50/p95/p99, revisiones pendientes, correcciones aplicadas."
          }
        }
      ]
    },
    {
      "name": "mcc",
      "description": "CRUD del catálogo MCC (ISO 18245).",
      "item": [
        {
          "name": "GET /mcc",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/mcc?categoria=&sin_categoria=false",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "mcc"
              ],
              "query": [
                {
                  "key": "categoria",
                  "value": "",
                  "description": "Filtro opcional por slug.",
                  "disabled": true
                },
                {
                  "key": "sin_categoria",
                  "value": "false",
                  "disabled": true
                }
              ]
            }
          }
        },
        {
          "name": "POST /mcc",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"cod_mcc\": \"5812\",\n  \"descripcion\": \"Restaurantes\",\n  \"categoria_slug\": \"restaurante\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/mcc",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "mcc"
              ]
            }
          }
        },
        {
          "name": "PATCH /mcc/:cod_mcc",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"categoria_slug\": \"alimentacion\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/mcc/:cod_mcc",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "mcc",
                ":cod_mcc"
              ],
              "variable": [
                {
                  "key": "cod_mcc",
                  "value": "5812"
                }
              ]
            }
          }
        },
        {
          "name": "DELETE /mcc/:cod_mcc",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/mcc/:cod_mcc",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "mcc",
                ":cod_mcc"
              ],
              "variable": [
                {
                  "key": "cod_mcc",
                  "value": "5812"
                }
              ]
            }
          }
        }
      ]
    },
    {
      "name": "marcas",
      "description": "Marcas conocidas (texto exacto → cat). Curado Mango.",
      "item": [
        {
          "name": "GET /marcas",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/marcas",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "marcas"
              ]
            }
          }
        },
        {
          "name": "POST /marcas",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"marca\": \"NETFLIX\",\n  \"categoria_slug\": \"entretenimiento\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/marcas",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "marcas"
              ]
            }
          }
        }
      ]
    },
    {
      "name": "comercios",
      "description": "Listado paginado + reasignación de `mcc_por_nombre` (capa 2 del pipeline).",
      "item": [
        {
          "name": "GET /comercios",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/comercios?limit=50&offset=0",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "comercios"
              ],
              "query": [
                {
                  "key": "categoria",
                  "value": "",
                  "disabled": true
                },
                {
                  "key": "q",
                  "value": "",
                  "disabled": true
                },
                {
                  "key": "rev_only",
                  "value": "true",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "50"
                },
                {
                  "key": "offset",
                  "value": "0"
                }
              ]
            }
          }
        },
        {
          "name": "PATCH /comercios/:id",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"categoria_slug\": \"alimentacion\",\n  \"requiere_revision\": false\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/comercios/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "comercios",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": ""
                }
              ]
            }
          }
        }
      ]
    },
    {
      "name": "catalogo (import)",
      "description": "Import bulk a `mcc_por_nombre` (capa 2). Chunked async.",
      "item": [
        {
          "name": "POST /catalogo/importar",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"rows\": [\n    { \"nombre\": \"PETROBRAS CENTRO\", \"mcc\": \"5541\", \"categoria_slug\": \"combustible\" }\n  ],\n  \"correr_cascada\": false\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/catalogo/importar",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "catalogo",
                "importar"
              ]
            }
          }
        },
        {
          "name": "GET /catalogo/importar/status",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/catalogo/importar/status",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "catalogo",
                "importar",
                "status"
              ]
            }
          }
        }
      ]
    },
    {
      "name": "test-batch",
      "description": "Tests masivos del pipeline (in-process worker). Agreement contra ground truth.",
      "item": [
        {
          "name": "POST /test-batch/start",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"batchId\": \"smoke-001\",\n  \"limit\": 100,\n  \"concurrency\": 4\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/test-batch/start",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "test-batch",
                "start"
              ]
            }
          }
        },
        {
          "name": "GET /test-batch/list",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/test-batch/list",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "test-batch",
                "list"
              ]
            }
          }
        },
        {
          "name": "GET /test-batch/:batch_id/stats",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/test-batch/:batch_id/stats",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "test-batch",
                ":batch_id",
                "stats"
              ],
              "variable": [
                {
                  "key": "batch_id",
                  "value": "smoke-001"
                }
              ]
            }
          }
        },
        {
          "name": "GET /test-batch/:batch_id/agreement",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/test-batch/:batch_id/agreement?ground_truth=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "test-batch",
                ":batch_id",
                "agreement"
              ],
              "variable": [
                {
                  "key": "batch_id",
                  "value": "smoke-001"
                }
              ],
              "query": [
                {
                  "key": "ground_truth",
                  "value": ""
                }
              ]
            },
            "description": "Agreement pipeline vs `categoria_xlsx` de `test_ground_truth`."
          }
        },
        {
          "name": "GET /test-batch/:batch_id/analisis",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/test-batch/:batch_id/analisis",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "test-batch",
                ":batch_id",
                "analisis"
              ],
              "variable": [
                {
                  "key": "batch_id",
                  "value": "smoke-001"
                }
              ]
            },
            "description": "Distribución por fuente/cat, patrones más usados, top sin-predicción, latencia p50/p95/p99."
          }
        },
        {
          "name": "POST /test-batch/stop",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "content-type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"batch_id\": \"smoke-001\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/test-batch/stop",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "test-batch",
                "stop"
              ]
            }
          }
        }
      ]
    },
    {
      "name": "auth-fail (sin api-key → 401)",
      "request": {
        "auth": {
          "type": "noauth"
        },
        "method": "GET",
        "header": [],
        "url": {
          "raw": "{{baseUrl}}/categorias",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "categorias"
          ]
        },
        "description": "**Test negativo:** request sin header `x-api-key`.\n\n**Espera 401:** `{\"error\":\"unauthorized\"}`.\n\nÚtil para verificar auth funciona antes de configurar API_KEY real en producción."
      }
    }
  ]
}