Vendor & VendorItem
1. Tổng quan
| Thuộc tính | Giá trị |
|---|---|
| ID | FEAT-INV-VEN |
| Status | Stable |
| Owner | inventory-team |
| Phụ thuộc | Material / ProductVariant, UnitOfMeasure, PurchaseOrder (caller của recordPurchase) |
Vendor là một nhà cung cấp. VendorItem là junction M:N liên kết một vendor với một principal item (Material hoặc ProductVariant) cho một merchant cụ thể — với giá báo, UoM, multiplier, cờ preferred, và snapshot điều khoản hóa đơn gần nhất. Principal không bao giờ mang vendorId (ADR-0002).
2. Mô hình Entity
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
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
vendorId | text | ✓ | FK tới Vendor |
merchantId | text | ✓ | Denormalize từ Vendor.merchantId |
itemType | text | ✓ | MATERIAL / PRODUCT_VARIANT |
itemId | text | ✓ | FK target id (đa hình) |
uomId | text | UoM trong catalog của vendor (soft ref) | |
multiplier | decimal(15,4) | Quy đổi UoM-to-base | |
unitPrice | decimal(15,4) | Giá báo mỗi UoM | |
isPreferred | boolean | ✓ | Partial unique theo (merchantId, itemType, itemId) — tối đa một preferred mỗi item |
status | text | ✓ | ACTIVATED / DEACTIVATED / ARCHIVED |
lastInvoiced | jsonb | Snapshot từ PO receive mới nhất: { unitPrice, uomId, multiplier, orderedAt, receivedAt } |
3. Vòng đời
| Từ | Sự kiện | Đến |
|---|---|---|
ACTIVATED | deactivate | DEACTIVATED |
DEACTIVATED | activate | ACTIVATED |
* (chưa archive) | archive | ARCHIVED (terminal) |
Cả
VendorvàVendorItemchia sẻ vòng đời này.
4. Vận hành
VendorService (vendor.service.ts — 313 dòng)
| Phương thức | Signature | Mục đích |
|---|---|---|
createAggregate | { context, data: TCreateVendorAggregateRequest } | Tạo Vendor + (tùy chọn) batch VendorItem trong một TX. Tên trường aggregate: items[] (ngữ nghĩa per-parent — xem rule memory feedback_aggregate_field_naming) |
updateAggregate | { context, id, data: TUpdateVendorAggregateRequest } | Patch Vendor + grant/omit batch VendorItem |
VendorItemService (vendor-item.service.ts — 772 dòng)
| Phương thức | Signature | Mục đích |
|---|---|---|
create (override) | { context, data, transaction? } | Tạo VendorItem; validate vendor.merchantId === context.merchantId; flip isPreferred atomic nếu isPreferred=true |
updateById (override) | { context, id, data, transaction? } | Update; cùng atomicity isPreferred |
setPreferred | { context, id } | Promote thành nguồn ưu tiên của vendor cho item này — atomic qua phương thức repo setPreferredAtomic |
activate | { context, id } | DEACTIVATED → ACTIVATED |
deactivate | { context, id } | ACTIVATED → DEACTIVATED |
recordPurchase | { vendorItemId, unitPrice, uomId, multiplier, orderedAt?, receivedAt? } | Cập nhật snapshot lastInvoiced — gọi bởi PurchaseOrderService khi receive; bỏ qua scope check (service-internal) |
upsertBatch | { context, merchantId, entries: TVendorItemAssignment[], transaction } | Bulk create/update — dùng bởi aggregate Vendor |
omitBatch | { context, vendorId, itemIds, transaction } | Soft-delete (archive) batch |
5. REST Endpoints
/vendors
| Verb | Path | Auth | Permission | Handler |
|---|---|---|---|---|
| 6× CRUD | /vendors | JWT/BASIC | Vendor.<crud> | CRUD |
POST | /vendors/aggregate | JWT/BASIC | Vendor.createAggregate | VendorService.createAggregate |
PATCH | /vendors/:id/aggregate | JWT/BASIC | Vendor.updateAggregate | VendorService.updateAggregate |
/vendor-items
| Verb | Path | Auth | Permission | Handler |
|---|---|---|---|---|
| 6× CRUD | /vendor-items | JWT/BASIC | VendorItem.<crud> | CRUD (với validate vendor↔merchant) |
POST | /vendor-items/:id/set-preferred | JWT/BASIC | VendorItem.setPreferred | setPreferred |
POST | /vendor-items/:id/activate | JWT/BASIC | VendorItem.activate | activate |
POST | /vendor-items/:id/deactivate | JWT/BASIC | VendorItem.deactivate | deactivate |
recordPurchase,upsertBatch,omitBatchkhông lộ ra REST — chỉ dùng nội bộ.
6. Sự kiện
Inbound: không trực tiếp.
Outbound: không trực tiếp. Các mutation Vendor / VendorItem chỉ được persist.
7. Pattern Quan trọng
Atomic isPreferred flip
VendorItemRepository.setPreferredAtomic thực hiện trong một câu lệnh:
- Hạ cấp tất cả row
isPreferred=truenơi(merchantId, itemType, itemId) = nhóm của targetANDid ≠ target.id. - Promote target thành
isPreferred=true.
Kết hợp với partial unique index trên (merchantId, itemType, itemId) WHERE isPreferred=true, điều này đảm bảo tối đa một preferred mỗi item.
Snapshot lastInvoiced từ PO receive
Bỏ qua scope check vì được gọi bên trong luồng PO receive đã được xác thực.
Backfill từ lịch sử PO sẵn có
inventory-0005-backfill-vendor-item.ts (migration one-shot) materialize các row VendorItem cho nhóm (merchantId, vendorId, itemType, itemId) bằng cách snapshot dòng PO non-cancelled mới nhất.
8. Đặt tên trường aggregate
Theo rule memory feedback_aggregate_field_naming:
| Aggregate | Trường collection con |
|---|---|
| Vendor → VendorItems | items[] (vendor-centric: "các items mà vendor này cung cấp") |
| Material → VendorItems | vendors[] (material-centric: "các vendor cung cấp material này") |
Cùng bảng junction, đặt tên semantic khác nhau theo context của parent.
9. Trang liên quan
- Purchase Order — caller của
recordPurchase - Material & BOM — Material là principal phổ biến nhất của VendorItem
- Mô hình miền — schema đầy đủ
- ADR-0002 Vendor chỉ qua VendorItem
- Quyết định