Skip to content

Material & BOM

1. Tổng quan

Thuộc tínhGiá trị
IDFEAT-INV-MAT
StatusStable (recipes Beta — chỉ type KIT; MANUFACTURED Beta)
Ownerinventory-team
Phụ thuộcInventoryItem, InventoryStock, Category, UnitOfMeasure, KitchenTicketItem (sale)

Material là một loại principal riêng biệt với ProductVariant. Material được tracking trong inventory nhưng không bán trực tiếp — chúng tham gia vào stock thông qua MaterialRecipe (BOM). Khi một ProductVariant có recipe đang active, các material thành phần được reserve lúc sale-payment và tiêu thụ khi kitchen ticket đạt READY.

2. Mô hình Entity

Trường Material

TrườngKiểuBắt buộcMô tả
merchantIdtextOwner
identifiertextTự động, tiền tố MAT
slugtextURL-safe
namei18n jsonbHiển thị
descriptioni18n jsonb
statustextMaterialStatuses (mặc định ACTIVATED)
typetextMaterialTypes (mặc định RAW)
uomjsonbIUomRole{ base, purchase, sale } (xem ADR-0005)
costdecimal(15,4)Tham chiếu standard cost
weightdecimal(15,4)
categoryIdtextFK
metadata.inventory.allowOverselljsonb pathChỉ làm seed từ migration 0009_charming_armor. Đọc duy nhất lúc tạo InventoryStock lần đầu rồi copy sang InventoryStock.metadata.allowOversell; sửa giá trị sau khi stock đã seed sẽ KHÔNG ảnh hưởng runtime. Mặc định false. Toggle per-location qua PATCH /inventory-items/{id}/stocks/{stockId}.
metadata.inventory.isInventoryTrackedjsonb pathMặc định true (quy ước qua IMaterialMetadata)

Trường MaterialIdentifier

TrườngKiểuBắt buộcMô tả
materialIdtextFK
schemetextSYSTEM (tự động, tiền tố MAT) / SLUG / SKU / BARCODE / QRCODE
valuetextUnique theo (materialId, scheme)

Trường MaterialRecipe

TrườngKiểuBắt buộcMô tả
merchantIdtextOwner
principalTypetextMATERIAL (trung gian) / PRODUCT_VARIANT (bán được)
principalIdtextRecipe này tạo ra cái gì
statustextDRAFT / ACTIVATED / DEACTIVATED
typetextKIT (auto-trừ khi bán) / MANUFACTURED (cần ProductionOrder)
versionintTăng bởi updateAggregate

Trường MaterialRecipeItem

TrườngKiểuBắt buộcMô tả
materialRecipeIdtextFK tới MaterialRecipe
principalTypetextLoại component đa hình (MATERIAL hoặc PRODUCT_VARIANT)
principalIdtextFK target id
quantitydecimal(15,4)Lượng cần mỗi đơn vị principal
uomIdtextSoft ref
isOptionalbooleanMặc định false — khi true, thiếu component không block production

3. Vòng đời

MaterialRecipe

Chỉ recipe ACTIVATED mới được khám phá lúc runtime. Recipe DRAFT chỉ tồn tại phía editor. Recipe DEACTIVATED bị loại khỏi explosion.

Hành vi theo Type của Recipe

TypeTrigger để tiêu thụ materialVòng đời
KITSale payment success → kitchen ticket READYTự động — bung lúc runtime
MANUFACTUREDTạo + complete ProductionOrder thủ côngMaterial được tiêu thụ với PO; stock của output material được tăng

4. Vận hành

MaterialService (material.service.ts — 773 dòng)

Phương thứcSignatureMục đích
validateMaterialIds{ materialIds, merchantId, transaction? }Kiểm tra tồn tại + scope hàng loạt
createAggregate{ context, data: TCreateMaterialAggregateRequest }Material + identifier + (tùy chọn) seed InventoryItem + (tùy chọn) batch VendorItem
updateAggregate{ context, id, data: TUpdateMaterialAggregateRequest }Patch + grant/omit batch VendorItem
Lifecycle hooksbeforeCreate, afterCreateValidate category; tự tạo MaterialIdentifier (SYSTEM) + seed InventoryItem nếu isInventoryTracked !== false

MaterialRecipeService (material-recipe.service.ts — 624 dòng)

Phương thứcSignatureMục đích
createAggregate{ context, data }Recipe + items trong một TX
updateAggregate{ context, id, data }Patch + add/update/delete items qua sự hiện diện id
flattenRecipe{ principalId, principalType, maxDepth? }Bung BOM đa cấp → Material lá

MaterialWorkerService (material-worker.service.ts — 1386 dòng)

Phương thứcSignatureMục đích
handleKitchenTicketItemStatusChanged{ data: TKitchenTicketItemStatusChangedMessage }Kafka handler — tiêu thụ material khi status READY, restore khi VOIDED
reserveMaterialsForSaleOrder{ saleOrderId, merchantId, inventoryLocationId?, saleOrderItems }Gọi bởi InventoryWorkerService sau khi trừ product
deductMaterialForSaleOrder{ ... }Dùng bởi luồng kitchen consume

5. REST Endpoints

VerbPathAuthPermissionHandler
6× CRUD/materialsJWT/BASICMaterial.<crud>CRUD
GET/materials/overviewJWT/BASICMaterial.findMaterialService.getOverview
POST/materials/aggregateJWT/BASICMaterial.createAggregateMaterialService.createAggregate
PATCH/materials/:id/aggregateJWT/BASICMaterial.updateAggregateMaterialService.updateAggregate
6× CRUD/material-identifiersJWT/BASICMaterialIdentifier.<crud>CRUD
6× CRUD/material-recipesJWT/BASICMaterialRecipe.<crud>CRUD
POST/material-recipes/aggregateJWT/BASICMaterialRecipe.createAggregateMaterialRecipeService.createAggregate
PATCH/material-recipes/:id/aggregateJWT/BASICMaterialRecipe.updateAggregateMaterialRecipeService.updateAggregate
POST/material-recipes/:id/flattenJWT/BASICMaterialRecipe.flattenMaterialRecipeService.flattenRecipe

GET /materials/overview — Dashboard cho màn hình Materials List

Cấp số liệu cho 4 thẻ KPI ở màn hình Inventory > Materials List. Bốn metric được fan-out song song từ repository.

Query

TênBắt buộcMặc địnhMô tả
merchantIdPhải nằm trong merchant scope của user theo Casbin (admin / always-allowed bỏ qua).

Low-stock threshold không phải query input. Từ migration 0010_mellow_threshold đây là config inventory-domain, không còn là field của Material. Overview materials là metric per-material (SUM(available) vs một threshold) nên đọc default item-level tại InventoryItem.metadata.lowStockThreshold, COALESCE với default hệ thống 5. (Override per-location trên InventoryStock.metadata không áp dụng cho sum cross-bucket.) Seed qua inventory.lowStockThreshold lúc tạo material; sửa default sau đó qua PATCH /inventory-items/{id}.

Response — MaterialOverviewResponse

CardFieldNguồn dữ liệu
Total MaterialstotalMaterials.totalCOUNT(*) Material (theo merchant).
Total Materials ("Across N categories")totalMaterials.categoryCountCOUNT(DISTINCT category_id) trên cùng tập. Material có categoryId NULL không tính.
Active TrackingactiveTracking.totalCOUNT(*)metadata.inventory.isInventoryTracked không bị đặt rõ là false (giữ đúng default lúc create).
Low StocklowStock.totalMaterial có 0 < SUM(quantity_available) <= threshold trên toàn bộ bucket, với threshold = COALESCE((inv_item.metadata->>lowStockThreshold)::numeric, 5) — default item-level per-material.
Out Of StockoutOfStock.totalMaterial có SUM(quantity_available) = 0 (hoặc không còn stock row).
Overselloversell.totalMaterial có SUM(quantity_available) < 0 — âm; chỉ xảy ra khi material cho phép oversell. Tách biệt với Out Of Stock và Low Stock.

Authorization: dùng lại Material.find vì đây là read-only aggregate trên cùng rows. Service trả 403 khi merchant nằm ngoài scope.

Caching: response được cache trong cache-redis 60 giây, key theo (merchantId). Scope check chạy trước cache lookup nên không bao giờ trả cache entry cho caller ngoài scope. Lỗi Redis fall-through sang query trực tiếp — Redis là best-effort, không phải hard dependency.

6. Sự kiện

Inbound:

TopicTriggerHandlerHành vi
kitchen-ticket-item.status-changed (KafkaTopics.KITCHEN_TICKET_ITEM_STATUS_CHANGED)KitchenTicketItem.status đổi (READY / VOIDED)MaterialWorkerService.handleKitchenTicketItemStatusChangedKhi READY: trừ stock thành phần + ghi tracking. Khi VOIDED: restore.

Outbound:

TopicKhi nàoPayload
material.stock-changed (KafkaTopics.MATERIAL_STOCK_CHANGED)Sau khi mutate stock thành phần{ materialId, merchantId, inventoryStockId, quantityBefore, quantityAfter, delta, referenceType, referenceId }
material.transferred (KafkaTopics.MATERIAL_TRANSFERRED)Chuyển material cross-location{ materialId, fromLocationId, toLocationId, quantity, reason }

7. Luồng Bung BOM

8. Luồng Reservation (sau sale-payment)

Thời điểm reservation: ngay sau khi trừ stock product (trong InventoryWorkerService.handlePaymentSuccess). Reservation không tiêu thụ quantityOnHand — chỉ giảm quantityAvailable và tăng quantityReserved. Tiêu thụ thực sự xảy ra khi kitchen READY.

9. Idempotency

LuồngKey
Reserve (sale payment)(SALE_ORDER, saleOrderId, materialStockId)
Consume (kitchen READY)(KITCHEN_TICKET_ITEM, kitchenTicketItemId, materialStockId)
Restore (kitchen VOIDED)nghịch đảo của trên; tracking row được ghi với reason RESERVATION_RELEASE

10. Trang liên quan

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.