← Arquitectura por épica

FT-CAT-01 · Administración del árbol de categorías web

Arquitectura del Nuevo PIM por épica · corte del 18-oct-2026

Propuesta del TL: no sustituye la spec de cada épica

Actualizado el 18-oct-2026. Primera versión de la arquitectura de esta épica.

Qué responde. Cómo funciona por dentro FT-CAT-01: qué piezas participan, en qué orden hablan, qué APIs hay que construir con su request y su response, y qué le entrega a la épica siguiente.

Para quién. Los equipos que van a construir la épica, QA y arquitectura.

Estado. [Propuesta] del TL del Nuevo PIM, 18-oct-2026, construida sobre ADR-12. La spec de cada épica dice el qué; estas páginas, el cómo.

Fuente. docs/arquitectura/por-epica/epicas/FT-CAT-01.json, generada con generar.py; la ficha de la épica es trazabilidad/features/FT-CAT-01-administracion-arbol-categorias-web.md.

En 30 segundos

El árbol de categorías web es el menú de la tienda: los productos cuelgan de sus hojas. Hoy mover o dar de baja una rama exige exportar, borrar y recargar archivos. Esta épica deja a Category administrarlo en el portal: crear con un asistente que valida nivel y mínimos, aprobar antes de que viaje, mover ramas, desligar sin borrar ni dejar productos huérfanos, cargar en masa con revisión previa y traer el árbol vivo de P360.

  • Módulo dueño: MS-01 Catálogo. También participan: MS-02 Producto, MS-03 Migración y convivencia, MS-05 Auditoría, MS-09 Publicador de categorías, MS-13 Identidad y acceso.
  • Etapa C · Configura el catálogo. Recibe «PE-T · Usuario con permisos y bitácora» y entrega «PE-C · Catálogo vigente».
  • APIs: construye 11 y usa 14 que construyen otras épicas.
  • Eventos: publica 3 y consume 2.

1. Dónde encaja

Recibe

DeQué
PE-T · Usuario con permisos y bitácoraQuien entra ya se identificó, MS-13 dice si puede crear, mover o aprobar categorías y cada cambio queda en la bitácora de MS-05.
FT-USR-02 · Matriz de roles y permisos por módulo, acción, negocio y…Los perfiles de Category con sus capacidades: crear y cambiar, mover y desligar, y aprobar.
FT-ARQ-02 · Bitácora inmutable y versionado de producto, plantilla y…La bitácora por campo con versión, de la que sale la historia de cada categoría.
FT-REF-01 · Datos de referencia administrablesEl catálogo único de sitios (los 14 y el reservado UNLINKED) con su código de departamento y su ID de catálogo (API-01-046 a API-01-049).
FT-CAT-02 · Publicación de categorías a la tiendaEl acuse de la tienda por categoría y versión, con el que MS-01 actualiza el indicador de publicada.

Entrega

AQué
PE-C · Catálogo vigenteEl árbol vigente: cada categoría con su ID que no cambia, nivel calculado, sitio, estatus (Creada, Aprobada o Desligada), indicador de publicada y versión, legible por la API de MS-01.
FT-CAT-02 · Publicación de categorías a la tiendaCada categoría aprobada, cambiada, movida o desligada, con su objeto completo y su versión, por catalog.governance.changed.
FT-CAT-03 · Categorización de productosLas hojas activas y publicadas, la búsqueda por nombre y qué categorías son restringidas, con sus documentos, requisitos y si bloquean o avisan.
FT-CAT-06 · Agrupadores y corrección masiva de categorías con…Los productos que perdieron una categoría desligada, para corregirlos y republicarlos.
FT-RAC-07 · Reportes, exportes, importes y búsquedas por atributosEl árbol con niveles calculados y sin las desligadas, para los reportes y las descargas por nivel.

2. Diagrama de contexto

Quién participa y cómo se conecta. Las pantallas llegan por el BFF del portal, que valida el token y compone; cada módulo vuelve a validar el token y decide sus permisos con los roles de MS-13.

🟩 Verde: nuevo, se construye · 🟦 Azul: ya existe y se queda, se reutiliza su código · 🟧 Naranja: temporal o se retira (se apaga en R3) · ⬜ Gris: sistema externo · ★ módulo dueño

Sistemas externos

Nuevo PIM · un solo sistema

Pantallas y sistemas

Personas

Una rama del árbol
Crear una categoría
y 6 más

Roles efectivos de quien llama

CATEGORY_UPSERTED
CATEGORY_MOVED
y 1 más

CATEGORY_UPSERTED

CATEGORY_UPSERTED
CATEGORY_UNLINKED

Impacto de quitar categorías

Asignar plantilla o categorías web
Confirmar un plan de Producto

CATEGORY_UNLINKED

Lanzar una corrida de migración
Conciliación de una corrida
y 1 más

Lee los nodos vivos del árbol web

Carga de la migración

permisos

bitácora

Administración del catálogo

Entrada Única · Vendor Center

BFF del portal

★ MS-01 Catálogo

MS-02 Producto

MS-03 Migración y convivencia

MS-05 Auditoría

MS-09 Publicador de categorías

MS-13 Identidad y acceso

Base pim

Eventos

P360

Contexto de FT-CAT-01

3. Diagrama de flujo

El camino principal, paso por paso. Cuando una pantalla llama a un módulo se ven dos flechas: la pantalla al BFF y el BFF al módulo. Los eventos salen del outbox del módulo después de guardar; quien los consume ignora los repetidos.

MS-09 Publicador de categoríasMS-05 AuditoríaEventosBase·pimMS-13 Identidad y accesoMS-01 CatálogoEntrada Única · Vendor CenterMS-09 Publicador de categoríasMS-05 AuditoríaEventosBase·pimMS-13 Identidad y accesoMS-01 CatálogoEntrada Única · Vendor CenterAdministración del catálogoBFF1. Abre el árbol de su sitio y elige el padre2. Pide la rama con su estatus y si está publicadaGET /api/v2/categories/{id}/tree3. Valida la categoría nueva mientras la capturaPOST /api/v2/categories4. Revisa que quien llama pueda crear categoríasGET /internal/identity/permissions?userId=5. Guarda la categoría en estado CreadaPOST /api/v2/categories6. Genera el ID, calcula el nivel y guarda versión 17. Revisa la rama nueva y la aprueba8. Pide la vista previa de la aprobaciónPOST /api/v2/categories/approvals9. Aprueba las categorías de la ramaPOST /api/v2/categories/approvals10. Pasa a Aprobada y guarda el aviso juntos11. Avisa la categoría aprobada con su objeto completoCATEGORY_UPSERTED12. Entrega el cambio a la bitácoraCATEGORY_UPSERTED13. Entrega la categoría aprobada al publicadorCATEGORY_UPSERTEDAdministración del catálogoBFF
Flujo principal de FT-CAT-01
Los pasos en texto
PasoDeAQué haceAPI o evento
1Administración del catálogoEntrada Única · Vendor CenterAbre el árbol de su sitio y elige el padre—
2Entrada Única · Vendor CenterMS-01 Catálogo (por el BFF)Pide la rama con su estatus y si está publicadaAPI-01-038
3Entrada Única · Vendor CenterMS-01 Catálogo (por el BFF)Valida la categoría nueva mientras la capturaAPI-01-040
4MS-01 CatálogoMS-13 Identidad y accesoRevisa que quien llama pueda crear categoríasAPI-13-001
5Entrada Única · Vendor CenterMS-01 Catálogo (por el BFF)Guarda la categoría en estado CreadaAPI-01-040
6MS-01 CatálogoBase pimGenera el ID, calcula el nivel y guarda versión 1—
7Administración del catálogoEntrada Única · Vendor CenterRevisa la rama nueva y la aprueba—
8Entrada Única · Vendor CenterMS-01 Catálogo (por el BFF)Pide la vista previa de la aprobaciónAPI-01-201
9Entrada Única · Vendor CenterMS-01 Catálogo (por el BFF)Aprueba las categorías de la ramaAPI-01-201
10MS-01 CatálogoBase pimPasa a Aprobada y guarda el aviso juntos—
11MS-01 CatálogoEventosAvisa la categoría aprobada con su objeto completocatalog.governance.changed/CATEGORY_UPSERTED
12EventosMS-05 AuditoríaEntrega el cambio a la bitácoracatalog.governance.changed/CATEGORY_UPSERTED
13EventosMS-09 Publicador de categoríasEntrega la categoría aprobada al publicadorcatalog.governance.changed/CATEGORY_UPSERTED

3.1 Mover una rama o desligarla sin dejar huérfanos

MS-09 Publicador de categoríasEventosBase·pimMS-02 ProductoMS-01 CatálogoEntrada Única · Vendor CenterMS-09 Publicador de categoríasEventosBase·pimMS-02 ProductoMS-01 CatálogoEntrada Única · Vendor CenterAdministración del catálogoBFF1. Arrastra la rama a otro padre o a «unlinked»2. Pide la vista previa del movimientoPATCH /api/v2/categories/{id}/parent3. Pregunta qué productos quedarían sin categoría activaPOST /internal/product/classifications/impact4. Reasigna los productos que quedarían huérfanos5. Pide el plan de reasignar sus categoríasPOST /api/v2/products/classifications/plans6. Confirma la reasignaciónPOST /api/v2/product-plans/{planId}/apply7. Confirma el movimiento con la versión que revisóPATCH /api/v2/categories/{id}/parent8. Recalcula niveles o marca Desligado el subárbol9. Avisa el movimiento de la ramaCATEGORY_MOVED10. Si fue a «unlinked», avisa cada categoría desligadaCATEGORY_UNLINKED11. Entrega el desligue para quitar la relaciónCATEGORY_UNLINKED12. Entrega el desligue para mandar link=falseCATEGORY_UNLINKEDAdministración del catálogoBFF
Mover una rama o desligarla sin dejar huérfanos

3.2 Carga masiva con revisión previa

EventosBase·pimMS-02 ProductoMS-01 CatálogoEntrada Única · Vendor CenterEventosBase·pimMS-02 ProductoMS-01 CatálogoEntrada Única · Vendor CenterBFFAdministración del catálogo1. Baja la plantilla del perfil de categorías webGET /api/v2/catalog-imports/profiles/{p}/template2. Llena altas, cambios y cambios de padre por ID3. Sube el archivo y lo revisa sin escribirPOST /api/v2/catalog-imports4. Revisa huérfanos de los renglones que desliganPOST /internal/product/classifications/impact5. Confirma el lote completoPOST /api/v2/catalog-imports/{id}/apply6. Aplica renglón por renglón con su aviso7. Avisa cada categoría creada o cambiadaCATEGORY_UPSERTED8. Consulta el resultado por renglónGET /api/v2/catalog-imports/{id}9. Aprueba en lote las categorías nuevasPOST /api/v2/categories/approvalsBFFAdministración del catálogo
Carga masiva con revisión previa

3.3 Carga inicial del árbol desde P360

MS-01 CatálogoBase·pimP360MS-03 Migración y convivenciaEntrada Única · Vendor CenterMS-01 CatálogoBase·pimP360MS-03 Migración y convivenciaEntrada Única · Vendor CenterBFF1. Lanza la corrida de categorías de la revisión 1POST /api/v2/migration/runs2. Lee los nodos vivos del árbol web3. Aparta lo que no migra y lo que va a cuarentena4. Carga las categorías por su ID, sin avisosPOST /internal/governance/bulk-load/{coleccion}5. Guarda el nivel calculado y el ID único sin mayúsculas6. Revisa la conciliación contra los conteos de P360GET /api/v2/migration/reconciliation?runId=7. Revisa la cuarentena de nodos dudososGET /api/v2/migration/quarantineBFF
Carga inicial del árbol desde P360

4. Si algo sale mal

  • Se cuelga un nivel 3 de un nivel 1, se pasa del nivel 5 o un nivel 1 no trae isBrand, rootIndex o fechas. La validación responde con el campo que falla y nada se guarda; el asistente lo marca en la pantalla mientras se captura.
  • El ID manual ya existe, aunque sólo cambien mayúsculas. Responde 409 con CATEGORY_ID_ALREADY_EXISTS; un ID nunca se cambia ni se reutiliza, porque para la tienda sería otra categoría.
  • Desligar o mover a «unlinked» dejaría productos sin ninguna categoría web activa. La confirmación se bloquea y la vista previa lista esos productos; Category los reasigna (o Repoblamiento con FT-CAT-06) y vuelve a confirmar.
  • MS-02 no responde la revisión de productos. El movimiento o el desligue responde 503 y no se hace a ciegas; Category lo reintenta más tarde y nada queda a medias.
  • Otra persona cambió la rama entre la vista previa y la confirmación. La confirmación responde 409 VERSION_CONFLICT; la pantalla recarga la rama y se vuelve a revisar.

5. APIs

Las que construye esta épica (11)

GET /bff/v1/catalog/categories/{id} · Ficha de una categoría
CampoValor
APIAPI-BFF-111 · BFF del portal
Para quéArma la ficha de una categoría: sus datos, versión, número de modificaciones e indicador de publicada (MS-01), su historia por campo (MS-05) y el detalle de su último envío a la tienda, con intentos y último error (MS-09). Reemplaza la bitácora y el diagnóstico que hoy el portal pide al servicio de categorías y a M4.
En esta épicaLa ficha de una categoría con su versión, su historia por campo y el detalle de su último envío
Quién la llamaEntrada Única · Vendor Center (por el BFF)
PermisoPantalla de categorías
CabecerasAuthorization, X-Request-Id
Parámetrosid = catst10045, historySize = 20

Request

null

Response · 200

{
  "success": true,
  "data": {
    "category": {
      "id": "catst10045",
      "name": "Camisas de vestir",
      "level": 3,
      "brandSite": "LIVERPOOL",
      "status": "APROBADA",
      "version": 8,
      "modifications": 7
    },
    "history": [
      {
        "at": "2026-10-18T15:04:05Z",
        "actor": "Category (usuario interno)",
        "version": 8,
        "changes": [
          {
            "field": "name.es",
            "before": "Camisas",
            "after": "Camisas de vestir"
          }
        ]
      }
    ],
    "store": {
      "status": "PUBLICADO",
      "sentVersion": 8,
      "acknowledgedAt": "2026-10-18T15:04:06Z",
      "attempts": 1,
      "lastError": null
    }
  }
}

Errores

HTTPCódigoCuándo
404NOT_FOUNDLa categoría no existe
GET /api/v2/categories · Buscar categorías
CampoValor
APIAPI-01-035 · MS-01 Catálogo
Para quéBusca categorías del árbol web por sitio, padre, nombre, nivel, estatus o si ya están publicadas en la tienda, o sólo las raíces, con su nivel calculado, sus ancestros, si es hoja y si es restringida. Las desligadas sólo salen si se piden.
En esta épicaBuscar categorías por sitio, padre, nombre, nivel, estatus o si están publicadas
Quién la llamaBFF del portal, MS-02 Producto, MS-04 Reportes, MS-15 IA y enriquecimiento
PermisoENT-L; los módulos con su cuenta de servicio
CabecerasAuthorization, X-Request-Id
ParámetrosbrandSite = LIVERPOOL, parentId = catst10040, q = camisas, level = 3, status = APROBADA, published = True, leavesOnly = True, rootsOnly = False, page = 1, size = 50
También la usanFT-CAT-03

Request

null

Response · 200

{
  "success": true,
  "data": {
    "items": [
      {
        "id": "catst10045",
        "name": {
          "es": "Camisas de vestir"
        },
        "parentId": "catst10040",
        "ancestorIds": [
          "catst10001",
          "catst10040"
        ],
        "level": 3,
        "brandSite": "LIVERPOOL",
        "status": "APROBADA",
        "isLeaf": true,
        "restricted": false,
        "version": 8,
        "modifications": 7,
        "publication": {
          "status": "PUBLICADO",
          "version": 8,
          "at": "2026-10-18T15:04:06Z"
        },
        "startDate": "2026-01-01T06:00:00Z",
        "endDate": "2099-12-31T06:00:00Z"
      }
    ],
    "page": 1,
    "size": 50,
    "total": 1
  }
}

Errores

HTTPCódigoCuándo
400VALIDATION_ERRORUn filtro trae un valor que no existe (sitio, estado o nivel fuera de 1 a 5)
GET /api/v2/categories/{id} · Ver una categoría
CampoValor
APIAPI-01-036 · MS-01 Catálogo
Para quéDevuelve una categoría completa: los campos que viajan a la tienda, su nivel, su sitio con su ID de catálogo, su estatus (Creada, Aprobada o Desligada), su versión y su número de modificaciones, su indicador de publicada (qué versión confirmó la tienda) y, si es restringida, sus documentos, sus requisitos y si bloquea o avisa.
En esta épicaVer una categoría completa, con su restricción y su indicador de publicada
Quién la llamaBFF del portal, MS-02 Producto
PermisoENT-L
CabecerasAuthorization, X-Request-Id
Parámetrosid = catst10045
También la usanFT-CAT-03

Request

null

Response · 200

{
  "success": true,
  "data": {
    "id": "catst10045",
    "name": {
      "es": "Camisas de vestir",
      "en": "Dress shirts"
    },
    "parentId": "catst10040",
    "ancestorIds": [
      "catst10001",
      "catst10040"
    ],
    "level": 3,
    "brandSite": "LIVERPOOL",
    "catalogId": "ICatalog1000",
    "status": "APROBADA",
    "isLeaf": true,
    "startDate": "2026-01-01T06:00:00Z",
    "endDate": "2099-12-31T06:00:00Z",
    "groupType": "NOT_SPECIFIED",
    "sentToFA": true,
    "categoryImage": "/imagenes/categorias/catst10045.jpg",
    "imageUrl": "",
    "googleCategory": "Apparel & Accessories > Clothing > Shirts",
    "megaMenuContent": null,
    "properties": {
      "kioskId": null
    },
    "restriction": null,
    "publication": {
      "status": "PUBLICADO",
      "version": 8,
      "at": "2026-10-18T15:04:06Z"
    },
    "version": 8,
    "modifications": 7,
    "approvedAt": "2026-10-17T18:20:00Z",
    "updatedAt": "2026-10-18T15:04:05Z"
  }
}

Errores

HTTPCódigoCuándo
404NOT_FOUNDLa categoría no existe
GET /api/v2/categories/{id}/children · Hijas de una categoría
CampoValor
APIAPI-01-037 · MS-01 Catálogo
Para quéLista las hijas directas de una categoría, en el orden del menú, para abrir el árbol nivel por nivel.
En esta épicaAbrir el árbol nivel por nivel
Quién la llamaBFF del portal
PermisoENT-L
CabecerasAuthorization, X-Request-Id
Parámetrosid = catst10040, includeUnlinked = False, page = 1, size = 100
También la usanFT-CAT-03

Request

null

Response · 200

{
  "success": true,
  "data": {
    "items": [
      {
        "id": "catst10045",
        "name": {
          "es": "Camisas de vestir"
        },
        "level": 3,
        "status": "APROBADA",
        "isLeaf": true,
        "children": 0,
        "publication": {
          "status": "PUBLICADO",
          "version": 8
        }
      },
      {
        "id": "catst10046",
        "name": {
          "es": "Camisas de lino"
        },
        "level": 3,
        "status": "CREADA",
        "isLeaf": true,
        "children": 0,
        "publication": null
      }
    ],
    "page": 1,
    "size": 100,
    "total": 2
  }
}

Errores

HTTPCódigoCuándo
404NOT_FOUNDLa categoría padre no existe
GET /api/v2/categories/{id}/tree · Una rama del árbol
CampoValor
APIAPI-01-038 · MS-01 Catálogo
Para quéDevuelve una categoría con toda su rama hasta la profundidad pedida, validada (sin ciclos ni saltos de nivel) y sin truncar, con el estatus y el indicador de publicada de cada nodo para pintar el árbol.
En esta épicaVer una rama completa con el estatus de cada nodo
Quién la llamaBFF del portal, MS-04 Reportes
PermisoENT-L
CabecerasAuthorization, X-Request-Id
Parámetrosid = catst10040, depth = 2, includeUnlinked = False

Request

null

Response · 200

{
  "success": true,
  "data": {
    "id": "catst10040",
    "name": {
      "es": "Camisas"
    },
    "level": 2,
    "status": "APROBADA",
    "publication": {
      "status": "PUBLICADO",
      "version": 5
    },
    "children": [
      {
        "id": "catst10045",
        "name": {
          "es": "Camisas de vestir"
        },
        "level": 3,
        "status": "APROBADA",
        "publication": {
          "status": "PUBLICADO",
          "version": 8
        },
        "children": []
      },
      {
        "id": "catst10046",
        "name": {
          "es": "Camisas de lino"
        },
        "level": 3,
        "status": "CREADA",
        "publication": null,
        "children": []
      }
    ]
  }
}

Errores

HTTPCódigoCuándo
404NOT_FOUNDLa categoría no existe
400VALIDATION_ERRORLa rama pasa del límite de nodos de una respuesta: se pide por partes, nunca se trunca
GET /api/v2/categories/tree · Árbol de un sitio
CampoValor
APIAPI-01-039 · MS-01 Catálogo
Para quéDevuelve el árbol web de un sitio desde su nivel 1 hasta la profundidad pedida; con depth=1 da sólo los niveles 1, como la pantalla de hoy.
En esta épicaVer el árbol de un sitio
Quién la llamaBFF del portal, MS-04 Reportes, MS-15 IA y enriquecimiento
PermisoENT-L
CabecerasAuthorization, X-Request-Id
ParámetrosbrandSite = LIVERPOOL, depth = 1, includeUnlinked = False
También la usanFT-CAT-06, FT-RAC-07

Request

null

Response · 200

{
  "success": true,
  "data": {
    "brandSite": "LIVERPOOL",
    "catalogId": "ICatalog1000",
    "items": [
      {
        "id": "catst10001",
        "name": {
          "es": "Ropa"
        },
        "level": 1,
        "status": "APROBADA",
        "children": 14,
        "publication": {
          "status": "PUBLICADO",
          "version": 12
        }
      },
      {
        "id": "catst10002",
        "name": {
          "es": "Hogar"
        },
        "level": 1,
        "status": "APROBADA",
        "children": 22,
        "publication": {
          "status": "ERROR",
          "version": 9
        }
      }
    ]
  }
}

Errores

HTTPCódigoCuándo
400VALIDATION_ERROREl sitio no existe o la profundidad pasa de 5
POST /api/v2/categories · Crear una categoría
CampoValor
APIAPI-01-040 · MS-01 Catálogo
Para quéCrea una categoría en estado Creada bajo su padre: si no se manda el ID, el sistema lo genera (catst y un consecutivo), el nivel se calcula del padre y se revisan los mínimos de su nivel. Con ?dryRun=true sólo valida, para el asistente. No viaja a la tienda hasta que se aprueba.
En esta épicaValidar con dryRun y crear categorías en Creada
Quién la llamaBFF del portal
PermisoCAT-E (Category manager)
CabecerasAuthorization, X-Request-Id, Idempotency-Key
ParámetrosdryRun = False
Escribe encategories, id_sequences, governance_outbox
Publicacatalog.governance.changed/CATEGORY_UPSERTED

Request

{
  "parentId": "catst10040",
  "name": {
    "es": "Camisas de lino"
  },
  "startDate": "2026-11-01T06:00:00Z",
  "endDate": "2099-12-31T06:00:00Z",
  "groupType": "NOT_SPECIFIED",
  "sentToFA": true,
  "categoryImage": "/imagenes/categorias/camisas-lino.jpg"
}

Response · 201

{
  "success": true,
  "data": {
    "id": "catst10046",
    "parentId": "catst10040",
    "level": 3,
    "brandSite": "LIVERPOOL",
    "status": "CREADA",
    "isLeaf": true,
    "version": 1,
    "modifications": 0,
    "createdAt": "2026-10-18T15:04:05Z"
  }
}

Errores

HTTPCódigoCuándo
400VALIDATION_ERRORFalta un mínimo de su nivel (en nivel 1: isBrand, isBrandLanding, rootIndex y fechas), la fecha de fin es anterior a la de inicio o sentToFA viene fuera de los niveles 2 a 5
409ID_ALREADY_EXISTSEl ID manual ya existe, aunque sólo cambien mayúsculas (detalle CATEGORY_ID_ALREADY_EXISTS)
422BUSINESS_ERRORQuedaría en un nivel mayor que 5 o saltaría un nivel (un nivel 3 bajo un nivel 1)
404NOT_FOUNDEl padre no existe o está Desligado
403AUTHORIZATION_ERRORQuien llama no tiene la capacidad de crear categorías
PUT /api/v2/categories/{id} · Cambiar una categoría
CampoValor
APIAPI-01-041 · MS-01 Catálogo
Para quéCambia los campos de una categoría (nombre, fechas, banderas, imágenes, propiedades y la restricción con sus documentos, requisitos y efecto); el ID y el nivel no se capturan. Sube la versión y el número de modificaciones. Si ya está Aprobada, el cambio viaja a la tienda al guardarse; un campo vaciado se guarda vacío para que la tienda lo borre.
En esta épicaCambiar campos, fechas, imágenes y la restricción; vaciar un campo
Quién la llamaBFF del portal
PermisoCAT-E (Category manager)
CabecerasAuthorization, X-Request-Id, Idempotency-Key, If-Match
Parámetrosid = catst10045, dryRun = False
Escribe encategories, governance_outbox
Publicacatalog.governance.changed/CATEGORY_UPSERTED

Request

{
  "name": {
    "es": "Camisas de vestir"
  },
  "categoryImage": "",
  "endDate": "2027-01-31T06:00:00Z"
}

Response · 200

{
  "success": true,
  "data": {
    "id": "catst10045",
    "status": "APROBADA",
    "version": 8,
    "modifications": 7,
    "travelsToStore": true,
    "changes": [
      {
        "field": "name.es",
        "before": "Camisas",
        "after": "Camisas de vestir"
      },
      {
        "field": "categoryImage",
        "before": "/imagenes/categorias/catst10045.jpg",
        "after": ""
      },
      {
        "field": "endDate",
        "before": "2099-12-31T06:00:00Z",
        "after": "2027-01-31T06:00:00Z"
      }
    ]
  }
}

Errores

HTTPCódigoCuándo
409VERSION_CONFLICTAlguien cambió la categoría desde que se abrió
409NOT_EDITABLE_IN_STATUSEstá Desligada: primero se regresa a un árbol activo
400VALIDATION_ERRORSe intenta cambiar el ID o el nivel, la fecha de fin es anterior a la de inicio o falta un mínimo
PATCH /api/v2/categories/{id}/parent · Mover una rama
CampoValor
APIAPI-01-042 · MS-01 Catálogo
Para quéMueve una categoría con toda su rama bajo otro padre: conserva hijas y productos, recalcula el nivel de la rama y hereda el sitio del nuevo padre. Moverla al sitio «unlinked» la desliga con su subárbol; moverla de ahí a un padre activo la regresa. Con ?dryRun=true dice qué va a pasar, incluidos los productos que se quedarían sin categoría activa, sin mover nada.
En esta épicaMover una rama, mandarla a «unlinked» o regresarla a un árbol activo
Quién la llamaBFF del portal
PermisoCLA-E (Category manager)
CabecerasAuthorization, X-Request-Id, Idempotency-Key, If-Match
Parámetrosid = catst10040, dryRun = True
Escribe encategories, governance_outbox
Publicacatalog.governance.changed/CATEGORY_MOVED, catalog.governance.changed/CATEGORY_UNLINKED

Request

{
  "parentId": "catst10002"
}

Response · 200

{
  "success": true,
  "data": {
    "dryRun": true,
    "categoryId": "catst10040",
    "from": {
      "parentId": "catst10001",
      "level": 2,
      "brandSite": "LIVERPOOL"
    },
    "to": {
      "parentId": "catst10002",
      "level": 2,
      "brandSite": "LIVERPOOL"
    },
    "descendants": 37,
    "levelsRecalculated": 0,
    "products": 4210,
    "productsWithoutOtherActiveCategory": 0,
    "travelsToStore": true,
    "canConfirm": true
  }
}

Errores

HTTPCódigoCuándo
422BUSINESS_ERROREl nuevo padre es la misma categoría o una descendiente (ciclo), o la rama quedaría con más de 5 niveles o con un salto
422BUSINESS_ERRORVa a «unlinked» y algún producto se quedaría sin ninguna categoría web activa (se listan)
409VERSION_CONFLICTLa categoría cambió desde la vista previa
503DEPENDENCY_UNAVAILABLEMS-02 no respondió la revisión de productos: no se mueve a ciegas
DELETE /api/v2/categories/{id} · Desligar sin borrar
CampoValor
APIAPI-01-043 · MS-01 Catálogo
Para quéDesliga una categoría sin borrarla: ella y todo su subárbol pasan a Desligada, salen del árbol activo y de las descargas, y la tienda recibe link=false; los productos sólo pierden esa relación. Se bloquea si algún producto se quedaría sin ninguna categoría web activa, hasta reasignarlo. Con ?dryRun=true devuelve esos productos.
En esta épicaDesligar sin borrar, con la lista de productos que quedarían huérfanos
Quién la llamaBFF del portal
PermisoCLA-E (Category manager)
CabecerasAuthorization, X-Request-Id, Idempotency-Key, If-Match
Parámetrosid = catst10045, reason = Categoría de temporada que ya no se usa, dryRun = True
Escribe encategories, governance_outbox
Publicacatalog.governance.changed/CATEGORY_UNLINKED

Request

null

Response · 200

{
  "success": true,
  "data": {
    "dryRun": true,
    "categoryId": "catst10045",
    "subtree": 3,
    "products": 128,
    "orphans": {
      "total": 2,
      "items": [
        {
          "productId": "PRD-0004812",
          "name": "Camisa de lino manga larga",
          "business": "MER"
        },
        {
          "productId": "PRD-0004813",
          "name": "Camisa de lino manga corta",
          "business": "MER"
        }
      ]
    },
    "canConfirm": false
  }
}

Errores

HTTPCódigoCuándo
422BUSINESS_ERRORQuedarían productos sin ninguna categoría web activa; la respuesta los lista
409VERSION_CONFLICTLa categoría cambió desde la vista previa
409NOT_EDITABLE_IN_STATUSYa está Desligada
503DEPENDENCY_UNAVAILABLEMS-02 no respondió la revisión de productos: no se desliga a ciegas
POST /api/v2/categories/approvals · Aprobar categorías
CampoValor
APIAPI-01-201 · MS-01 Catálogo
Para quéAprueba de 1 a 500 categorías Creadas para que viajen a la tienda: revisa mínimos, sitio y que su padre ya esté aprobado o venga en el mismo lote. Con ?dryRun=true dice qué saldría y qué falta, sin aprobar nada. Al confirmar quedan Aprobadas y el aviso a MS-09 se guarda en la misma operación.
En esta épicaAprobar de 1 a 500 categorías para que viajen
Quién la llamaBFF del portal
PermisoCategory manager con la capacidad de aprobar categorías (MS-13)
CabecerasAuthorization, X-Request-Id, Idempotency-Key
ParámetrosdryRun = False
Escribe encategories, governance_outbox
Publicacatalog.governance.changed/CATEGORY_UPSERTED

Request

{
  "categoryIds": [
    "catst10045",
    "catst10046"
  ],
  "comment": "Rama de camisas para primavera"
}

Response · 200

{
  "success": true,
  "data": {
    "approved": 2,
    "blocked": 0,
    "items": [
      {
        "categoryId": "catst10045",
        "result": "APPROVED",
        "status": "APROBADA",
        "version": 2,
        "travels": "FULL"
      },
      {
        "categoryId": "catst10046",
        "result": "APPROVED",
        "status": "APROBADA",
        "version": 2,
        "travels": "FULL"
      }
    ]
  }
}

Errores

HTTPCódigoCuándo
422BUSINESS_ERRORUna categoría no tiene sus mínimos o su sitio, o su padre no está aprobado: no se aprueba nada del lote
409NOT_EDITABLE_IN_STATUSAlguna está Desligada o ya Aprobada sin cambios
403AUTHORIZATION_ERRORQuien llama no tiene la capacidad de aprobar categorías
400VALIDATION_ERRORMás de 500 categorías

Las que usa y construye otra épica (14)

Su request y su response están en la página de la épica que la construye.

APIMétodo y rutaPara qué, aquíLa construye
API-01-046GET /api/v2/brandsLeer los sitios con su código de departamento y su ID de catálogoFT-REF-01 · Datos de referencia administrables
API-01-030GET /api/v2/catalog-imports/profiles/{p}/templateBajar la plantilla del perfil de carga de categorías webFT-PLT-09 · Carga masiva de configuración de plantillas con…
API-01-031POST /api/v2/catalog-importsSubir y revisar sin escribir la carga masiva (lotes de hasta 500)FT-PLT-09 · Carga masiva de configuración de plantillas con…
API-01-032POST /api/v2/catalog-imports/{id}/applyConfirmar la carga masivaFT-PLT-09 · Carga masiva de configuración de plantillas con…
API-01-033GET /api/v2/catalog-imports/{id}Resultado de la carga por renglónFT-PLT-09 · Carga masiva de configuración de plantillas con…
API-01-053POST /internal/governance/bulk-load/{coleccion}Carga inicial por ID sin avisosFT-PLT-01 · Fundamentos de gobierno de plantillas
API-13-001GET /internal/identity/permissions?userId=Permisos efectivos de quien llamaFT-USR-01 · Identidad y autenticación de usuarios internos…
API-05-001GET /api/v2/audit/entities/{entityType}/{entityId}/eventsLa historia por campo que compone la ficha de la categoríaFT-ARQ-02 · Bitácora inmutable y versionado de producto, plantilla y…
API-02-016POST /api/v2/products/classifications/plansReasignar la categoría web de los productos que quedarían huérfanosFT-CAT-03 · Categorización de productos
API-02-057POST /api/v2/product-plans/{planId}/applyConfirmar la reasignaciónFT-FLU-01 · Motor de estados configurable con dos carreteras y reglas…
API-03-001POST /api/v2/migration/runsCorrida de migración de categoríasFT-MIG-01 · Perfilado y limpieza previa
API-03-003GET /api/v2/migration/reconciliation?runId=Conciliación contra los conteos de P360FT-MIG-02 · Migración de plantillas, atributos y listas de valores
API-03-004GET /api/v2/migration/quarantineCuarentena: nivel 6, IDs que sólo difieren en mayúsculas, huérfanosFT-MIG-03 · Migración inicial de productos desde P360
API-02-114POST /internal/product/classifications/impactQué productos quedarían sin categoría web activaFT-CAT-06 · Agrupadores y corrección masiva de categorías con…

6. Eventos

Publica

EventoCuándoQuién lo consume
catalog.governance.changed/CATEGORY_UPSERTEDSe crea, se cambia o se aprueba una categoría (lleva su estatus; sólo viaja la Aprobada)FT-ARQ-02, FT-CAT-02
catalog.governance.changed/CATEGORY_MOVEDUna rama cambia de padre o regresa de «unlinked» a un árbol activoFT-ARQ-02, FT-CAT-02
catalog.governance.changed/CATEGORY_UNLINKEDSe desliga una categoría: uno por cada categoría del subárbolFT-CAT-02, FT-CAT-06, FT-RAC-07

Consume

EventoPara quéQuién lo publica
catalog.governance.changed/CATEGORY_UPSERTEDMS-05 guarda el cambio por campo y MS-09 publica la categoría aprobadaesta misma épica
catalog.governance.changed/CATEGORY_UNLINKEDMS-02 quita la categoría de sus productos y MS-09 manda link=falseesta misma épica

7. Datos

Cada colección tiene un solo dueño, el único que escribe en ella.

ColecciónDueñoQué guardaEsta épica
categoriesMS-01Cada categoría web con su lugar en el árbol y su estado.escribe
id_sequencesMS-01El siguiente identificador de plantillas y catálogos.escribe
import_jobsMS-01Cargas masivas de configuración del catálogo, con revisión previa y resultado por fila.escribe
governance_receiptsMS-01El recibo de cada escritura confirmada en el catálogo (antes operation_receipts).escribe
governance_outboxMS-01Los cambios del catálogo que esperan salir como evento (antes outbox).escribe
productsMS-02La familia del producto con sus variantes, valores, imágenes (URL), clasificaciones, estado, tareas abiertas y rechazos.escribe
brandsMS-01Los sitios (banners): Liverpool, Suburbia y los demás.lee
taxonomiesMS-01Los árboles: plantillas, grupos de artículo y sitios web.lee

8. Historias que cubre

HistoriaPrioridadCómo la cubre
FT-CAT-01·H1 · Crear categorías con un asistente que valide nivel y mínimosP1pasos 1, 3, 4, 5, 6; API-01-040
FT-CAT-01·H2 · Desligar sin borrarP1flujo «Mover una rama o desligarla sin dejar huérfanos»; API-01-043
FT-CAT-01·H3 · Mover ramas y reasignar rutas sin exportar ni recargar archivosP1flujo «Mover una rama o desligarla sin dejar huérfanos»; API-01-042
FT-CAT-01·H4 · Crear y modificar en masa con validación previaP1flujo «Carga masiva con revisión previa»; API-01-030, API-01-031, API-01-032, API-01-033
FT-CAT-01·H5 · Cargar el árbol de la revisión 1 sin la deuda que se retireP1flujo «Carga inicial del árbol desde P360»; API-01-053, API-03-001, API-03-003, API-03-004
FT-CAT-01·H6 · Aprobar una categoría antes de que salga, y que sólo perfiles autorizados editenP2pasos 4, 7, 8, 9, 10; API-01-201, API-13-001
FT-CAT-01·H7 · Deslinquear como acción explícitaP1flujo «Mover una rama o desligarla sin dejar huérfanos»; API-01-043, API-01-042, API-01-035
FT-CAT-01·H8 · Sitio de categorías retiradasP1flujo «Mover una rama o desligarla sin dejar huérfanos»; API-01-042
FT-CAT-01·H9 · Ningún producto sin categoríaP1flujo «Mover una rama o desligarla sin dejar huérfanos»; API-02-114, API-02-016
FT-CAT-01·H10 · Modelo de la categoría webP1pasos 5, 6, 10; API-BFF-111, API-01-036

9. Decisiones de esta épica

Las tomó el TL como arquitecto; quedan como [Propuesta] hasta que el equipo de la épica las revise.

  • D-CAT-01-1. MS-01 es el único dueño del árbol (categories) y de los sitios, incluido el indicador de publicada de cada categoría, que actualiza con el acuse de MS-09; MS-09 no guarda categorías, sólo el detalle de cada envío. Las pantallas de hoy que llaman al servicio de categorías y a M4 pasan una sola vez al BFF en R1 y esas rutas se apagan con tráfico cero; las de /api/v1 no se conservan. Por qué: Hoy dos servicios escriben el mismo documento y se pisan el estado de publicación. Con un dueño, quien necesita «hoja publicada» (FT-CAT-03, FT-CAT-06, FT-INT-07) pregunta en un solo lugar, como ya fijaba contratos-api §3.1. Se apoya en ADR-12, PD-07, DUD-07.
  • D-CAT-01-2. Dos estatus, como acordó el taller: Creada (guardada, no viaja) y Aprobada (viaja); Desligada es un estado de vida aparte. Aprobar es una acción explícita, en lotes de hasta 500, con la capacidad de aprobar de MS-13; puede hacerla quien creó. Una categoría ya aprobada viaja con cada cambio que confirma un perfil autorizado: la vista previa de esa confirmación es su revisión. Por qué: Cumple «mientras no se aprueba no viaja» sin un segundo aprobador que nadie ha definido; pedir aprobación de cada cambio duplicaría trabajo y obligaría a guardar dos versiones de cada categoría. Se apoya en RF-CAT-19, DUD-01, DUD-22.
  • D-CAT-01-3. El ID lo genera MS-01 como catst y un consecutivo, con una secuencia que arranca arriba del mayor migrado; no cambia, no se reutiliza y se compara sin mayúsculas. El nivel se calcula del padre (1 a 5, sin saltos) y no se captura. Por qué: Un ID distinto es otra categoría para la tienda y uno reutilizado cuelga productos donde no van; el nivel capturado es la deuda de hoy (0 de 7,733 documentos con nivel). Se apoya en PD-20, RF-CAT-19.
  • D-CAT-01-4. Desligar, o mover a «unlinked», vale con hijas, con productos y desde el nivel 1: todo el subárbol pasa a Desligada, la tienda recibe link=false y los productos sólo pierden esa relación; nada se borra. Se bloquea sólo si algún producto se quedaría sin ninguna categoría web activa, hasta reasignarlo. Las fechas sólo controlan la visibilidad: una categoría vencida no se desliga sola ni pierde productos. Por qué: El negocio quiere proteger al producto, no al nodo. Desligar por fecha borraría las relaciones de las categorías de temporada que regresan cada año. Se apoya en PD-28, DMS-60.
  • D-CAT-01-5. Las categorías conservan la forma de comando directo con ?dryRun=true e If-Match que fija contratos-api §2.1, en vez de plan y confirmación: cada comando toca una categoría o una rama y su vista previa no se guarda. Lo masivo va por la carga de catálogo (API-01-031), que revisa todo antes de escribir y rechaza el lote si un renglón falla. API-01-045 no se construye: la historia la sirve MS-05 y la compone el BFF (API-BFF-111). Por qué: Respeta la ruta base sin duplicar caminos: un solo mecanismo de carga masiva para todo el catálogo y un solo dueño de la bitácora. Se apoya en ADR-12, DMS-61.
  • D-CAT-01-6. Carga inicial: se migra el árbol vivo de la revisión 1 con sus ID tal cual, incluidas las cat que tienen productos o están en árboles activos; no se migra lo que cuelga de Unlinked; los nodos de nivel 6, los pares que sólo difieren en mayúsculas y los huérfanos entran a la cuarentena de MS-03 para que Category decida antes del corte. La carga no publica: la tienda ya tiene esas categorías. Por qué:* Cambiar un ID rompería la tienda; la cuarentena deja visible la deuda sin frenar la carga. PD-20 sigue abierta y aquí se propone cómo cerrarla. Se apoya en PD-20, DEC-ARQ-07, RN-MIG-05.
  • D-CAT-01-7. Dentro del PIM las fechas son nativas y en UTC; el texto yyyy-MM-dd HH:mm:ss en hora de Ciudad de México sólo existe en el mensaje a la tienda, que arma MS-09. Por qué: El texto en la base es lo que hoy rompe los filtros por fecha de governance; el contrato con la tienda no cambia. Se apoya en DEC-ARQ-11, DUD-20.
  • D-CAT-01-8. El evento de categoría lleva el objeto completo (los campos que viajan, sitio e ID de catálogo ya resueltos) y la lista de cambios, como excepción a la regla de eventos sólo con identificadores. Por qué: Así MS-09 arma el mensaje sin leer la base de Catálogo, y una categoría pesa poco. Se apoya en ADR-12, PD-07.
  • D-CAT-01-9. La restricción es un dato de la categoría: documentos, requisitos y si bloquea o avisa. La administra Category con API-01-041 y MS-02 la aplica al ligar productos y al aprobarlos (FT-CAT-03). Por qué: Quien liga un producto necesita ver los requisitos en el momento; guardarlos en otro lado obligaría a reconciliar dos listas. Se apoya en FT-CAT-03, ADR-12.
  • D-CAT-01-10. La carga masiva de categorías usa la revisión y la confirmación de la carga de catálogo, pero no abre una solicitud de cambio: aplica como los comandos directos (las nuevas quedan Creadas) y Category las aprueba en lote con API-01-201. Por qué: Las categorías ya tienen su aprobación propia (D-CAT-01-2); una segunda aprobación por la solicitud del catálogo sería doble trabajo para lo mismo. Se apoya en RF-CAT-19, DEC-ARQ-12.

Qué falta decidir

IDQué falta
PD-20Ratificar con Category y Gobierno de Producto qué se migra, se congela o se retira del árbol heredado (propuesta en D-CAT-01-6).
PD-28Ratificar que la guarda protege al producto y no al nodo (D-CAT-01-4).
DUD-01Confirmar con Category si un cambio a una categoría ya aprobada necesita otra aprobación y quién aprueba.
DUD-22Cerrar con FT-USR-02 qué perfiles crean, mueven y aprueban categorías.
DUD-20Probar en QA el valor de rootIndex vacío y el borrado de un campo vaciado.

También siguen abiertos: PEN-40, Registrar que las rutas viven en el PIM: MS-01 es su dueño (ADR-12).

Fuentes

  1. [D] trazabilidad/features/FT-CAT-01-administracion-arbol-categorias-web.md: la ficha de la épica, con sus historias, reglas y bloqueos.
  2. [D] ADR-12 · Un solo sistema: backend compartido y una sola base (sección 3.4).
  3. [D] docs/arquitectura/por-epica/base/: piezas, colecciones, eventos y el hilo de punta a punta.
  4. [D] docs/arquitectura/omega-a-pim-headless/contratos-api.md: convenciones y endpoints de partida.
  5. [D] docs/arquitectura/maquina-de-estados-to-be/propuesta-maquina-de-estados.md §7: estados, tareas y canales.
  6. [I] Las decisiones de la sección 9 son del TL sobre esas fuentes.