Mô hình miền
Tất cả schema được định nghĩa trong
@nx/core/src/models/schemas/inventory/. Tên bảng dùng PascalCase. Cột số dùngstandardNumeric=decimal(15, 4).
1. ERD đầy đủ
2. Cột Chung
Mọi entity bên dưới đều thêm các cột này qua
generateCommonColumnDefs()trừ khi có ghi chú.
| Cột | Kiểu | Ghi chú |
|---|---|---|
id | text | PK, Snowflake qua IdGenerator |
createdAt | timestamptz | mặc định now() |
modifiedAt | timestamptz | cập nhật khi ghi |
createdBy | text | user id |
modifiedBy | text | user id |
deletedAt | timestamptz | marker soft-delete |
metadata | jsonb | extension bag |
status | text | khi dùng generateCommonColumnWithStatusDefs |
3. Entities
3.1 InventoryLocation
| Thuộc tính | Giá trị |
|---|---|
| Bảng | InventoryLocation |
| Source | core/src/models/schemas/inventory/inventory-location/schema.ts |
| Soft-delete | có |
| Owner | merchantId |
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
merchantId | text | ✓ | Owner |
parentId | text | Self-ref cho phân cấp | |
identifier | text | ✓ | Tự động, tiền tố LOC |
isDefault | boolean | ✓ | Mặc định false; partial unique theo merchant khi true |
status | text | ✓ | NEW / ACTIVATED / DEACTIVATED / ARCHIVED (mặc định NEW) |
name | i18n jsonb | ✓ | Tên hiển thị |
type | text | ✓ | InventoryLocationTypes (mặc định PHYSICAL) |
location | jsonb | Address ({ main, sub, long, lat, postCode }) |
Invariants: chính xác một isDefault=true mỗi merchant; parentId an toàn cycle.
3.2 InventoryItem
| Thuộc tính | Giá trị |
|---|---|
| Bảng | InventoryItem |
| Đa hình | có — (itemType, itemId) qua generatePrincipalColumnDefs({ discriminator: 'item' }); tham chiếu Material hoặc ProductVariant |
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
merchantId | text | ✓ | Owner |
itemType | text | ✓ | MATERIAL / PRODUCT_VARIANT |
itemId | text | ✓ | FK target id |
identifier | text | ✓ | Tự sinh, tiền tố INI |
status | text | ✓ | InventoryItemStatuses (mặc định ACTIVATED) |
Indexes: partial unique (merchantId, itemType, itemId) WHERE deletedAt IS NULL; non-unique (merchantId), (merchantId, status). Upsert idempotent: InventoryItemRepository.ensureInventoryItem key theo (merchantId, itemType, itemId).
3.3 InventoryStock
| Thuộc tính | Giá trị |
|---|---|
| Bảng | InventoryStock |
| Source | core/src/models/schemas/inventory/inventory-stock/schema.ts |
| Bucket key | (inventoryItemId, inventoryLocationId, lotNumber, serialNumber) UNIQUE NULLS NOT DISTINCT |
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
inventoryItemId | text | ✓ | FK |
inventoryLocationId | text | ✓ | FK |
merchantId | text | ✓ | Denormalize từ InventoryItem |
quantityOnHand | decimal(15,4) | ✓ | Tổng stock vật lý |
quantityReserved | decimal(15,4) | ✓ | Đã reserve cho đơn |
quantityAvailable | decimal(15,4) | ✓ | Trường lưu trữ; service duy trì = onHand − reserved |
lastCountedAt | timestamptz | — | |
lastStockedAt | timestamptz | — | |
lotNumber | text | Mở rộng bucket-key | |
serialNumber | text | Mở rộng bucket-key | |
expiryDate | timestamptz | Hỗ trợ FEFO | |
manufactureDate | timestamptz | — | |
averageCost | decimal(15,4) | Snapshot AVCO | |
costingMethod | text | ✓ | AVERAGE (mặc định), FIFO, LIFO, v.v. |
Indexes: (inventoryItemId), (inventoryLocationId), (merchantId).
Mutator atomic: InventoryStockRepository.adjustStock({ stockId, adjustOnHand, adjustAvailable, adjustReserved, forceNonNegative }) — một SQL UPDATE duy nhất với guard non-negative tùy chọn. Trả về null nếu guard fail.
3.4 InventoryTracking
| Thuộc tính | Giá trị |
|---|---|
| Bảng | InventoryTracking |
| Mutability | append-only audit log; repository có CRUD nhưng service chỉ ghi khi stock thay đổi |
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
inventoryStockId | text | ✓ | FK |
merchantId | text | ✓ | Denormalize từ stock chain |
referenceType | text | ✓ | Một trong §4; typed<TInventoryTrackingReferenceType> |
referenceId | text | ID doc khởi nguồn (nullable cho adjustment mồ côi) | |
uomId | text | ✓ | Đơn vị lúc ghi |
multiplier | decimal(15,4) | ✓ | Hệ số quy đổi UoM (mặc định 1) |
quantityBefore | decimal(15,4) | ✓ | Snapshot trước mutation |
quantityChange | decimal(15,4) | ✓ | Delta (có dấu) |
quantityAfter | decimal(15,4) | ✓ | Snapshot sau mutation |
effectivePrice | decimal(15,4) | Cost đơn vị (chỉ với write PURCHASE) | |
fromLocationId | text | Nguồn TRANSFER | |
toLocationId | text | Đích TRANSFER | |
reasonCode | text | Một trong 13 InventoryTrackingReasons | |
lotNumber / serialNumber / expiryDate | text / timestamptz | Snapshot bất biến của bucket được di chuyển | |
remainingQuantity | decimal(15,4) | Tracker layer FIFO (giảm dần khi outbound tiêu thụ) | |
note | text | Tự do (xem InventoryTrackingNotes) | |
createdBy / modifiedBy | text | Audit user (cho phép ẩn danh) |
Indexes: (inventoryStockId), (referenceId), (referenceType, referenceId), (fromLocationId), (toLocationId), (uomId), (merchantId).
Idempotency: lookup theo (referenceType, referenceId, inventoryStockId) trước khi ghi để tránh đếm kép khi Kafka redeliver.
3.5 InventoryIdentifier
| Thuộc tính | Giá trị |
|---|---|
| Bảng | InventoryIdentifier |
| Đa hình | tag InventoryItem hoặc InventoryStock |
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
principalType | text | ✓ | INVENTORY_ITEM / INVENTORY_STOCK |
principalId | text | ✓ | FK target id |
scheme | text | ✓ | SKU / BARCODE / QRCODE / IMEI / SERIAL |
value | text | ✓ | Chuỗi identifier |
Constraint: unique (scheme, value) mỗi principal — ngăn barcode trùng.
3.6 InventoryTicket / InventoryTicketItem
Trường InventoryTicket:
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
merchantId | text | ✓ | Owner |
identifier | text | ✓ | Tự động, tiền tố ITI |
type | text | ✓ | InventoryTicketTypes (mặc định UNKNOWN); xem §5.4 cho tập đầy đủ |
status | text | ✓ | Mặc định DRAFT |
partnerType / partnerId | text | VENDOR / CUSTOMER (khi áp dụng) | |
sourceLocationId / destinationLocationId | text | Cho TRANSFER | |
originReferenceType / originReferenceId | text | Lineage tới doc gốc (vd ref sale return) | |
returnOfTicketId | text | FK self — khi ticket này là return của ticket khác | |
backorderOfTicketId | text | FK self — lineage backorder | |
spawnedPurchaseOrderId | text | FK tới PurchaseOrder — khi ticket sinh ra một PO | |
reasonCode | text | Một trong InventoryTrackingReasons | |
effectiveDate / submittedAt / approvedAt / startedAt / completedAt / cancelledAt | timestamptz | Timestamp theo từng trạng thái | |
approvedBy | text | Audit user cho bước approval | |
note | text | Tự do |
Hành vi: ticket là một workflow document — không có hiệu ứng stock đến khi COMPLETED (lúc đó các dòng InventoryTicketItem mới điều khiển stock adjust).
3.7 PurchaseOrder / PurchaseOrderItem
Trường PurchaseOrder:
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
merchantId | text | ✓ | Owner |
purchaseOrderNumber | text | ✓ | Unique; mặc định = <YYYYMMDDHHmmss>-<snowflake> |
name | text | Mặc định = PurchaseOrder-<snowflake> | |
slug | text | Unique; cùng pattern mặc định | |
vendorId | text | ✓ | FK tới Vendor |
inventoryLocationId | text | ✓ | Đích khi nhận hàng |
status | text | ✓ | Xem §5.1 — 6 giá trị, mặc định DRAFT |
orderDate | timestamptz | ✓ | Mặc định now() |
expectedDeliveryDate / actualDeliveryDate | timestamptz | — | |
draftAt / processingAt / confirmedAt / receivedAt / completedAt / closedAt / cancelledAt | timestamptz | Timestamp theo từng trạng thái | |
currency | text | ✓ | Mặc định VND |
exchangeRate | decimal(12,6) | Mặc định 1 | |
subtotal / discount / tax / total | decimal(15,4) | ✓ | Tính lại bởi updateSummaryFromItems |
Indexes: (inventoryLocationId), (merchantId), (merchantId, status), (vendorId).
Trường PurchaseOrderItem:
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
purchaseOrderId | text | ✓ | FK |
itemType | text | ✓ | MATERIAL / PRODUCT_VARIANT (mặc định ProductVariant) |
itemId | text | ✓ | FK target id (đa hình, không có DB FK) |
currency | text | ✓ | Mặc định VND |
uomId | text | ✓ | Soft-ref tới UnitOfMeasure.id |
multiplier | decimal(15,4) | ✓ | UoM-to-base (mặc định 1) |
quantity | decimal(15,4) | ✓ | Số lượng đặt |
receivedQuantity | decimal(15,4) | ✓ | Đã nhận tích lũy |
unitPrice | decimal(15,4) | ✓ | Mỗi UoM |
discount / tax | decimal(15,4) | ✓ | Mỗi line |
total | decimal(15,4) | Tính toán | |
lotNumber / expiryDate / manufactureDate | text / timestamptz | Metadata lot/serial mỗi line | |
serialNumbers | jsonb | string[] — cho inventory có serial | |
landedCostShare | decimal(15,4) | Phần landed cost được phân bổ | |
effectiveCost | decimal(15,4) | Cột generated (unitPrice + landedCostShare) |
Idempotency khi add: cùng (purchaseOrderId, itemType, itemId, uomId) → cộng dồn quantity thay vì duplicate.
3.8 Vendor / VendorItem
Trường Vendor:
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
merchantId | text | ✓ | Owner |
identifier | text | ✓ | Tự động, tiền tố VEN |
slug | text | ✓ | URL-safe |
name | i18n jsonb | ✓ | Hiển thị |
description | i18n jsonb | — | |
status | text | ✓ | ACTIVATED / DEACTIVATED / ARCHIVED (mặc định ACTIVATED) |
location | jsonb | { main, sub, long, lat, postCode } | |
taxNumber | text | Mã số thuế | |
currency | text | ✓ | Mặc định VND |
contacts | jsonb (array) | Array<IVendorContact> (mặc định []) | |
note | text | Tự do |
Trường VendorItem — catalog M:N (KHÔNG có vendorId trên Material / ProductVariant):
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
vendorId | text | ✓ | FK |
merchantId | text | ✓ | Denormalize |
itemType | text | ✓ | MATERIAL / PRODUCT_VARIANT |
itemId | text | ✓ | FK target id |
uomId | text | UoM trong catalog của vendor (soft ref) | |
unitPrice | decimal(15,4) | Giá báo | |
multiplier | decimal(15,4) | Quy đổi UoM-to-base | |
isPreferred | boolean | ✓ | Partial unique theo (merchantId, itemType, itemId) |
status | text | ✓ | ACTIVATED / DEACTIVATED / ARCHIVED |
lastInvoiced | jsonb | Snapshot từ PO receive mới nhất: { unitPrice, uomId, multiplier, orderedAt, receivedAt } |
Atomic preferred flip: VendorItemRepository.setPreferredAtomic hạ cấp các row khác và nâng cấp target trong một câu lệnh.
3.9 Material / MaterialIdentifier
Trường Material:
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
merchantId | text | ✓ | Owner |
identifier | text | ✓ | Tự động, tiền tố MAT |
slug | text | ✓ | URL-safe |
name | i18n jsonb | ✓ | Hiển thị |
description | i18n jsonb | — | |
status | text | ✓ | MaterialStatuses (mặc định ACTIVATED) |
type | text | ✓ | MaterialTypes (mặc định RAW); xem enum nguồn cho tập đầy đủ |
uom | jsonb | IUomRole — { base, purchase, sale } (mặc định chuỗi rỗng); xem ADR-0005 | |
cost | decimal(15,4) | Tham chiếu standard cost (không có mặc định) | |
weight | decimal(15,4) | — | |
categoryId | text | FK tới Category | |
metadata | jsonb | IMaterialMetadata — theo quy ước chứa inventory.allowOversell (mặc định false) + inventory.isInventoryTracked (mặc định true) |
Trường MaterialIdentifier:
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
materialId | text | ✓ | FK |
scheme | text | ✓ | SYSTEM (tự động, tiền tố MAT) / SLUG / SKU / BARCODE / QRCODE |
value | text | ✓ | Unique theo (materialId, scheme) |
3.10 MaterialRecipe / MaterialRecipeItem
Trường MaterialRecipe:
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
merchantId | text | ✓ | Owner |
principalType | text | ✓ | MATERIAL / PRODUCT_VARIANT |
principalId | text | ✓ | Recipe này tạo ra cái gì |
status | text | ✓ | DRAFT / ACTIVATED / DEACTIVATED |
type | text | ✓ | KIT (trừ lúc bán) / MANUFACTURED (cần ProductionOrder) |
version | int | Tăng khi update aggregate |
Trường MaterialRecipeItem:
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
materialRecipeId | text | ✓ | FK tới MaterialRecipe |
principalType | text | ✓ | Loại component đa hình (MATERIAL hoặc PRODUCT_VARIANT) |
principalId | text | ✓ | FK target id (component) |
quantity | decimal(15,4) | ✓ | Lượng cần mỗi đơn vị principal |
uomId | text | ✓ | Soft ref |
isOptional | boolean | ✓ | Mặc định false — khi true, thiếu component không block production |
Indexes: partial unique (principalType, principalId, materialRecipeId) WHERE deletedAt IS NULL; non-unique (materialRecipeId), (uomId).
3.11 ProductionOrder
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
merchantId | text | ✓ | Owner |
productionNumber | text | ✓ | Unique theo merchant |
targetType | text | ✓ | Loại đa hình của thứ được sản xuất (MATERIAL / PRODUCT_VARIANT); mặc định ProductVariant |
targetId | text | ✓ | FK target id |
materialRecipeId | text | ✓ | FK tới recipe được dùng |
plannedQuantity / actualQuantity / scrapQuantity | decimal(15,4) | ✓ | Theo dõi sản xuất |
uom | text | ✓ | Code UoM (text, không phải jsonb) |
locationId | text | ✓ | FK tới InventoryLocation |
status | text | ✓ | ProductionOrderStatuses (mặc định DRAFT) |
scheduledStartAt / scheduledEndAt / startedAt / completedAt / cancelledAt | timestamptz | Timestamp vòng đời | |
outputLotNumber / outputExpiryDate | text / timestamptz | Metadata bucket output |
3.12 UnitOfMeasure
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
merchantId | text | NULL = system-wide; nếu khác là override theo merchant | |
code | text | ✓ | vd kg, box, pair |
name | i18n jsonb | ✓ | Hiển thị |
category | text | ✓ | COUNT / WEIGHT / VOLUME / TIME |
referenceCode | text | Code base unit (self-ref) | |
ratio | decimal(15,4) | ✓ | Tỷ lệ tới base (1.0 cho base unit) |
Phạm vi 3 cấp: system (merchantId NULL) → merchant override → product/material (qua uom jsonb trên Material).
4. InventoryTrackingReferenceTypes
| Giá trị | Dùng bởi |
|---|---|
PURCHASE_ORDER | PO receive |
SALE_ORDER | Sale payment success → trừ product + reserve material |
KITCHEN_TICKET | Tiêu thụ kitchen ticket |
KITCHEN_TICKET_ITEM | Tiêu thụ cấp item của kitchen ticket |
INVENTORY_TICKET | Workflow ticket (transfer, adjust, count, scrap, return) |
PRODUCTION_ORDER | Tiêu thụ + output sản xuất |
ADJUSTMENT | Nhập tay của admin |
UNKNOWN | Fallback |
5. Enum Trạng thái
5.1 PurchaseOrderStatuses
| Giá trị | Code | Giai đoạn |
|---|---|---|
DRAFT | 001_DRAFT | Items có thể sửa |
PROCESSING | 203_PROCESSING | Items đóng băng, chờ hàng |
RECEIVED | 205_RECEIVED | Một phần/toàn bộ items đã nhận, hoàn thành một phần |
COMPLETED | 303_COMPLETED | Tất cả items đã nhận đầy đủ |
CLOSED | 404_CLOSED | Terminal — không còn thay đổi |
CANCELLED | 505_CANCELLED | Terminal — đã hủy |
5.2 MaterialRecipeStatuses
| Giá trị | Code |
|---|---|
DRAFT | 001_DRAFT |
ACTIVATED | 201_ACTIVATED |
DEACTIVATED | 202_DEACTIVATED |
5.3 Vendor / VendorItemStatuses
| Giá trị | Code |
|---|---|
ACTIVATED | 201_ACTIVATED |
DEACTIVATED | 202_DEACTIVATED |
ARCHIVED | 300_ARCHIVED |
5.4 InventoryTicketStatuses
| Giá trị | Code |
|---|---|
DRAFT | 001_DRAFT |
SUBMITTED | 200_SUBMITTED |
APPROVED | 250_APPROVED |
IN_PROGRESS | 300_IN_PROGRESS |
COMPLETED | 303_COMPLETED |
CANCELLED | 505_CANCELLED |
5.5 ReceivePurchaseOrderItemModes
| Giá trị | Hành vi |
|---|---|
OVERRIDE (mặc định) | newReceived = receivedQuantity |
ACCUMULATIVE | newReceived = currentReceived + receivedQuantity |
6. FixedInventoryTrackingTypes (19)
| Hướng | Types |
|---|---|
| Inbound (6) | STOCK_IN, PURCHASE, TRANSFER_IN, RETURN_FROM_CUSTOMER, ADJUSTMENT_IN, PRODUCTION_COMPLETE |
| Outbound (10) | STOCK_OUT, SALE, TRANSFER_OUT, RETURN_TO_VENDOR, ADJUSTMENT_OUT, EXPIRED, LOST, DAMAGED, USED_INTERNAL, USED_AS_MATERIAL |
| Neutral (2) | INVENTORY_COUNT, ADJUSTMENT_NEUTRAL |
| Custom (1) | CUSTOM |
7. Invariant cross-entity
| Invariant | Ép buộc bởi |
|---|---|
quantityAvailable = quantityOnHand − quantityReserved (post-condition) | Service layer duy trì; adjustStock mutate cả ba |
Chính xác một InventoryLocation mặc định mỗi merchant | InventoryLocationRepository.setDefaultAtomic |
Chính xác một VendorItem ưu tiên mỗi (merchantId, itemType, itemId) | VendorItemRepository.setPreferredAtomic |
MaterialRecipeItem.principalId tham chiếu Material hoặc ProductVariant (đa hình qua principalType) | Schema; zod cấp service validate principal tồn tại |
InventoryTracking là append-only (không UPDATE trừ qua tooling admin) | Quy ước repository; service chỉ ghi khi stock thay đổi |
Đa hình InventoryItem — chính xác một (merchantId, itemType, itemId) | upsert ensureInventoryItem |
Material.metadata.inventory.allowOversell điều khiển cờ forceNonNegative truyền vào adjustStock | InventoryService.loadPrincipalRefs |
Liên kết Vendor tới items đi qua VendorItem only — không có cột vendorId trên principal | Schema + ADR |
8. Hành vi Soft-delete
| Entity | Soft-delete | Ghi chú |
|---|---|---|
InventoryLocation, InventoryItem, InventoryStock, InventoryIdentifier, InventoryTicket, InventoryTicketItem, Vendor, VendorItem, Material, MaterialIdentifier, MaterialRecipe, MaterialRecipeItem, ProductionOrder, PurchaseOrder, PurchaseOrderItem, UnitOfMeasure | ✓ | Marker deletedAt; mặc định read là IS NULL |
InventoryTracking | ✓ chỉ phía schema | Service xem là audit bất biến; không bao giờ ghi deletedAt |