Skip to content

PRD: Đơn vị đo

ModuleKho (CORE-06)PRD IDPRD-ITM-001
StatusShippedOwnerInventory squad
Date2026-05-04Versionv1.0
Packages@nx/inventory · @nx/core · apps/clientURDITM · 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

MetricMụ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ủ seedMỗ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ếnMerchant 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 đổiKhô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

PersonaMụ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 staffChọn đúng đơn vị khi tạo item và dòng PO; tin rằng "kg" chỉ có một nghĩa
SystemSeed 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.
  • 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ầuURD ref
FR-1Một entity Đơn vị đo với name/description i18n, category, status, ratio quy đổi, referenceId tự tham chiếu, và merchantIdURD-ITM-002 · Definitions §3
FR-2Scoping 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ắngDefinitions §3
FR-3Partial 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 merchantURD-ITM-002
FR-4Phân loại category (count / weight / volume / length / time / area / other) với nhãn i18n; category chặn quy đổi liên categoryDefinitions §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ặpDefinitions §3
FR-6Vòng đời đơn vị ACTIVATED → DEACTIVATED → ARCHIVED, cô lập theo merchant, và soft-deleteURD-ITM-005
FR-7Một CRUD API cho đơn vị (create / update / list), được gate quyền dưới subject unit-of-measuresURD-ITM-002
FR-8Một seed nạp sẵn danh mục đơn vị mặc định hệ thống lúc khởi độngDefinitions §3
FR-9Cá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 itemURD-POI-005
FR-10Mộ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 schemaURD-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ạnhYêu cầu
Toàn vẹn dữ liệuMộ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 & authzTheo 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
ResolutionTra 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
i18nTên đơn vị, mô tả và nhãn category là song ngữ ({ en, vi })
Độ chính xácTỉ 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

EntityVai trò
UnitOfMeasureMộ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 UoMPhâ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 migrationNạ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 inventoryADR-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 uom và mặc định về đơn vị gốc của item.
  • @nx/core - sở hữu schema UnitOfMeasure, 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ỏiGiả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ốngResolution 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òngreferenceId 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 categoryNgoà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ạnhKế hoạch
PhaseP1 (nền tảng) - hỗ trợ tính năng ITM trong catalog tính năng URD
RolloutTất cả merchant; không feature flag
MigrationDanh 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ắtSeed 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átSố đơ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

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.