Inventory Location
1. Tổng quan
| Thuộc tính | Giá trị |
|---|---|
| ID | FEAT-INV-LOC |
| Status | Stable |
| Owner | inventory-team |
| Phụ thuộc | Merchant (nguồn CDC cho default seed) |
InventoryLocation đại diện cho một warehouse, store, hoặc sub-location của merchant. Chính xác một location mỗi merchant mang isDefault=true. Locations hỗ trợ phân cấp self-reference (parentId) và vòng đời 4 trạng thái. Default location được tự tạo khi một row Merchant được commit qua Debezium CDC.
2. Mô hình Entity
Trường
| 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 (mặc định) / ACTIVATED / DEACTIVATED / ARCHIVED |
name | i18n jsonb | ✓ | Hiển thị |
type | text | ✓ | InventoryLocationTypes (mặc định PHYSICAL) |
location | jsonb | { main, sub, long, lat, postCode } |
3. Vòng đời
| Từ | Sự kiện | Đến | Guards |
|---|---|---|---|
NEW / DEACTIVATED | activate | ACTIVATED | — |
ACTIVATED | deactivate | DEACTIVATED | — |
ACTIVATED / DEACTIVATED | archive | ARCHIVED | không phải default của merchant; không có row InventoryStock đang sống |
4. Vận hành
InventoryLocationService (inventory-location.service.ts)
| Phương thức | Signature | Mục đích | Dòng |
|---|---|---|---|
getOverview | { context, filter } | Thẻ KPI cho màn hình Location (phạm vi theo merchant) | 122-129 |
getList | { context, filter } | Danh sách phân trang + tổng hợp tồn kho & cảnh báo theo từng location | 131-145 |
getCount | { context, where } | Tổng số đi kèm cho getList | 147-158 |
createAggregate | { context, data } | Tạo location + reconcile link kênh bán (một TX) | 194-221 |
updateAggregate | { context, id, data } | Patch location + thay thế toàn bộ link kênh bán (một TX) | 224-272 |
setDefault | { context, id } | Promote thành default của merchant; atomic hạ cấp các default khác | 478-494 |
activate | { context, id } | NEW/DEACTIVATED → ACTIVATED | 497-505 |
deactivate | { context, id } | ACTIVATED → DEACTIVATED | 507-519 |
archive | { context, id } | Chuyển sang ARCHIVED với guard | 522-548 |
Phương thức tùy biến InventoryLocationRepository
| Phương thức | Mục đích |
|---|---|
getLocationOverview | Thẻ KPI tổng hợp: số lượng location + tổng giá trị tồn + số location cần chú ý. Tái sử dụng các fragment dùng chung stock-posture.sql |
findInventoryLocations | Danh sách phân trang; tổng hợp tồn theo location + số kênh bán + cờ cảnh báo (out/low/oversell qua BOOL_OR) |
countInventoryLocations | Tổng số đi kèm cho findInventoryLocations |
ensureDefaultLocation | Idempotent — tìm hoặc tạo default cho merchant; gọi bởi handler Merchant CDC |
setDefaultAtomic | Một câu lệnh: hạ cấp các row isDefault khác + nâng cấp target |
findParentChain | Đi qua chuỗi parentId lên đến maxDepth — dùng để phát hiện cycle |
countStockReferences | Đếm InventoryStock đang sống tham chiếu location này — dùng bởi guard beforeDelete, KHÔNG bởi archive |
5. REST Endpoints
| Verb | Path | Auth | Permission | Handler |
|---|---|---|---|---|
| 6× CRUD | /inventory-locations | JWT/BASIC | InventoryLocation.<crud> | merchant-scoped |
POST | /inventory-locations/aggregate | JWT/BASIC | InventoryLocation.createAggregate | createAggregate |
PATCH | /inventory-locations/:id/aggregate | JWT/BASIC | InventoryLocation.updateAggregate | updateAggregate |
GET | /inventory-locations/overview | JWT/BASIC | InventoryLocation.find | getOverview |
GET | /inventory-locations/list | JWT/BASIC | InventoryLocation.find | getList |
GET | /inventory-locations/list/count | JWT/BASIC | InventoryLocation.count | getCount |
POST | /inventory-locations/:id/default | JWT/BASIC | InventoryLocation.setDefault | setDefault |
POST | /inventory-locations/:id/activate | JWT/BASIC | InventoryLocation.activate | activate |
POST | /inventory-locations/:id/deactivate | JWT/BASIC | InventoryLocation.deactivate | deactivate |
POST | /inventory-locations/:id/archive | JWT/BASIC | InventoryLocation.archive | archive |
5.1 Số liệu List & Overview
Cả ba route đều bắt buộc chỉ định merchant qua filter where (merchant picker trên màn hình) — /overview và /list đọc filter[where][merchantId], còn /list/count đọc where[merchantId]. Service bắt buộc phải có (400 nếu thiếu), kiểm tra nằm trong scope của người gọi (403 nếu không), rồi query đúng merchant đó. Phần stock posture theo từng location tái sử dụng các fragment dùng chung stock-posture.sql, nên phép tính cảnh báo dùng chung một nguồn với danh sách stock và item.
Row của /list — các trường location cộng summary và needAttention:
| Khối | Trường | Ý nghĩa |
|---|---|---|
saleChannels | total | Số sale channel được route tới location này (link SaleChannelInventoryLocation đang sống) |
summary | itemCount | Số InventoryItem riêng biệt có bucket tồn tại location này |
summary | onHand | { quantity, value } cộng dồn qua các bucket của location |
summary | reserved | { quantity, value } cộng dồn qua các bucket của location |
needAttention | out | Có bucket với available ≤ 0 |
needAttention | oversell | Có bucket với available < 0 (tập con của out) |
needAttention | low | Có bucket với 0 < available ≤ lowStockThreshold |
Sắp xếp nhận name / identifier / type / status / createdAt (id làm tiebreaker); mặc định name ASC.
Thẻ của /overview:
| Khối | Trường | Ý nghĩa |
|---|---|---|
counts | total / physical / simulation / active | Số location theo type / trạng thái ACTIVATED |
counts | withStock / empty | Location có ≥1 bucket tồn vs. không có (empty = total − withStock) |
stock | totalOnHand / totalValue | Số lượng on-hand + định giá trên toàn bộ bucket của merchant |
needAttention | — | Số location có ≥1 bucket cần chú ý (out ∪ low) |
5.2 Aggregate create / update
POST /aggregate và PATCH /{id}/aggregate tạo/patch location và reconcile các kênh bán bán từ location đó, trong một transaction. Phần ghi location đi qua các lifecycle hook bình thường (trạng thái NEW, isDefault atomic, kiểm tra cycle cha); sau đó các link kênh bán được reconcile với SaleChannelInventoryLocation.
saleChannels[] (mỗi phần tử { saleChannelId, priority? }) là tập khai báo đầy đủ (full set):
| Verb | saleChannels | Tác dụng |
|---|---|---|
POST | có / bỏ qua | Tập link cần tạo (bỏ qua = không tạo). |
PATCH | bỏ qua | Giữ nguyên link hiện có. |
PATCH | có | Trở thành tập chính xác — kênh có trong list được upsert (patch priority), kênh không có bị soft-delete. [] xoá hết. |
Mỗi saleChannelId được kiểm tra phải thuộc merchant của location (400 không tìm thấy, 403 khác merchant). merchantId / identifier bất biến khi update. Response: { location, saleChannels[] }.
isDefault: được áp dụng ở cả hai verb, nhưng chỉ promote (đặt làm default). Với PATCH, isDefault: true chạy đúng logic của POST /{id}/default (dùng chung _promoteToDefault): row phải đang ACTIVATED (nếu không → 400), rồi flip atomic — hạ default cũ của merchant và promote row này trong một câu lệnh. isDefault: false / bỏ qua là no-op (bỏ default bằng cách promote location khác). status không được aggregate nhận — chuyển trạng thái đi qua /activate · /deactivate · /archive.
Commerce cũng có CRUD ghi các link này (phía sale-channel). Cả hai cùng ghi bảng
SaleChannelInventoryLocation— một nguồn sự thật, hai entry point.
6. Sự kiện
Inbound:
| Topic | Trigger | Handler | Tác dụng |
|---|---|---|---|
nx.seller.public.merchant (Debezium CDC) | INSERT/UPDATE row Merchant | InventoryWorkerService.handleMerchantCDC | ensureDefaultLocation(merchantId) — idempotent |
Outbound: không trực tiếp.
7. Luồng Seed Default Location
Idempotent — re-delivery là no-op.
8. Invariants
| Invariant | Ép buộc bởi |
|---|---|
Chính xác một isDefault=true mỗi merchant | Partial unique index + setDefaultAtomic |
Chuỗi parentId không có cycle | Phát hiện cycle ở cấp service khi update |
| Không thể archive default location | Guard service trong archive |
| Không thể xóa location còn stock đang sống | Guard beforeDelete qua countStockReferences (lưu ý: archive hôm nay không có guard này) |
archive không cascade tới children | Children phải được archive/reparent trước |
9. Trang liên quan
- Inventory Stock — locations giữ stock bucket
- Purchase Order — default location là đích nhận hàng fallback
- Kiến trúc — luồng Merchant CDC
- Mô hình miền