Este dominio cubre cuatro entidades relacionadas que soportan manufactura, costeo y preparación de productos: materiales (insumos con costeo por lotes, útiles en producción), insumos (supplies, consumibles operativos como empaque o etiquetas), lista de materiales (BOM — qué materiales consume un producto al venderse o producirse) y modificadores (opciones de personalización para verticales de comida como Uber Eats, Rappi o DiDi Food).
No existe una entidad HTTP 'bundle'
El único mecanismo de composición producto-de-productos con endpoints propios es la lista de materiales (BOM), orientada a manufactura y consumo de inventario. Un "bundle" o kit de venta (bundleConfig) existe como un sub-objeto dentro del payload del producto (PUT /products/{id}, ver Actualizar un producto) — no tiene rutas /products/{sku}/bundle propias. Si buscas exponer/vender un kit compuesto por varios productos, es bundleConfig, no BOM.
Materiales, insumos, BOM y modificadores se persisten sobre el mismo modelo Product: un material es un Product con productType:'material' y su configuración específica en materialConfig; un insumo tiene productType:'supply' y supplyConfig; la lista de materiales vive en billOfMaterials de cualquier producto; los modificadores viven en modifiersConfig. No son colecciones separadas.
Los materiales viven en una colección dedicada, no en materialConfig
A diferencia de lo que sugiere el modelo conceptual (§ arriba), el código real de MaterialsService persiste los materiales en una colección materials dedicada (no en el documento Product con productType:'material' + materialConfig). Por eso el objeto que devuelven estos endpoints es plano: los campos (baseUnit, costing, stockAlerts, etc.) van al nivel raíz del documento, no anidados bajo materialConfig. La tabla de modelo de datos describe el campo embebido que sí existe en el schema de Product, pero no es lo que esta superficie REST retorna.
Estos tres endpoints permiten simular y ejecutar el consumo de materiales que un producto necesita al venderse o prepararse — típicamente invocados por el flujo de venta (POS, checkout) antes o durante la confirmación de un pedido.
Verifica disponibilidad de materiales para producir/vender una cantidad de un producto, sin consumir
productSkustringrequired
SKU del producto a evaluar (el que tiene BOM asociado).
locationIdstringrequired
Ubicación donde se verifica el stock de materiales.
quantitynumber
Cantidad del producto a producir/vender. Default 1.
selectedModifiersarray
Modificadores seleccionados, si el producto usa modificadores con consumo asociado: [{ groupId, modifierId, quantity? }].
Los tres endpoints de consumo llaman a MaterialConsumptionService.calculateConsumption, que lanza Product {productSku} not found cuando el producto no existe; el handler solo mapea a 404 los mensajes que contienen la subcadena "not found" — coincide en este caso, así que sí verás 404 not-found. Si el producto existe pero no tiene billOfMaterials ni modifiersConfig con consumo, la operación no falla: simplemente calcula sobre cero componentes (totalMaterials: 0).
Los insumos (supplies) son consumibles operativos — empaque, etiquetas, materiales de oficina — con la misma estructura CRUD que los materiales, pero sin costeo por lotes ni seguimiento de vencimiento. Igual que los materiales, SuppliesService persiste en una colección supplies dedicada con forma plana — no en Product.supplyConfig.
La BOM (Bill of Materials) define qué materiales consume un producto — ya sea al venderse (on_sale), al producirse (on_production), o solo cuando se dispara manualmente (manual). Vive en el campo billOfMaterials del producto mismo, no en una colección separada.
Obtiene la BOM de un producto
skustringrequired
SKU del producto.
404
{ "code": "not-found", "message": "Product or BOM not found" }
Permiso requerido:products:read
Asigna una BOM a un producto
skustringrequired
SKU del producto.
consumptionModestringrequired
'on_sale' | 'on_production' | 'manual'.
yieldobjectrequired
{ quantity, unit } — rendimiento de la receta/ensamble.
componentsarrayrequired
Al menos un componente: { materialSku, quantity, unit, required?(def true), sequence?, substitutes?[{materialSku,conversionFactor}], notes? }.
200, no 201 — y sin ApiResponse de error documentado
A pesar de ser la operación que crea la BOM, assignBOM responde 200 (no 201). Si algún materialSku de components no existe, BOMService lanza BOM validation failed: Material {sku} not found, pero el handler no tiene una rama que mapee ese mensaje a 400 — cae directo al catch genérico y responde 500internal-error, no un 400 de validación.
{ "code": "internal-error", "message": "Failed to consume BOM" }
Permiso requerido:products:update
No deduce inventario real ni valida stock insuficiente
BOMService.consumeComponentscalcula el costo y las cantidades como si se consumieran (usando el costo vigente de cada material), pero no descuenta inventario real — el propio código fuente lo marca explícito: "In production, this would actually consume from inventory using MaterialsService.consumeMaterial()". Por eso siempre responde success:true, incluso sin stock suficiente; no existe hoy un 400 insufficient-stock alcanzable en este endpoint. Además, si el producto no tiene BOM asignada, el servicio lanza Product {sku} has no BOM — ese mensaje no contiene la subcadena "not found" que el handler busca para mapear a 404, así que en ese caso verás 500internal-error, no 404.
Los modificadores implementan opciones de personalización estilo food-delivery (Uber Eats, Rappi, DiDi Food) — por ejemplo "sin cebolla" o "extra queso" — con ajuste de precio y, opcionalmente, consumo de materiales.
Obtiene la configuración de modificadores de un producto
{ "code": "not-found", "message": "Product not found" }
Permiso requerido:products:update
materialSku de entrada se guarda como sku
inventoryConsumption en el request usa el campo materialSku (objeto único). Al guardarse, ModifiersService.buildModifier lo convierte a un arreglo con el campo sku (no materialSku) — es la forma que verás en la respuesta y en GET /products/{sku}/modifiers. No asumas que el nombre de campo se conserva entre request y response.
Valida una selección de modificadores contra las reglas configuradas (por ejemplo, min/max de un grupo) y calcula el ajuste de precio y el consumo de materiales
skustringrequired
SKU del producto.
selectionsarrayrequired
[{ groupId, modifierIds:string[], quantities?: Record<modifierId, number> }] — groupId/modifierId son los `id` (UUID) generados por el servidor, no el `code`.
{ "code": "not-found", "message": "Product not found" }
Permiso requerido:products:delete
No elimina la configuración, solo la desactiva
Pese a su nombre y descripción, este endpoint invoca ModifiersService.toggleModifiers(tenantId, sku, false): pone modifiersConfig.enabled = false pero conservagroups[] intacto. Si luego vuelves a activar (POST /products/{sku}/modifiers con enabled:true), los grupos previos siguen ahí. Para borrar grupos de verdad usa DELETE /products/{sku}/modifiers/{groupId} por cada uno.
Elimina un grupo de modificadores específico
skustringrequired
SKU del producto.
groupIdstringrequired
`id` (UUID) del grupo — NO el `code`.
200
{ "success": true, "message": "Modifier group removed" }
404
{ "code": "not-found", "message": "Product or group not found" }
Permiso requerido:products:delete
groupId inexistente no da 404 — es un no-op silencioso
ModifiersService.removeModifierGroup solo valida que el producto exista y tenga modifiersConfig; si el groupId enviado no coincide con ningún grupo, hace filter() sobre el arreglo (que no quita nada) y aun así responde 200{success:true, message:"Modifier group removed"} — no hay forma de distinguir "grupo borrado" de "groupId que nunca existió" desde la respuesta.
bundleConfigno tiene endpoints propios. Se gestiona dentro de PUT /products/{id} (ver Actualizar un producto). Estructura: inventoryMode (calculated o reserved, default calculated) y components[] con al menos un elemento — productSku requerido, variantMode (fixed/selectable/any, default fixed), quantity (mínimo 1, default 1), required (default true).