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_sapprimero; si falta, buscar porSupplierCatalogNo = sku(mismo orden queprocessOrderLines). 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 =
pricedel hit (IVA incluido) → va comoPriceAfterVATen la línea padre. Confirmar con el integrador el caso oferta (on_sale/special_price). quantitypor 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:
- 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: ItemCodeautogenerado por SAP conSeries: 79(serie interna); el SKU externo va enSupplierCatalogNo— NO se usa el SKU como ItemCode.- 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. 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 susku(no es relevante para la receta: los componentes explotan en 0).- Agregar al payload (no está hoy):
ItemWarehouseInfoCollectionexplícito — crítico, sin él el pedido del combo falla con el error opacoInternal error (-10)(03 §2). - Ítem padre si no existe → mismo patrón (Series 79, SKU del combo en
SupplierCatalogNo) pero conInventoryItem: tNO(no mueve stock jamás).TreeCodedel BOM = ItemCode autogenerado del padre. - BOM → si no existe:
POST ProductTrees(TreeType: iSalesTree,HideBOMComponentsInPrintout: tNO); si existe y está desactualizado:PATCH ProductTreescon la lista completa de líneas y headerB1S-ReplaceCollectionsOnPatch: true(03 §1 — sin header el SL hace merge/append). - 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 incluirTaxCodeexplícito (02 §5, 03 §3) además dePriceAfterVAT(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/synccon 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"). sapPayloaddecreate()(línea ~255): agregarTaxCodeal mapeo de líneas (solo se envía para líneas padre de combo; las normales siguen igual).SapOrderLineData(interfaces): agregarTreeTypepara 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 conTreeType≠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): agregarproduct_type?: stringycomponents?: { 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 delonAdd(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 flagisCompositepara la UI. Como solo se envía el padre, el_reassignLineNumsactual no rompe nada al crear. Al cargar un pedido existente (GET), las líneas ya vienen explotadas de SAP conTreeType— agrupar por secuencia (uniSalesTreeseguido de susiIngredient).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)yitemsApi.syncComposite(sku).- Factura desde pedido: al armar
DocumentLinesde la factura, incluir todas las líneas del pedido (ya es el comportamiento si se mapean todas; no filtrar lasiIngredient).
7. Reglas duras (checklist de implementación)¶
- POST de pedido con combo: solo línea padre, con
TaxCode+PriceAfterVAT. - Alta de cualquier ítem vía SL: siempre
ItemWarehouseInfoCollectionexplícito. - Actualización de receta: siempre lista completa +
B1S-ReplaceCollectionsOnPatch: true. - Factura: referenciar todas las líneas del pedido.
- Combos nuevos:
HideBOMComponentsInPrintout: tNO. - Sync idempotente (tolerar
-2035y carreras). - 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.
- 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):
- Precio del combo en oferta: confirmar con el integrador si con
on_sale: trueel campopriceya reflejaspecial_priceo hay que elegirlo en el OMS. - 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).