Saltar a contenido

Requerimiento funcional — Frontend: productos compuestos (combos) en pedidos

Audiencia: instancia de Claude / dev que trabaje en frontend/ (Next.js 15, ver frontend/CLAUDE.md). Contexto de negocio y diseño: 01-requerimiento.md, 04-diseno-oms.md. Maqueta visual: 07-maqueta-ui.html (en _borradores/, no publicado) — wireframes de baja fidelidad de las 7 pantallas (abrir en el navegador). Este documento es autocontenido: incluye los contratos necesarios. El backend se implementa en paralelo según 05-plan-implementacion-backend.md.


1. Resumen

El OMS debe permitir vender productos compuestos (combos/kits): productos publicados en Algolia con product_type: "composite" y una lista components[]. En SAP se representan como Sales BOM: el pedido lleva una sola línea "padre" y SAP explota los componentes automáticamente. El combo puede no existir aún en SAP (drop-shipping): el frontend debe orquestar la verificación y, con confirmación del vendedor, su creación en SAP antes de agregarlo al pedido.

Reglas de negocio inviolables:

  • La receta (lista de componentes) es inmutable en el OMS. No se puede cambiar, quitar ni agregar componentes de un compuesto — ni en el picker ni en el pedido. La fuente de la verdad es Algolia, no SAP.
  • Los pedidos que contienen un combo no se editan (el backend rechaza el PATCH con 409). Para cambiar algo: cancelar y recrear el pedido.
  • El combo tiene precio propio (campo price del hit, IVA incluido); los componentes van con precio 0.

2. Contratos

2.1 Hit de Algolia (índice product_items_view_index / _dev)

Campos relevantes (hit real, recortado):

{
  "objectID": "17774",
  "name": "Notebook Gateway ... + estuche",
  "sku": "PCM-12167",              // SKU publicado del combo
  "sku_sap": "PCM-12167",          // ItemCode SAP si se conoce (puede faltar)
  "product_type": "composite",     // ← discriminador de combo
  "components": [
    { "product_item_id": 6205, "sku": "03357", "sku_sap": "03357", "name": "Notebook ...", "quantity": 1 },
    { "product_item_id": 1426, "sku": "00414", "sku_sap": "00414", "name": "MOUSE ...",    "quantity": 1 }
  ],
  "price": 3114430,                // precio final del combo, Gs. IVA incluido
  "total_stock": 1, "in_stock": true,
  "images": ["..."], "brand": "...", "category": "..."
}

2.2 Endpoints nuevos del backend

POST /items/composite/verify — body { "sku": "PCM-12167" }:

{
  "status": "ok" | "not_found" | "outdated",
  "sku": "PCM-12167",
  "itemCode": "09xxx",                    // ItemCode SAP del padre (si existe)
  "diff": {                                // solo si outdated
    "missing":  [{ "sku": "03357", "quantity": 1 }],
    "extra":    [{ "itemCode": "00999", "quantity": 1 }],
    "changed":  [{ "itemCode": "00414", "sapQty": 1, "publishedQty": 2 }]
  },
  "componentsToCreate": ["07110"]          // componentes que no existen como ítems en SAP
}

POST /items/composite/sync — body { "sku": "PCM-12167", "warehouseCode": "CEN" } (warehouseCode = almacén/sucursal del pedido en curso). Crea/actualiza en SAP el ítem padre, los componentes faltantes y la receta. Respuesta: mismo shape del verify, con status: "ok" e itemCode definitivo. Errores: 4xx con mensaje mostrable.

Ambos son idempotentes; sync puede tardar varios segundos (crea N ítems en SAP).

2.3 GET del pedido: jerarquía por TreeType

El backend expondrá TreeType en cada línea del pedido (GET /orders/:docEntry):

TreeType Significado
iSalesTree Línea padre del combo — lleva precio, impuesto y cantidad editable en SAP
iIngredient Componente explotado por SAP — precio 0, pertenece al padre que lo precede
iNotATree Línea normal

Las líneas iIngredient que siguen (en orden de LineNum) a una iSalesTree son sus componentes. Un pedido puede tener varios combos y líneas normales mezcladas.

3. Requerimientos funcionales

RF-1: Identificar combos en el buscador de productos

En ProductPicker (src/app/components/products/ProductPicker.tsx), los hits con product_type === "composite" se muestran con un distintivo visual (badge/chip "Combo") y, de ser posible, la lista de componentes visible (nombre × cantidad) en el detalle del hit antes de agregarlo.

Modelo: extender AlgoliaHit (ProductPicker.tsx:26) y Product (src/models/products.models.ts) con product_type? y components?.

RF-2: Verificación al seleccionar un combo

Al seleccionar un hit composite (hoy: mapHitToDocumentLine + onAdd, ProductPicker.tsx:187-239), en lugar de agregarlo directo:

  1. Llamar itemsApi.verifyComposite(sku) (nuevo método en src/utils/omsBackend/omsBackend.ts, patrón de itemsApi existente, línea ~401).
  2. Según status:
  3. ok → agregar la línea al pedido (RF-4). Sin fricción para el vendedor.
  4. not_found → modal: "Este combo aún no existe en SAP", lista de componentes (marcando los de componentsToCreate como "se crearán en SAP") y botón [Crear en SAP y agregar].
  5. outdated → modal: "La receta del combo cambió", mostrando el diff (missing / extra / changed) y botón [Actualizar en SAP y agregar].
  6. El botón de confirmación llama itemsApi.syncComposite(sku, warehouseCode) con estado de carga visible (puede tardar). Si responde ok → agregar la línea (RF-4). Si falla → mensaje de error del backend, no se agrega nada.
  7. Cancelar el modal = no se agrega nada al pedido.

El vendedor nunca puede editar la lista de componentes en estos modales.

RF-3: Línea de combo en el pedido en edición (pre-guardado)

  • Se agrega una sola línea al store (addOrderLine, src/store/order.store.ts:262): la del padre, con ItemCode = itemCode devuelto por verify/sync (NO el sku publicado: SAP autogenera los códigos), PriceAfterVAT = price del hit, cantidad 1, flag isComposite: true y components[] (informativos, del hit).
  • En la tabla de líneas (DesktopRow / MobileList, src/app/components/documents/), la línea padre se muestra expandible con sub-filas de componentes solo lectura (nombre, cantidad × cantidad del padre). Sin acciones de quitar/editar por componente.
  • Editable en la línea padre: cantidad y precio (como una línea normal). Quitar la línea padre quita el combo completo.
  • Los componentes NO se envían al backend: mapOrderToCreateDto (src/utils/mappers/order.mapper.ts:29) manda solo la línea padre. (SAP explota los componentes; enviarlos los duplicaría.)
  • Cuidado con _reassignLineNums (order.store.ts:50): los componentes no son líneas del store, así que no debe romper nada — verificar.

RF-4: Pedido guardado / vista de detalle

  • Al cargar un pedido (ordersApi.getByDocEntry), agrupar líneas por TreeType (§2.3): fila padre + sub-filas de componentes solo lectura, indentadas/agrupadas visualmente. Líneas iNotATree como siempre.
  • Mostrar por componente un indicador de stock (stock disponible ya se obtiene hoy por línea — getCurrentStockAndWarranty): si algún componente no tiene stock suficiente, señalizarlo (la factura fallará hasta que haya recepción de mercadería — modelo drop-shipping).

RF-5: Bloqueo de edición de pedidos con combos

Si un pedido cargado contiene alguna línea iSalesTree/iIngredient, la UI deshabilita la edición del pedido completo (botón editar/guardar oculto o disabled con tooltip "Los pedidos con combos no se editan — cancelar y recrear"). El backend igualmente rechaza el PATCH con 409; la UI debe manejar ese error con mensaje claro.

RF-6: Facturación

Al generar la factura desde el pedido (invoicesApi, líneas {BaseType: 17, BaseEntry, BaseLine}), incluir TODAS las líneas del pedido — padre + componentes + normales. Nunca filtrar las iIngredient (referenciar solo el padre falla en SAP). Si la factura falla por stock (negative inventory), mostrar el mensaje indicando qué componente no tiene stock.

4. Fuera de alcance

  • Editar/armar combos en el OMS (la receta viene de Algolia y es inmutable).
  • Sincronización proactiva de combos con SAP (solo bajo demanda con confirmación).
  • Crear componentes que no estén publicados en Algolia.
  • Manejo de compra/recepción de mercadería para el drop-shipping.

5. Criterios de aceptación

  1. Un combo publicado que ya existe y coincide en SAP se agrega al pedido sin fricción.
  2. Un combo inexistente en SAP muestra el modal, se crea con un clic y queda agregado.
  3. Un combo con receta desactualizada muestra el diff, se actualiza con un clic y queda agregado.
  4. El pedido guardado muestra el combo como padre + componentes solo lectura, con indicador de stock por componente.
  5. No existe ninguna vía en la UI para modificar componentes de un combo.
  6. Un pedido con combo no puede editarse desde la UI (y el 409 del backend se muestra con mensaje claro si ocurre).
  7. La factura generada desde un pedido con combo referencia todas las líneas.
  8. Un producto normal (product_type ausente/distinto) se comporta exactamente igual que hoy — cero regresiones en el flujo actual.

6. Datos de prueba

  • Backend dev apunta a SAP ZZ_COMPU_3: ítems TESTBOM-*, BOM ProductTrees('TESTBOM-PC') (4 componentes), TESTBOM-SSD libre para el caso "componente a crear", cliente C32857.
  • Hit composite real de referencia: sku PCM-12167 (§2.1).
  • Swagger del backend: /api-docs (endpoints composite tras la etapa B3 del plan backend).