Saltar a contenido

Plan de implementación — Backend

Base: 04-diseno-oms.md. Hallazgos: 02, 03. Datos de prueba en dev: ítems TESTBOM-*, BOM ProductTrees('TESTBOM-PC'), cliente C32857 (02 §8, 03 §5).

Estado (2026-07-17): B1–B5 IMPLEMENTADAS y verificadas. Build ✅, lint de los archivos nuevos ✅, 21 tests unitarios nuevos ✅ (las 3 fallas de orders.service.spec sobre autorización son preexistentes, igual que las 13 de items.service.spec — les falta el mock de CacheService; no son de esta feature). E2E real contra dev con el combo publicado PCM-12160: verify not_found → sync (creó componente 06558, padre 06559, BOM) → verify ok → POST Orders con línea única del padre → explosión correcta (padre iSalesTree precio 278.000, componente ×2 en 0, TaxCode propagado, sin error -10) → pedido 33266 cancelado. Ver "Decisiones in situ" al final.

Etapas en orden de dependencia; cada una compila, pasa lint/tests y es verificable por sí sola.


Etapa B1 — Cliente Algolia de productos

Hoy el backend solo consulta el índice de clientes. Se necesita leer el índice de productos (product_items_view_index / _dev) para que verify/sync usen la fuente de la verdad sin confiar en el payload del cliente.

  • backend/src/services/algolia/: agregar acceso search-only al índice de productos. Método principal: findProductBySku(sku) → busca por sku_sap con fallback a sku (mismo criterio que frontend/src/utils/algolia/fetchProductByLine.ts).
  • Config: nombre del índice por env (ALGOLIA_PRODUCTS_INDEX), siguiendo el patrón de indexing.config.ts. Credenciales search-only ya existentes en algolia.client.ts.
  • Tipado del hit según el contrato de 04 §3 (product_type, components[], price, …).

Verificación: unit test del servicio + llamada real en dev buscando un SKU conocido.

Etapa B2 — Extensión de itemsService.create()

items.service.ts:48 / item.mapper.ts:131. Sin romper el uso actual:

  • Aceptar overrides de defaults (para el padre: InventoryItem: 'tNO').
  • Soportar ItemWarehouseInfoCollection en el payload — siempre enviarlo en las altas de esta funcionalidad (03 §2: sin almacén asignado, el pedido del combo falla con Internal error (-10); no depender de la parametrización de empresa).
  • Se mantiene: Series 79 (ItemCode autogenerado), SKU externo en SupplierCatalogNo, ItemPrices lista 1.

Verificación: unit tests del mapper/payload.

Etapa B3 — Endpoints composite (verify / sync)

Submódulo en modules/items (p. ej. composite/): controller + service + DTOs.

  • POST /items/composite/verify { sku } → contrato de 04 §4: resuelve la definición en Algolia (B1), resuelve padre y componentes en SAP (ItemCode = sku_sap → fallback SupplierCatalogNo = sku), compara receta vs ProductTreeLines por set (ItemCode, Quantity), responde ok | not_found | outdated + diff + componentsToCreate.
  • POST /items/composite/sync { sku, warehouseCode } → orquesta: componentes faltantes (B2) → padre (B2, tNO) → POST ProductTrees o PATCH con lista completa
  • header B1S-ReplaceCollectionsOnPatch: true (03 §1) → verify final como respuesta.
  • HideBOMComponentsInPrintout: 'tNO' en BOMs nuevos.
  • Idempotente: tolerar -2035 (ya existe) en cada POST y continuar; carreras entre vendedores terminan en el mismo estado.
  • warehouseCode: lo manda el frontend (sucursal del pedido) — decisión in situ pendiente (04 §8); mientras tanto parámetro explícito.
  • Precio de componentes nuevos: opcionalmente buscar cada sku en Algolia para cargar ItemPrices; si no se encuentra, crear con precio 0 (no afecta la receta ni el combo).

Verificación: unit tests del diff; prueba manual completa contra dev vía Swagger: combo nuevo inventado sobre TESTBOM-* + TESTBOM-SSD como componente a crear/agregar.

Etapa B4 — Pedidos con combos

orders.service.ts / interfaces / DTOs:

  • SapOrderLineData (+ mapeo de respuesta): exponer TreeType por línea (iSalesTree / iIngredient / iNotATree) para que la UI arme la jerarquía.
  • processOrderLines() (línea ~66):
  • Ítem resuelto con TreeType: iSalesTree → línea padre de combo: pasa como línea única (los componentes NUNCA se envían — SAP los explota; enviarlos los duplica, 02 §2) y se le fuerza TaxCode explícito (IVA del combo; 02 §5 y 03 §3) junto con PriceAfterVAT.
  • Ítem inexistente en SAP cuyo hit de Algolia es product_type: composite → 409 con mensaje accionable ("verificar/crear el combo primero" — flujo verify/sync). El fallback actual de auto-creación silenciosa NO aplica a compuestos.
  • create() (mapeo de líneas, línea ~255): agregar TaxCode al payload (solo presente en líneas padre).
  • updateOrder() (línea ~359): si el pedido en SAP contiene alguna línea con TreeType !== 'iNotATree' → 409 ("los pedidos con productos compuestos no se editan; cancelar y recrear"). Decisión de negocio, 04 §1.

Verificación: unit tests (guard de update, procesamiento de línea padre) + manual en dev: pedido con TESTBOM-PC desde Swagger → 6 líneas, TaxCode e importes correctos; intento de PATCH → 409.

Etapa B5 — Facturación (defensivo, opcional)

invoices.service.ts → validateDocumentLines() / buildSapPayload() (~612): si las líneas referencian un pedido que contiene iSalesTree, validar que estén referenciadas todas las líneas de ese pedido (02 §7: referenciar solo el padre falla en SAP con error poco claro; mejor 400 propio con mensaje explícito). Recordar el caso stock 0: la factura falla con negative inventory hasta que haya recepción de mercadería (03 §4) — no se maneja en backend, solo se documenta para soporte.

Verificación: manual contra dev — factura del pedido de prueba de B4 (con stock) y factura parcial → 400 propio.

Etapa B6 — Cierre

  • npm run lint, npm run test, build.
  • Swagger: revisar documentación de los endpoints nuevos (/api-docs).
  • Actualizar estos docs con lo aprendido (decisiones in situ de 04 §8 que se hayan tomado: precio en oferta, almacén).
  • Coordinar despliegue con el frontend (06-requerimiento-frontend.md): el front depende de verify/sync y del TreeType en el GET del pedido.

Implementación realizada (2026-07-17)

Archivos nuevos:

Archivo Etapa Qué hace
src/config/products-search.config.ts B1 Config ALGOLIA_PRODUCTS_APP_ID/SEARCH_KEY/INDEX (opcionales al boot; sin ellas los endpoints composite responden 503)
src/services/algolia/products-search.service.ts (+module, +interfaz del hit, +spec) B1 findProductBySku(sku) search-only contra el índice del integrador
src/modules/items/composite/ (controller, service, dtos, interfaces, recipe-diff.ts + spec) B3 POST /items/composite/verify y /sync

Archivos modificados:

Archivo Etapa Cambio
src/app.module.ts B1 Registra el schema de validación nuevo
backend/.env B1 Variables ALGOLIA_PRODUCTS_* (espejo de las NEXT_PUBLIC_ALGOLIA_* del frontend)
items/mappers/item.mapper.ts + items.service.ts B2 create(dto, {sapOverrides, warehouseCodes}) → ItemWarehouseInfoCollection explícito
items/dtos/ItemDto.ts + interfaces/sap-item-data.interface.ts B4 Expone treeType del ítem
items/items.module.ts B3 Registra composite + importa ProductsSearchModule
orders/orders.service.ts B4 Línea padre iSalesTree → TaxCode forzado (salesIva ∥ IVA_10); 409 al alta silenciosa de combo publicado; 409 al editar pedidos con combos
orders/dtos/CreateOrderLineDto.ts + interfaces/sap-order-data.interface.ts B4 TaxCode en la línea; TreeType en la respuesta (la UI arma la jerarquía)
orders/orders.module.ts B4 Importa ProductsSearchModule
invoices/invoices.service.ts B5 validateCompositeKitLines: 400 propio si no se referencian todas las líneas del kit

Decisiones in situ (cierra 04 §8)

  1. Precio del combo: se usa hit.price del índice tal cual (va como PriceAfterVAT de la línea padre; también como precio de lista 1 del padre al crearlo). El caso oferta (on_sale/special_price) sigue pendiente de confirmar con el integrador — si price no reflejara la oferta, el único cambio es en createParentItem/frontend.
  2. Almacén: parámetro explícito warehouseCode en /sync (lo manda el frontend con la sucursal del pedido). Se usa para: ítems nuevos (ItemWarehouseInfoCollection), cabecera y líneas del BOM.
  3. findProductBySku usa filters: exactos por facet (sku_sap:"…" con prioridad, sku:"…" como fallback). Historial: hasta 2026-07-17 sku/sku_sap NO eran facets (el filtro devolvía vacío) y se resolvía por query + match en memoria; el 2026-07-20 se agregaron como facets de solo filtro y se migró al filtro directo (verificado contra el índice). Se mantiene la desambiguación: el índice tiene registros duplicados por SKU (la vista composite y un registro simple), se prefiere el que declara product_type.
  4. TaxCode del combo: salesIva del ítem con fallback IVA_10 (quirk dev: ArTaxCode no persiste, 03 §3).

Datos creados en dev por el E2E

06558 (componente, SupplierCatalogNo 08969), 06559 (padre del combo PCM-12160, no inventariable) + su ProductTree; pedido 33266 cancelado.

Pendiente (no bloquea el backend)

  • Confirmar con el integrador el caso on_sale (decisión 1).
  • Frontend: 06-requerimiento-frontend.md.
  • Los specs preexistentes rotos (autorización en orders, CacheService en items) — deuda ajena a esta feature.

Propuesta pendiente: optimizar el verify (análisis 2026-07-17)

El verify actual es lento porque resuelve cada componente publicado contra Items (1–2 GETs full-entity por componente, sin $select): un combo de 10 componentes genera ~12–22 llamadas al SL que, al viajar por la misma sesión B1, se encolan del lado del servidor → varios segundos por verificación.

Rediseño propuesto (sin cambiar el contrato del endpoint):

  1. Algolia: definición publicada (1 llamada, fuera de SAP).
  2. GET ProductTrees('sku_sap') directo — el TreeCode ES el ItemCode del padre; solo si da 404 (o no hay sku_sap) se consulta Items con $select mínimo para distinguir "ítem sin BOM" / "no existe" / resolver por SupplierCatalogNo.
  3. Comparar en memoria components[].sku_sap vs ProductTreeLines[] (el BOM ya trae ItemCode+Quantity; no hace falta pasar por Items).
  4. SOLO los componentes que no matchean (normalmente ninguno) se resuelven en una query batcheada: GET Items?$filter=(ItemCode eq 'a' or SupplierCatalogNo eq 'a' or ...)&$select=ItemCode,SupplierCatalogNo (patrón ya existente: findItemNamesByCodes).

Resultado: caso común ok pasa de ~12–22 llamadas SL a 2; outdated/ not_found a 3–4. El mismo camino acelera sync (hoy repite la resolución completa dos veces). Cambio acotado a composite.service.ts + un método de búsqueda batcheada en items.service.ts; tests del diff intactos; re-verificar con el E2E de PCM-12160.