Saltar a contenido

Diseño OMS: productos compuestos con alta bajo demanda

Estado: propuesta de diseño (2026-07-16). Basado en los hallazgos verificados de 02 y 03 y en la exploración del código actual del monorepo.


1. Decisiones de alcance

Tema Decisión
Edición de componentes de un combo en el pedido NO se permite. El precio del combo se calcula en otro sistema; modificar la receta en el pedido rompería ese precio.
Origen de los combos Sistema externo (integrador) que publica al índice de productos de Algolia con type: composite. No se sincroniza proactivamente con SAP.
Creación en SAP Bajo demanda (drop-shipping): al agregar el combo a un pedido, se verifica contra SAP y, con confirmación del vendedor, se crea/actualiza el ítem padre, sus componentes faltantes y el ProductTree.
Componentes inexistentes en SAP Se crean como ítems de inventario (mismo modelo drop-ship que ya usa processOrderLines para ítems simples).
Fuente de la verdad de la receta El índice de Algolia (lo publica el integrador), NO SAP. SAP se crea/actualiza desde Algolia vía sync; si SAP quedó desactualizado, se actualiza al vender — nunca al revés. La receta no se puede modificar desde el OMS.
Edición de pedidos que contienen combos NO se permite desde el OMS (decisión 2026-07-16). No se puede cambiar, quitar ni agregar componentes de un compuesto en el pedido. Esto elimina la incógnita del PATCH con líneas explotadas (03 §6).

2. Flujo de usuario

Vendedor busca producto en ProductPicker (Algolia)
  └─ hit.product_type === 'composite'
       └─ [async, al seleccionar] POST /items/composite/verify
            ├─ status: ok        → se agrega al pedido directo
            ├─ status: not_found → modal "El combo no existe en SAP" + detalle
            │                       de componentes (incl. los que también faltan)
            │                       + botón [Crear en SAP]
            └─ status: outdated  → modal con diff receta publicada vs SAP
                                    + botón [Actualizar en SAP]
                 └─ [confirmación] POST /items/composite/sync
                      └─ ok → se agrega la línea del combo al pedido

La verificación es liviana (2–3 GETs al SL en paralelo); la creación es la operación pesada y queda detrás de la confirmación explícita — no penaliza el caso común.

3. Contrato del hit de Algolia (fuente de la verdad)

Hit real del índice product_items_view_index (2026-07-16). Un compuesto se identifica por product_type: "composite" y trae su receta en components[]:

{
  "id": 17774,
  "objectID": "17774",                  // = id como string
  "name": "Notebook Gateway GWTC116-2BK ... + estuche",
  "sku": "PCM-12167",                   // SKU publicado del combo
  "sku_sap": "PCM-12167",               // ItemCode SAP (si ya se conoce)
  "product_type": "composite",          // ← discriminador; ítems simples: otro valor/ausente
  "components": [
    {
      "product_item_id": 6205,
      "sku": "03357",
      "sku_sap": "03357",               // puede no existir aún en SAP (drop-ship)
      "name": "Notebook Gateway GWTC116-2BK ... RAM 4GB ...",
      "quantity": 1
    },
    { "product_item_id": 14592, "sku": "07110", "sku_sap": "07110", "name": "ESTUCHE ...", "quantity": 1 },
    { "product_item_id": 1426,  "sku": "00414", "sku_sap": "00414", "name": "MOUSE ...",   "quantity": 1 }
  ],
  "price": 3114430,                     // precio final del combo (Gs., IVA incluido) → PriceAfterVAT
  "price_type": "REGULAR",              // REGULAR | oferta (ver on_sale/special_price)
  "regular_price": "3114430.00",
  "special_price": "0.00",
  "on_sale": false,
  "total_stock": 1,
  "in_stock": true,
  "supplier_id": 1,
  "brand": "Gateway",
  "category": "Notebooks",
  "images": ["https://imagedelivery.net/..."],
  "short_description": "<ul><li>...</li></ul>",
  "url": "/notebook-gateway-gwtc116-2bk-6205/variant_...",
  "updated_at": "2026-07-16 13:26:31",
  "created_at": 1784211830
}

Reglas derivadas:

  • Resolución de ItemCode SAP (combo y componentes): sku_sap primero; si falta, buscar por SupplierCatalogNo = sku (mismo orden que processOrderLines).
  • components[] es inmutable para el OMS: se usa para verificar/crear/actualizar en SAP, nunca se edita desde el pedido ni se reescribe en Algolia.
  • Precio del combo = price del hit (IVA incluido) → va como PriceAfterVAT en la línea padre. Confirmar con el integrador el caso oferta (on_sale/special_price).
  • quantity por componente ya viene en el hit — es la cantidad de la receta (ProductTreeLines[].Quantity).

4. Endpoints nuevos (backend)

Módulo sugerido: extender modules/items (o submódulo composite/).

POST /items/composite/verify

Entrada: { sku: string } — el backend resuelve la definición publicada consultando el índice de productos de Algolia por SKU (no confiar en la composición que mande el cliente). Nota: hoy el backend solo tiene cliente Algolia de clientes (services/algolia); hay que agregar el search del índice de productos, product_items_view_index.

Respuesta:

{
  "status": "ok" | "not_found" | "outdated",
  "sku": "COMBO-123",
  "itemCode": "TESTBOM-PC",        // ItemCode SAP si existe (puede diferir del sku publicado)
  "diff": {                         // solo si outdated
    "missing":  [{ "sku": "...", "quantity": 2 }],   // en receta publicada, no en SAP
    "extra":    [{ "itemCode": "...", "quantity": 1 }], // en SAP, ya no publicado
    "changed":  [{ "itemCode": "...", "sapQty": 1, "publishedQty": 2 }]
  },
  "componentsToCreate": ["sku-a", "sku-b"]  // componentes que no existen como Items en SAP
}

Comparación de recetas: por set (ItemCode, Quantity) entre ProductTreeLines y la composición publicada. Resolución de componente publicado → ItemCode SAP: mismo orden que processOrderLines (por ItemCode, luego por SupplierCatalogNo).

POST /items/composite/sync

Entrada: { sku: string } (misma resolución vía Algolia). Orquesta, en orden:

  1. Componentes faltantes → reutilizar el patrón de itemsService.create() (items.service.ts:48 + item.mapper.ts:131), que ya define la política de alta:
  2. ItemCode autogenerado por SAP con Series: 79 (serie interna); el SKU externo va en SupplierCatalogNo — NO se usa el SKU como ItemCode.
  3. Defaults actuales (STATIC_ITEM_DATA_FOR_CREATION, items.service.ts:29): ItemsGroupCode: 157, ItemType: itItems, InventoryItem/SalesItem: tYES, IndirectTax: tYES, ArTaxCode/ApTaxCode: IVA_10, TaxType: tt_Yes.
  4. ItemPrices: [{ PriceList: 1, Price: dto.Price ?? 0, Currency: 'GS' }] — el hit del combo no trae precio por componente; si se quiere cargar, buscar el componente en Algolia por su sku (no es relevante para la receta: los componentes explotan en 0).
  5. Agregar al payload (no está hoy): ItemWarehouseInfoCollection explícito — crítico, sin él el pedido del combo falla con el error opaco Internal error (-10) (03 §2).
  6. Ítem padre si no existe → mismo patrón (Series 79, SKU del combo en SupplierCatalogNo) pero con InventoryItem: tNO (no mueve stock jamás). TreeCode del BOM = ItemCode autogenerado del padre.
  7. BOM → si no existe: POST ProductTrees (TreeType: iSalesTree, HideBOMComponentsInPrintout: tNO); si existe y está desactualizado: PATCH ProductTrees con la lista completa de líneas y header B1S-ReplaceCollectionsOnPatch: true (03 §1 — sin header el SL hace merge/append).
  8. Devuelve el resultado de un verify final (status: ok + itemCode).

Idempotencia/concurrencia: dos vendedores pueden confirmar el mismo combo a la vez. Capturar el error de duplicado del SL (-2035) en cada POST y continuar como éxito; re-verificar al final.

5. Cambios en el flujo de pedido (backend)

Puntos de anclaje (exploración 2026-07-16):

  • orders.service.ts → processOrderLines() (línea ~66): hoy valida cada línea y crea el ítem si no existe. Cambios:
  • Si el ítem resuelto tiene TreeType: iSalesTree → es línea padre de combo: enviar solo esa línea (SAP explota los componentes; nunca enviar los hijos — se duplican, 02 §2) e incluir TaxCode explícito (02 §5, 03 §3) además de PriceAfterVAT (precio manual del combo, 02 §4).
  • El fallback "crear ítem si no existe" no aplica a un type: composite: el alta del combo pasa por /items/composite/sync con confirmación del vendedor, no silenciosamente en el POST del pedido. Si llega un pedido con un combo inexistente → error 409 con mensaje accionable ("verificar/crear el combo primero").
  • sapPayload de create() (línea ~255): agregar TaxCode al mapeo de líneas (solo se envía para líneas padre de combo; las normales siguen igual).
  • SapOrderLineData (interfaces): agregar TreeType para que el GET del pedido exponga la jerarquía a la UI (iSalesTree / iIngredient / iNotATree).
  • updateOrder() (línea ~359): rechazar la edición de pedidos que contengan líneas de combo (cualquier línea con TreeType ≠ iNotATree) — decisión de negocio (§1). Error 409 con mensaje claro; para cambiar algo se cancela y se recrea el pedido.
  • Facturación (invoices.service.ts → buildSapPayload(), línea ~612): sin cambios estructurales — ya referencia las líneas que manda el frontend con {BaseType, BaseEntry, BaseLine}. Regla: el frontend debe incluir todas las líneas del pedido (padre + iIngredient); referenciar solo el padre falla (02 §7). Anticipar en la UI la falla por stock: si un componente no tiene stock, la factura devuelve "Quantity falls into negative inventory" (03 §4) — flujo drop-ship: pedido → compra/recepción → factura.

6. Cambios en el frontend

  • Modelo de producto (models/products.models.ts): agregar product_type?: string y components?: { product_item_id, sku, sku_sap, name, quantity }[] según el contrato del índice (§3).
  • ProductPicker.tsx → mapHitToDocumentLine() (línea ~187): si el hit es composite, disparar el verify y el modal de confirmación antes del onAdd(line). La línea agregada al store es una sola (el padre); los componentes son informativos (vienen del hit / del verify).
  • order.store.ts: la línea del combo viaja como línea normal con un flag isComposite para la UI. Como solo se envía el padre, el _reassignLineNums actual no rompe nada al crear. Al cargar un pedido existente (GET), las líneas ya vienen explotadas de SAP con TreeType — agrupar por secuencia (un iSalesTree seguido de sus iIngredient).
  • DesktopRow / MobileList: fila padre con precio/cantidad editables (cantidad del padre escala los hijos en SAP, 02 §6) + sub-filas de componentes solo lectura (precio 0, sin acciones de quitar/editar). Mostrar indicador de stock por componente (anticipa la falla de facturación, 03 §4).
  • omsBackend.ts: itemsApi.verifyComposite(sku) y itemsApi.syncComposite(sku).
  • Factura desde pedido: al armar DocumentLines de la factura, incluir todas las líneas del pedido (ya es el comportamiento si se mapean todas; no filtrar las iIngredient).

7. Reglas duras (checklist de implementación)

  1. POST de pedido con combo: solo línea padre, con TaxCode + PriceAfterVAT.
  2. Alta de cualquier ítem vía SL: siempre ItemWarehouseInfoCollection explícito.
  3. Actualización de receta: siempre lista completa + B1S-ReplaceCollectionsOnPatch: true.
  4. Factura: referenciar todas las líneas del pedido.
  5. Combos nuevos: HideBOMComponentsInPrintout: tNO.
  6. Sync idempotente (tolerar -2035 y carreras).
  7. La receta viene de Algolia y es inmutable para el OMS: no cambiar, quitar ni agregar componentes de un compuesto — ni en el pedido ni en el índice. SAP se actualiza desde Algolia, nunca al revés.
  8. Pedidos que contienen combos: no editables desde el OMS (rechazar el PATCH).

8. Puntos abiertos

Resueltos el 2026-07-16: datos de alta de componentes y estrategia de ItemCode — se reutiliza la política existente de itemsService.create() (Series interna 79, SKU externo en SupplierCatalogNo, precio opcional buscándolo en Algolia por sku); ver §4 paso 1–2.

Los dos siguientes quedan diferidos a la etapa de implementación (decisión 2026-07-16: se resuelven in situ, donde se usen):

  1. Precio del combo en oferta: confirmar con el integrador si con on_sale: true el campo price ya refleja special_price o hay que elegirlo en el OMS.
  2. Almacén del combo/componentes: ¿siempre el del pedido/sucursal del vendedor? (el BOM guarda un almacén por línea, pero el del documento manda al explotar).

Plan de ejecución: 05-plan-implementacion-backend.md (backend) y 06-requerimiento-frontend.md (frontend).