Skip to content

PRD: Nhập tồn đầu kỳ

ModuleKho (CORE-06)PRD IDPRD-STK-002
StatusDraftOwnerPhat Nguyen (Inventory + BO)
Date2026-05-10Versionv0.1
Packages@nx/inventory · apps/bo · @nx/signalURDSTK · TRK · LOC

TL;DR

Cho phép onboarding ops hoặc một merchant nạp hàng loạt tồn ngày-1 từ bảng tính, post nó thành một ticket STOCK_IN có thể audit cho mỗi địa điểm trong dưới 30 phút với ≤500 SKU. Kết quả: mọi merchant đang chuyển đổi bắt đầu với tồn chính xác - không nhập tay từng ticket, không vendor ảo, không PO giả làm bẩn danh mục.

1. Bối cảnh & Vấn đề

Mô hình dữ liệu kho đã hỗ trợ ghi tồn đầu kỳ - một InventoryTicket kiểu STOCK_IN không tham chiếu PO. Cái còn thiếu là cách đưa nó vào nhanh: không có UX, không có công cụ import bảng tính, và không có luồng dẫn dắt. Hôm nay con đường duy nhất là tạo từng ticket STOCK_IN theo từng dòng qua UI CRUD, điều này không dùng được khi chuyển đổi hàng trăm SKU.

Nỗi đau là cụ thể và đang diễn ra:

  • Đội chuyển đổi từ POS365 tốn hàng giờ nhập tay tồn.
  • Một số operator giả "vendor ảo" cùng một PO giả để dựa vào luồng PO → STOCK_IN sẵn có, làm bẩn danh sách vendor.
  • Nhiều merchant bỏ qua cách lách hoàn toàn, nên tháng đầu tiên báo cáo của họ sai.

Vì sao bây giờ: merchant thật đang chuyển đổi từ POS365 trong đợt WK20-22. "Khởi tạo tồn kho" là một luồng chủ lực của POS365, và sự vắng mặt của nó trong KICKO chặn mọi cuộc chuyển đổi. Primitive dữ liệu (InventoryTicket + STOCK_IN + originReferenceType) đã sẵn sàng, và các nền tảng WK19 (Material aggregate, merchant scope, zod hardening) đã hoàn tất - chỉ còn UX và luồng import.

2. Mục tiêu & Ngoài phạm vi

Mục tiêu

  • Onboarding ops hoặc một merchant upload tồn đầu kỳ cho ≤500 SKU trong ≤30 phút.
  • Mỗi lần upload tạo một InventoryTicket(type=STOCK_IN, originReferenceType='OpeningBalance') có thể audit cho mỗi địa điểm.
  • Không vendor ảo và không PO giả trong luồng.
  • Đội chuyển đổi có thể dán một bản export POS365 với ≤5 chỉnh sửa ánh xạ cột.

Ngoài phạm vi

  • Một kiểu InventoryTicket mới - tồn đầu kỳ tái dùng STOCK_IN + bộ phân biệt originReferenceType.
  • Tự động kéo từ một API POS365 - v1 chỉ upload bảng tính thủ công.
  • Tạo nguyên vật liệu - thuộc về phần carry-over nguyên vật liệu, không phải feature này.
  • Trường cost basis / pricing - thuộc về PriceList, riêng biệt.
  • Kiểm kê định kỳ / cycle count - đã có kiểu ticket CYCLE_COUNTPhiếu kho bao phủ.
  • Theo dõi lot / serial / hạn dùng lúc init - dùng một ticket ADJUSTMENT sau init.

3. Success Metrics

MetricMục tiêu / tín hiệu
Merchant có tồn đầu kỳ được post trước SaleOrder đầu tiên100% (checklist onboarding)
Thời lượng init (≤200 SKU, một địa điểm)<30 phút P95 (telemetry upload-start → confirm)
Tỉ lệ validation pass ngay lần đầu≥70% (log lỗi upload, hàng tuần)
Ticket re-init / hiệu chỉnh trong 24h<10% (đếm InventoryTicket theo originReferenceType)
Định tính onboarding ops"dễ hơn POS365" (retro onboarding)

4. Personas & Use Cases

PersonaMục tiêu trong feature này
Onboarding ops (BO, nội bộ)Upload bảng tính của merchant đang chuyển đổi, sửa vài lỗi dòng, confirm
Chủ merchant (BO, tự phục vụ)Đếm tồn cửa hàng thủ công, điền template tải về, upload, confirm
Đội chuyển đổiOnboard nhiều merchant POS365 trong một tuần với một luồng giống hệt, lặp lại được
Auditor (sau khi xảy ra)Lọc các ticket có originReferenceType='OpeningBalance' để thấy tồn đầu kỳ như một dòng

Core scenarios: tải template → điền / dán export POS365 → upload → preview các dòng đã parse với lỗi theo từng dòng → confirm → một ticket STOCK_IN được post và tồn tăng cùng một audit trail.

5. User Stories

  • onboarding ops, tôi muốn upload một bảng tính tồn đầu kỳ, để tôi có thể khởi tạo tồn của một merchant trong vài phút thay vì hàng giờ.
  • chủ merchant, tôi muốn một template tải về để điền, để tôi có thể tự phục vụ việc đếm đầu kỳ của mình.
  • onboarding ops, tôi muốn preview các dòng đã parse với lỗi theo từng dòng trước khi post, để tôi có thể sửa dữ liệu mà không bị post thất bại.
  • onboarding ops, tôi muốn bỏ qua các dòng lỗi và post phần còn lại, để một dòng lỗi không chặn cả lần upload.
  • đội chuyển đổi, tôi muốn upload lại cùng một file trả về cùng một ticket, để các lần thử lại không post tồn hai lần.
  • auditor, tôi muốn tồn đầu kỳ là một ticket nhận diện được cho mỗi địa điểm, để tôi có thể thấy tồn ngày-1 như một entry truy vết được.

6. Functional Requirements

#Yêu cầuURD ref
FR-1Tồn đầu kỳ post thành InventoryTicket(type=STOCK_IN) với originReferenceType='OpeningBalance', originReferenceId=null - không kiểu ticket mớiURD-STK-004 · URD-TRK-003
FR-2Một InventoryTicketItem cho mỗi (material, location, qty, uom); ticket chạy DRAFT → IN_PROGRESS → COMPLETED (không cổng submit/approve)URD-TKT-002..003
FR-3Một template bảng tính tải về với các cột material_code, location_code, qty, uom_code, noteURD-STK-004
FR-4Upload stream và parse các dòng, validate chúng, và tạo ticket ở DRAFT; preview trả về các dòng đã parse cộng lỗi theo từng dòngURD-STK-006
FR-5Confirm chuyển DRAFT → IN_PROGRESS → COMPLETED và post InventoryTracking; body mang { skipBadRows: boolean }URD-STK-004 · URD-TRK-001
FR-6Post tăng tồn theo từng dòng và ghi một tracking entry bất biến, idempotent theo (referenceType, referenceId)URD-TRK-001..004
FR-7Số lượng dùng độ chính xác thập phân (4 chữ số) và phải > 0URD-STK-003
FR-8uom_code phải quy đổi được về đơn vị cơ sở của material; dòng (material, location) trùng được gộp với cảnh báo lấy-giá-trị-cuốiURD-STK-003
FR-9Upload bị chặn khi merchant có bất kỳ SaleOrder nào (chuyển hướng sang ADJUSTMENT), hoặc khi đã tồn tại một STOCK_IN tồn-đầu-kỳ đã post cho cùng (merchantId, locationId) trừ khi ticket trước bị hủyURD-STK-004 · URD-CON-004
FR-10Biên nhận thành công hiển thị định danh ticket (ITI-…), số dòng, và một liên kết tới chi tiết ticketURD-STK-004

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ì lặp lại.

7. Non-Functional Requirements

Khía cạnhYêu cầu
Toàn vẹn dữ liệuPost là transactional - cập nhật ticket + N tracking inserts trong một transaction; một lần post một-phần là không thể. Validation chạy ngoài transaction
Bất biếnPost ghi qua cùng đường tracking như một STOCK_IN thường; các tracking entry chỉ ghi thêm (append-only)
IdempotentKhóa upload opening_balance:{merchantId}:{sha256(file)} trả về cùng ticket khi upload lại; lần confirm thứ hai trả về ticket COMPLETED đã có
Tenancy & authzEndpoint được scope theo merchant (không init xuyên merchant); gated bởi một permission chi tiết Inventory.openingBalance gắn cho OPERATOR mặc định
Hiệu năng / quy mô1k dòng < 5 giây end-to-end trên staging; 10k dòng < 30 giây qua một parser stream tránh OOM
ObservabilityLog merchantId, ticketId, rowCount, errorCount, durationMs (IGNIS key: %s); emit Kafka inventory.opening-balance.posted sau commit cho signal + reporting
i18nNhãn/trạng thái hướng người dùng song ngữ ({ en, vi })

8. UX & Flows

Các màn hình chính (trong apps/bo, tại /inventory/opening-balance): một empty state với CTA "Lấy template", drop zone, và liên kết "Vì sao tôi bị khóa?"; một spinner parse; một preview sạch với banner "Sẵn sàng post"; một preview lỗi với tooltip theo từng dòng và một toggle "Bỏ qua dòng lỗi"; một progress bar khi post; một card biên nhận thành công; và một card bị khóa (khi đã có SaleOrder) liên kết sâu tới luồng ADJUSTMENT.

9. Data & Domain

EntityVai trò
InventoryTicketTài liệu tồn đầu kỳ - type=STOCK_IN, originReferenceType='OpeningBalance', một cho mỗi địa điểm
InventoryTicketItemMột dòng - (material, location, qty, uom, note)
InventoryTrackingEntry biến động bất biến được ghi theo từng dòng khi confirm
InventoryStockBản ghi tồn được tăng tại địa điểm của dòng
OPENING_BALANCE_ORIGIN_TYPEMột hằng số inventory mới ('OpeningBalance') dùng làm bộ phân biệt ticket - không bảng mới, không cột mới

Chỉ ở mức khái niệm - schema và bất biến đầy đủ trong domain model kho.

10. Phụ thuộc & Giả định

Phụ thuộc vào

  • Mức tồn & audit biến động (URD-STK · URD-TRK) - post xây trên các primitive tồn và tracking cùng tính idempotent của chúng.
  • Phiếu kho (URD-TKT) - tái dùng InventoryTicketService cho vòng đời; không triển khai song song.
  • Địa điểm kho (URD-LOC) - mỗi dòng rơi vào một địa điểm của merchant.
  • Nguyên vật liệu (URD-MAT) - material_code phải đã tồn tại cho merchant; feature này không tạo nguyên vật liệu.
  • Signal (@nx/signal) - emit event Kafka sau commit được signal và reporting tiêu thụ; BO subscribe observation/inventory/inventory-ticket để cập nhật trạng thái preview trực tiếp.

Giả định

  • Nguyên vật liệu và địa điểm được tham chiếu bởi upload đã tồn tại cho merchant.
  • Merchant chưa đặt SaleOrder đầu tiên (tồn đầu kỳ là một thao tác ngày-1).
  • Onboarding có sẵn số đếm tồn của merchant trong một bảng tính (hoặc một export POS365).

11. Rủi ro & Câu hỏi mở

Rủi ro / câu hỏiGiảm thiểu / trạng thái
Chính sách khóa - chặn sau SaleOrder đầu tiên, hay sau bất kỳ tracking row non-INIT nào?Bản nháp hiện tại: chặn sau SaleOrder đầu tiên
Re-init trước khi bán (hủy cũ + post mới) hay một-lần nghiêm ngặt?Bản nháp hiện tại: cho phép re-init trước khi bán
Đa kho - một upload cho mọi địa điểm hay một cho mỗi địa điểm?Bản nháp hiện tại: một upload với cột location_code
Định dạng export POS365 - khớp từng cột hay một bộ chuyển đổi?Mở - quyết định trong đợt chuyển đổi WK20
UoM trộn trong một dòng - chấp nhận nếu quy đổi được hay buộc đơn vị cơ sở?Bản nháp hiện tại: chấp nhận nếu quy đổi được về UoM cơ sở
Tồn đầu kỳ như một bộ lọc danh sách InventoryTicket riêng hay chỉ qua query originReferenceType?Mở

12. Kế hoạch Phát hành & Tiêu chí Ra mắt

Khía cạnhKế hoạch
PhaseP1 (nền tảng) - xem danh mục feature URD
RolloutSandbox nội bộ (WK20) → 1 merchant pilot VNPAY (WK21) → GA bật-mặc-định cho merchant mới, opt-in cho merchant hiện hữu (WK22+); sau flag theo merchant inventory.openingBalance
MigrationKhông - zero bảng/cột mới; tái dùng InventoryTicket + InventoryTicketItem
Tiêu chí ra mắtMột init 100 dòng hoàn tất trong <15 phút cho onboarding ops; 0 bug nghiêm trọng sau pilot 1 tuần; metric re-init / hết hàng trong mục tiêu
Giám sátThời lượng init, tỉ lệ validation pass lần đầu, tỉ lệ re-init theo originReferenceType, số ticket đã post; độ trễ Kafka inventory.opening-balance.posted

13. FAQ

Việc này có thêm một kiểu inventory ticket mới không? Không - tồn đầu kỳ tái dùng STOCK_IN với originReferenceType='OpeningBalance' làm bộ phân biệt. Zero bảng mới, zero cột mới.

Tôi có thể import tồn đầu kỳ qua một đơn mua hàng không? Không - không có cách lách vendor-ảo/PO-giả. Tồn đầu kỳ là luồng riêng của nó; việc mua hàng đi qua Đơn mua hàng.

Điều gì xảy ra nếu một dòng bị lỗi? Preview hiển thị lỗi theo từng dòng. Bạn có thể sửa bảng tính và upload lại, hoặc bật toggle "Bỏ qua dòng lỗi" và post các dòng hợp lệ.

Nếu tôi upload cùng một file hai lần? Idempotent - cùng một file (theo SHA-256) trả về cùng một ticket, và lần confirm thứ hai trả về ticket COMPLETED đã có. Tồn không bị post hai lần.

Vì sao tôi bị khóa? Merchant đã có một SaleOrder, nên tồn đầu kỳ không còn là công cụ đúng - dùng một ticket ADJUSTMENT thay thế. Card bị khóa liên kết sâu tới luồng đó.

Nó có tạo nguyên vật liệu không? Không - material_code phải đã tồn tại cho merchant. Tạo nguyên vật liệu là một carry-over riêng.

References

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