PRD: Đơn vị đo
| Module | Kho (CORE-06) | PRD ID | PRD-ITM-001 |
| Status | Shipped | Owner | Inventory squad |
| Date | 2026-05-04 | Version | v1.0 |
| Packages | @nx/inventory · @nx/core · apps/client | URD | ITM · POI |
TL;DR
Cung cấp cho merchant một danh mục đơn vị đo được quản lý - "kg", "cái", "tá" và cả những đơn vị tự định nghĩa - để mọi inventory item và dòng đơn mua hàng tham chiếu một đơn vị có thật, dùng chung, thay vì một chuỗi ngầm định. Mỗi đơn vị mang một category, một tỉ lệ quy đổi và một chuỗi đơn vị gốc, được seed sẵn các mặc định hệ thống, và sửa được từ một màn hình client. Kết quả: chiều đơn vị mà mọi entity danh mục chạy trên đó cuối cùng đã tường minh, nhất quán, và do merchant sở hữu.
1. Bối cảnh & Vấn đề
Inventory item được khóa theo (variant, location, unit) (URD-ITM-002) và dòng đơn mua hàng gộp theo (itemType, itemId, uom) (URD-POI-001) - nhưng chưa có một danh mục đơn vị nào được quản lý. Các mã như "kg", "cái" hay "tá" là chuỗi ngầm định, không có tỉ lệ quy đổi dùng chung, không có đơn vị tùy biến riêng cho từng merchant, và không có UI để merchant xem hay sửa các đơn vị mà danh mục của họ chạy trên đó. Mục Definitions §3 đã định nghĩa Đơn vị đo là một "đơn vị đo với 3 tầng scope: mặc định hệ thống → merchant → sản phẩm/nguyên vật liệu," nhưng chưa có gì hiện thực hóa nó.
Điều này khiến chiều đơn vị mong manh: hai item có thể lệch nhau về ý nghĩa của "thùng", tỉ lệ bị lặp lại ở mọi nơi cần đến, và merchant không thể đưa vào những đơn vị riêng cho ngành hàng của mình. Một danh mục Đơn vị đo hạng nhất là tiền đề cho phép tính tồn nhất quán, gộp dòng PO, và (về sau) xử lý đơn vị recipe/BOM.
2. Mục tiêu & Không phải mục tiêu
Mục tiêu
- Một danh mục UoM hai tầng: các mặc định hệ thống được seed (
merchantId = NULL) cộng với các đơn vị theo merchant; resolution theo merchant → hệ thống, khớp đầu tiên cho mỗi mã sẽ thắng. - Mỗi đơn vị mang một name/description i18n, một category nhóm (count, weight, volume, length, time, area, other), một tỉ lệ quy đổi, và một đơn vị gốc tự tham chiếu (
referenceId) để tỉ lệ nối chuỗi thay vì bị lặp. - Một vòng đời đơn vị đầy đủ (ACTIVATED → DEACTIVATED → ARCHIVED), cô lập theo merchant, soft-delete, và một partial index duy nhất trên
(code, merchantId). - Một CRUD API cho đơn vị, với seed nạp sẵn danh mục mặc định lúc khởi động.
- Một màn hình quản lý UoM phía client: các view tạo, sửa và liệt kê, một bộ chọn category, lựa chọn đơn vị gốc, và validation theo schema.
Không phải mục tiêu
- Quy đổi liên category (weight ↔ volume) - category tồn tại chính là để chặn các quy đổi vô nghĩa ở tầng ứng dụng, không phải để bắc cầu giữa chúng.
- Một tầng UoM thứ ba riêng cho sản phẩm/nguyên vật liệu - override quy cách đóng gói của sản phẩm nằm trên entity sản phẩm qua các role UoM base/purchase/sale, không phải một dòng UoM.
- Xử lý đơn vị recipe/BOM - thuộc về area Recipes / BOM đang làm dở.
- Trang UoM phía back-office (
apps/bo) - một surface riêng, về sau.
3. Success Metrics
| Metric | Mục tiêu / tín hiệu |
|---|---|
| Tính nhất quán đơn vị | 100% inventory item và dòng PO tham chiếu một đơn vị trong danh mục (không phải chuỗi tạm) |
| Độ phủ seed | Mỗi merchant resolve được trọn danh mục đơn vị mặc định lúc khởi động, không thiếu sót |
| Mức dùng đơn vị tùy biến | Merchant trong ngành hàng nhiều đơn vị tạo ít nhất một đơn vị tùy biến |
| Toàn vẹn quy đổi | Không quy đổi liên category nào được chấp nhận; các chuỗi đơn vị gốc resolve không tạo vòng |
4. Personas & Use Cases
| Persona | Mục tiêu trong tính năng này |
|---|---|
| Owner | Định nghĩa các đơn vị mà danh mục của mình chạy trên đó, kể cả đơn vị riêng cho ngành hàng |
| Inventory staff | Chọn đúng đơn vị khi tạo item và dòng PO; tin rằng "kg" chỉ có một nghĩa |
| System | Seed danh mục đơn vị mặc định để mỗi merchant bắt đầu với một bộ dùng được |
Kịch bản cốt lõi: merchant mở màn hình UoM → thấy các mặc định hệ thống đã seed → thêm một đơn vị tùy biến kèm category, tỉ lệ quy đổi và một đơn vị gốc → sửa hoặc ngừng kích hoạt các đơn vị khi danh mục thay đổi → mọi inventory item và dòng PO chọn đơn vị từ danh mục này.
5. User Stories
- Là một owner, tôi muốn một bộ đơn vị thông dụng có sẵn từ đầu, để tôi có thể dùng đơn vị mà không cần cấu hình gì trước.
- Là một owner, tôi muốn thêm một đơn vị tùy biến riêng cho ngành hàng của mình, để danh mục của tôi nói bằng đơn vị của tôi, không chỉ các mặc định.
- Là một owner, tôi muốn mỗi đơn vị thuộc về một category và nối chuỗi tới một đơn vị gốc bằng một tỉ lệ, để các quy đổi được dùng chung thay vì nhập lại ở mọi nơi.
- Là inventory staff, tôi muốn chọn một đơn vị từ một danh sách được quản lý khi tạo item và dòng PO, để chiều đơn vị nhất quán trên toàn danh mục.
- Là một owner, tôi muốn ngừng kích hoạt hoặc archive một đơn vị tôi không còn dùng, để đơn vị cũ ngừng xuất hiện trong khi lịch sử vẫn được giữ lại.
6. Functional Requirements
| # | Yêu cầu | URD ref |
|---|---|---|
| FR-1 | Một entity Đơn vị đo với name/description i18n, category, status, ratio quy đổi, referenceId tự tham chiếu, và merchantId | URD-ITM-002 · Definitions §3 |
| FR-2 | Scoping hai tầng: mặc định hệ thống được seed (merchantId = NULL) + đơn vị theo merchant; resolution merchant → hệ thống, khớp đầu tiên cho mỗi mã sẽ thắng | Definitions §3 |
| FR-3 | Partial index duy nhất trên (code, merchantId) khi chưa xóa, để một mã là duy nhất trong scope của một merchant | URD-ITM-002 |
| FR-4 | Phân loại category (count / weight / volume / length / time / area / other) với nhãn i18n; category chặn quy đổi liên category | Definitions §3 |
| FR-5 | Đơn vị gốc tự tham chiếu (referenceId) với một ratio quy đổi để tỉ lệ nối chuỗi thay vì bị lặp | Definitions §3 |
| FR-6 | Vòng đời đơn vị ACTIVATED → DEACTIVATED → ARCHIVED, cô lập theo merchant, và soft-delete | URD-ITM-005 |
| FR-7 | Một CRUD API cho đơn vị (create / update / list), được gate quyền dưới subject unit-of-measures | URD-ITM-002 |
| FR-8 | Một seed nạp sẵn danh mục đơn vị mặc định hệ thống lúc khởi động | Definitions §3 |
| FR-9 | Các role UoM (base / purchase / sale) gắn entity danh mục với đơn vị; dòng PO và item mặc định về đơn vị gốc của item | URD-POI-005 |
| FR-10 | Một màn hình quản lý UoM phía client: tạo, sửa, liệt kê, một bộ chọn category, lựa chọn đơn vị gốc, và validation theo schema | URD-ITM-002 |
Toàn văn yêu cầu và tiêu chí chấp nhận nằm trong URD Kho. PRD này tham chiếu chúng thay vì nhắc lại.
7. Non-Functional Requirements
| Khía cạnh | Yêu cầu |
|---|---|
| Toàn vẹn dữ liệu | Một mã là duy nhất trong scope của một merchant qua partial index (code, merchantId); các chuỗi đơn vị gốc không được tạo vòng |
| Tenancy & authz | Theo merchant (x-merchant-id); mặc định hệ thống dùng chung chỉ-đọc; mutation được gate bởi subject quyền unit-of-measures |
| Resolution | Tra cứu hai tầng resolve theo merchant → hệ thống, khớp đầu tiên cho mỗi mã sẽ thắng - xác định cho mỗi merchant |
| Soft-delete | Đơn vị dùng soft-delete; các đơn vị bị ngừng kích hoạt/archive/xóa được giữ lại và loại khỏi các danh sách chuẩn |
| i18n | Tên đơn vị, mô tả và nhãn category là song ngữ ({ en, vi }) |
| Độ chính xác | Tỉ lệ quy đổi dùng float(value, 4) |
8. UX & Flows
Các màn hình chính (trong apps/client): view liệt kê UoM cùng các màn hình tạo/sửa với một bộ chọn category và một component lựa chọn đơn vị gốc, tất cả dựa trên validation theo schema.
9. Data & Domain
| Entity | Vai trò |
|---|---|
UnitOfMeasure | Một dòng đơn vị - name/description i18n, category, status, ratio quy đổi, referenceId tự tham chiếu, merchantId (NULL cho mặc định hệ thống) |
| Category UoM | Phân loại nhóm (count / weight / volume / length / time / area / other) với nhãn i18n; giới hạn các quy đổi hợp lệ |
Role UoM (base / purchase / sale) | Gắn một entity danh mục (variant/nguyên vật liệu) với các đơn vị của nó; dòng PO mặc định về đơn vị gốc |
| Seed migration | Nạp sẵn danh mục đơn vị mặc định hệ thống lúc khởi động |
Chỉ ở mức khái niệm - schema và bất biến đầy đủ nằm trong domain model của inventory và ADR-0005 UoM hai tầng.
10. Dependencies & Assumptions
Phụ thuộc vào
- Inventory Items (URD-ITM) - item được khóa theo
(variant, location, unit); danh mục cấp đơn vị đó. - Purchase Order Items (URD-POI) - dòng PO gộp theo
uomvà mặc định về đơn vị gốc của item. @nx/core- sở hữu schemaUnitOfMeasure, các hằng category, interface role UoM, và repository có scope.
Giả định
- Danh mục đơn vị mặc định hệ thống được seed trước khi bất kỳ merchant nào resolve đơn vị.
- Category là một phân loại cố định ở tầng ứng dụng; quy đổi liên category là cố tình bất khả thi.
- Override quy cách đóng gói của sản phẩm/nguyên vật liệu được thể hiện qua các role UoM trên variant, không phải bằng thêm tầng danh mục.
11. Risks & Open Questions
| Rủi ro / câu hỏi | Giảm thiểu / trạng thái |
|---|---|
| Một mã của merchant có thể trùng với một mặc định hệ thống | Resolution theo merchant → hệ thống, khớp đầu tiên cho mỗi mã sẽ thắng; đơn vị của merchant che mặc định một cách xác định |
| Các chuỗi đơn vị gốc có thể tạo vòng | referenceId tự tham chiếu phải trỏ tới một đơn vị đang tồn tại; các chuỗi được validate để kết thúc tại một đơn vị gốc |
| Kỳ vọng quy đổi liên category | Ngoài phạm vi theo thiết kế - category chặn nó; ghi nhận như một ràng buộc |
| Sửa một đơn vị đang được item/dòng tham chiếu | Đơn vị dùng soft-delete và một vòng đời status; ngừng kích hoạt thay vì hard-delete để giữ các tham chiếu |
12. Release Plan & Launch Criteria
| Khía cạnh | Kế hoạch |
|---|---|
| Phase | P1 (nền tảng) - hỗ trợ tính năng ITM trong catalog tính năng URD |
| Rollout | Tất cả merchant; không feature flag |
| Migration | Danh mục đơn vị mặc định hệ thống được seed lúc khởi động; không di chuyển dữ liệu theo merchant |
| Tiêu chí ra mắt | Seed resolve trọn danh mục mặc định; create/edit/list đã kiểm chứng đầu-cuối; (code, merchantId) duy nhất được enforce; quy đổi liên category bị từ chối |
| Giám sát | Số đơn vị tùy biến được tạo theo merchant, độ đầy đủ của seed-resolution, tỉ lệ từ chối quy đổi |
13. FAQ
Các đơn vị ban đầu đến từ đâu? Một seed nạp sẵn một danh mục mặc định hệ thống (merchantId = NULL) lúc khởi động, nên mỗi merchant có một bộ dùng được mà không cần cấu hình gì.
Một đơn vị tùy biến quan hệ thế nào với một mặc định hệ thống có cùng mã? Resolution theo merchant → hệ thống, khớp đầu tiên cho mỗi mã sẽ thắng - đơn vị của merchant che mặc định hệ thống đối với merchant đó.
Tôi có quy đổi kilogram sang lít được không? Không - category (weight, volume, …) tồn tại chính là để chặn quy đổi liên category. Quy đổi chỉ nối chuỗi trong một category qua tỉ lệ đơn vị gốc.
Có một tầng đơn vị thứ ba riêng cho sản phẩm không? Không - quy cách đóng gói của sản phẩm/nguyên vật liệu được thể hiện qua các role UoM base/purchase/sale trên variant, không phải bằng thêm một dòng danh mục UoM.
Màn hình UoM ở đâu? Trong apps/client - một view liệt kê cùng các màn hình tạo/sửa với một bộ chọn category và lựa chọn đơn vị gốc. Trang back-office là một surface riêng, về sau.
References
- URD: Kho - ITM · Purchase Order Items · Definitions §3
- PRD liên quan: Đơn mua hàng · Recipes / BOM
- Module: Kho - tổng quan + traceability
- Developer: @nx/inventory · domain model · ADR-0005 UoM hai tầng