Purchase Order
1. Tổng quan
| Thuộc tính | Giá trị |
|---|---|
| ID | FEAT-INV-PO |
| Status | Stable |
| Owner | inventory-team |
| Phụ thuộc | Vendor, VendorItem, Material / ProductVariant, InventoryLocation, UnitOfMeasure |
Purchase Order đại diện cho việc nhận hàng từ vendor. Aggregate API (POST /aggregate, PATCH /:id/aggregate) tạo/cập nhật một PO + các item của nó trong một transaction. Nhận hàng mutate InventoryStock và ghi vào InventoryTracking. Các chân thanh toán tùy chọn trong receive call kích hoạt một Kafka emit được tiêu thụ bởi @nx/finance.
2. Mô hình Entity
Trường PurchaseOrder
| Trường | Kiểu | Bắt buộc | Ghi chú |
|---|---|---|---|
id | text | ✓ | Snowflake |
purchaseOrderNumber | text | ✓ | Unique; mặc định <YYYYMMDDHHmmss>-<snowflake> |
name | text | Mặc định PurchaseOrder-<snowflake> | |
slug | text | Unique; cùng pattern mặc định | |
merchantId | text | ✓ | Owner |
vendorId | text | ✓ | FK |
inventoryLocationId | text | ✓ | Đích nhận hàng |
status | text | ✓ | Xem §3; 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) | ✓ | Duy trì bởi updateSummaryFromItems |
metadata | jsonb | IPurchaseOrderMetadata |
Trường PurchaseOrderItem
| Trường | Kiểu | Bắt buộc | Ghi chú |
|---|---|---|---|
purchaseOrderId | text | ✓ | FK |
itemType | text | ✓ | MATERIAL / PRODUCT_VARIANT |
itemId | text | ✓ | FK target id (đa hình) |
uomId | text | Soft ref | |
multiplier | decimal(15,4) | Quy đổi UoM-to-base | |
quantity | decimal(15,4) | ✓ | Đã đặt |
receivedQuantity | decimal(15,4) | ✓ | Tích lũy |
unitPrice | decimal(15,4) | ✓ | Mỗi UoM |
discount / tax | decimal(15,4) | Mỗi line |
Idempotency khi add: cùng (purchaseOrderId, itemType, itemId, uomId) cộng dồn quantity thay vì duplicate.
3. Vòng đời
| Từ | Sự kiện | Đến | Guard |
|---|---|---|---|
DRAFT | confirmPurchaseOrder | PROCESSING | items.length ≥ 1 |
PROCESSING | revertPurchaseOrder | DRAFT | chưa nhận item nào |
PROCESSING / RECEIVED | receivePurchaseOrder(items) | RECEIVED | nhận một phần |
PROCESSING / RECEIVED | receivePurchaseOrder(items) | COMPLETED | tất cả items có receivedQuantity ≥ quantity |
RECEIVED / COMPLETED | completePurchaseOrder | COMPLETED | idempotent |
RECEIVED / COMPLETED | closePurchaseOrder | CLOSED | terminal |
| any non-terminal | cancelPurchaseOrder | CANCELLED | terminal |
| Status code (giá trị DB) | Hiển thị |
|---|---|
001_DRAFT | DRAFT |
203_PROCESSING | PROCESSING |
205_RECEIVED | RECEIVED |
303_COMPLETED | COMPLETED |
404_CLOSED | CLOSED |
505_CANCELLED | CANCELLED |
4. Chế độ Receive
Định nghĩa trong
inventory/src/common/constants.ts:20(ReceivePurchaseOrderItemModes).
| Mode | Hành vi |
|---|---|
OVERRIDE (mặc định) | newReceivedQuantity = receivedQuantity (caller tự tính giá trị target tuyệt đối) |
ACCUMULATIVE | newReceivedQuantity = currentReceivedQuantity + receivedQuantity |
InventoryService.receiveInventoryFromPurchaseOrderItem()luôn vận hành trên một delta — caller resolve delta từ mode đã chọn trước khi truyền xuống.
Quy tắc auto-complete: nếu mọi item thỏa receivedQuantity ≥ quantity sau một receive call, PurchaseOrder.status chuyển sang COMPLETED trong cùng operation.
5. Vận hành
| Phương thức | Signature | Mục đích | Dòng |
|---|---|---|---|
createAggregate | { context, data: TCreatePurchaseOrderAggregateRequest } | PO + items trong một TX; merge item idempotent | 212 |
updateByIdAggregate | { context, id, data: TUpdatePurchaseOrderAggregateRequest } | Patch metadata + items qua sự hiện diện id | 277 |
addItemToPurchaseOrder | { purchaseOrderId, item: TAddPurchaseOrderItemRequest } | Append vào DRAFT; merge nếu cùng (itemType, itemId, uomId) | 338 |
clearPurchaseOrderItems | { purchaseOrderId } | Soft-delete tất cả items; chỉ DRAFT | 379 |
confirmPurchaseOrder | { purchaseOrderId } | DRAFT → PROCESSING | 412 |
receivePurchaseOrder | { purchaseOrderId, inventoryLocationId?, items, transaction? } | PROCESSING/RECEIVED → RECEIVED/COMPLETED; mutate stock; ghi tracking; emit Kafka | 436 |
revertPurchaseOrder | { purchaseOrderId } | PROCESSING → DRAFT | 522 |
cancelPurchaseOrder | { purchaseOrderId, reason? } | bất kỳ non-terminal → CANCELLED | 534 |
completePurchaseOrder | { purchaseOrderId } | RECEIVED → COMPLETED (idempotent) | 564 |
closePurchaseOrder | { purchaseOrderId } | RECEIVED/COMPLETED → CLOSED | 591 |
Nguồn:
inventory/src/services/purchase-order.service.ts(1227 dòng).
Ngữ nghĩa items của updateByIdAggregate
| Hình dạng | Diễn giải |
|---|---|
Chỉ { id } | DELETE item này |
{ id, ...fields } | UPDATE item hiện có |
{ mode, itemId, quantity, ... } (không có id) | CREATE mới |
Chi tiết luồng Receive
effectiveQuantity = adjustedQuantity * multiplier— quy đổi base-unit từ UoM của line.
6. REST Endpoints
| Verb | Path | Auth | Permission | Handler |
|---|---|---|---|---|
POST | /purchase-orders/aggregate | JWT/BASIC | PurchaseOrder.createAggregate | createAggregate |
PATCH | /purchase-orders/:id/aggregate | JWT/BASIC | PurchaseOrder.updateAggregate | updateByIdAggregate |
POST | /purchase-orders/:id/items | JWT/BASIC | PurchaseOrder.addItem | addItemToPurchaseOrder |
DELETE | /purchase-orders/:id/items | JWT/BASIC | PurchaseOrder.clearItems | clearPurchaseOrderItems |
POST | /purchase-orders/:id/confirm | JWT/BASIC | PurchaseOrder.confirm | confirmPurchaseOrder |
POST | /purchase-orders/:id/revert | JWT/BASIC | PurchaseOrder.revert | revertPurchaseOrder |
POST | /purchase-orders/:id/receive | JWT/BASIC | PurchaseOrder.receive | receivePurchaseOrder |
POST | /purchase-orders/:id/cancel | JWT/BASIC | PurchaseOrder.cancel | cancelPurchaseOrder |
POST | /purchase-orders/:id/complete | JWT/BASIC | PurchaseOrder.complete | completePurchaseOrder |
POST | /purchase-orders/:id/close | JWT/BASIC | PurchaseOrder.close | closePurchaseOrder |
| 6× CRUD | /purchase-orders | JWT/BASIC | PurchaseOrder.<crud> | CRUD |
7. Sự kiện
Inbound: không trực tiếp. PO chỉ được mutate qua REST.
Outbound:
| Topic | Khi nào | Payload |
|---|---|---|
purchase-order.received (KafkaTopics.PURCHASE_ORDER_RECEIVED) | Sau khi receivePurchaseOrder commit, chỉ khi payments[] không rỗng | { purchaseOrderId, merchantId, payments[], items[] (với inventoryItemId, quantity, unitCost, uomId, multiplier, effectiveAt) } |
Consumer:
@nx/finance(ghi nhận chân EXPENSE / COGS).
8. Side effects khi receive
| Side effect | Ở đâu |
|---|---|
InventoryStock.adjustStock(+effectiveQuantity) | Mỗi item, atomic |
Insert row InventoryTracking (type=PURCHASE, referenceType=PURCHASE_ORDER, referenceId=<poId>) | Mỗi item, sau khi adjust stock |
Cập nhật snapshot VendorItem.lastInvoiced | Mỗi item qua VendorItemService.recordPurchase |
Tính lại PurchaseOrder.subtotal/tax/total | Sau khi xử lý hết items |
Chuyển trạng thái sang RECEIVED hoặc COMPLETED | Dựa trên receivedQuantity tích lũy |
Emit Kafka PURCHASE_ORDER_RECEIVED | Post-commit, chỉ khi payments[] không rỗng |
9. Trang liên quan
- Vendor & VendorItem — chi tiết snapshot
recordPurchase - Inventory Stock — ngữ nghĩa
adjustStock - Inventory Tracking — định dạng audit trail
- Quyết định