Skip to content

PRD: Khởi tạo tồn kho — Import số dư tồn đầu kỳ

ModuleCORE-06Trạng tháiDraft
Ngày2026-05-10Tác giảPhat Nguyen

Team chủ trì: inventory + BO · Stakeholder: inventory, BO frontend, signal, ops, onboarding

1. Vấn đề

Data model đã hỗ trợ ghi nhận tồn kho ban đầu (một ticket STOCK_IN không kèm tham chiếu PO). Nhưng chưa có UX, chưa có Excel importer, chưa có flow để nhập 200 SKU nhanh chóng. Hiện tại con đường duy nhất là tạo từng ticket STOCK_IN theo dòng qua UI CRUD có sẵn, vốn không thể dùng cho migration.

Triệu chứng:

  • Team migration từ POS365 mất hàng giờ gõ tay.
  • Một số operator tạo "vendor ảo" + PO ảo để mượn flow PO→STOCK_IN có sẵn → vendor list bị bẩn.
  • Nhiều merchant bỏ qua workaround → tháng đầu báo cáo sai.

2. Tại sao bây giờ

Tín hiệuChi tiết
Merchant thật migrate từ POS365Lứa đầu WK20-22
Khoảng cách parity với POS365"Khởi tạo tồn kho" là flow chủ lực ở POS365; thiếu ở BANA chặn mọi migration
Data model đã sẵnInventoryTicket + STOCK_IN + originReferenceType đã đủ primitive — chỉ cần công việc UX/import
Dependency WK19 đã landMaterial aggregate (BANA-1040), merchant scope (BANA-1041), zod hardening (BANA-1042) — nền tảng vững chắc

3. Mục tiêu

  • Onboarding ops hoặc merchant upload số dư đầu kỳ cho ≤500 SKU trong ≤30 phút.
  • Kết quả là một InventoryTicket(type=STOCK_IN, originReferenceType='OpeningBalance') duy nhất, audit được, mỗi địa điểm.
  • Không có vendor ảo, không có PO ảo.
  • Team migration paste được export POS365 với ≤5 chỉnh sửa ánh xạ cột.

4. Không-mục-tiêu

  • Type InventoryTicket mới (tái sử dụng discriminator STOCK_IN + originReferenceType).
  • Auto-pull từ API POS365 (chỉ xlsx thủ công ở v1).
  • Tạo material (đã được phủ bởi carry-over riêng: BANA-890–894).
  • Trường cost basis / pricing (PriceList là feature riêng).
  • Stock count định kỳ / cycle count (type CYCLE_COUNT có sẵn đã phủ).
  • Lot / serial / expiry tracking lúc init (dùng ADJUSTMENT sau init).

5. Chỉ số thành công

Chỉ sốMục tiêuNguồnCửa sổ
Merchant có số dư đầu kỳ được post trước SaleOrder đầu tiên100%onboarding checklistmỗi merchant
Thời gian init (≤200 SKU, một địa điểm)<30 phút P95telemetry upload-start → confirm30 ngày sau GA
Tỉ lệ pass validation lần đầu≥70%upload error logshàng tuần
Re-init / correction ticket trong 24h<10%đếm InventoryTicket theo originReferenceType30 ngày
Đánh giá định tính của onboarding ops"dễ hơn POS365"retro onboardingmỗi đợt onboarding

6. Người dùng & kịch bản

UserKịch bản
Onboarding ops (BO, nội bộ)Nhận xlsx từ merchant đang migrate → upload qua BO /inventory/opening-balance → fix 5–15 dòng lỗi → confirm
Chủ merchant (BO, self-service)Đếm tồn shop thủ công → điền template tải về → upload qua merchant settings → confirm
Team migrationOnboard 5 merchant POS365 trong một tuần → muốn flow giống nhau mỗi lần
Kiểm toán (về sau)Lọc ticket có originReferenceType='OpeningBalance' để xem số dư đầu kỳ như một dòng

7. Giải pháp

7.1 Tái sử dụng, không mở rộng

  • Ticket type: InventoryTicketTypes.STOCK_IN (không thêm enum mới).
  • Discriminator: originReferenceType = 'OpeningBalance', originReferenceId = null.
  • Status flow: chuẩn DRAFT → IN_PROGRESS → COMPLETED (bỏ qua submit/approve — số dư đầu kỳ do operator xác nhận, không qua cổng duyệt).
  • Lines: mỗi InventoryTicketItem cho một (material, location, qty, uom).

7.2 Excel template (tải về từ BO)

CộtBắt buộcGhi chú
material_codeYPhải tồn tại với merchant
location_codeYPhải tồn tại với merchant
qtyYĐộ chính xác float(value, 4); phải > 0
uom_codeYConvert được về base UoM của material
noteNFree text mỗi dòng

7.3 Flow

BướcAPIHành vi
1 UploadPOST /v1/api/inventory/opening-balance/uploadStream parse, validate dòng, tạo InventoryTicketDRAFT
2 PreviewGET /v1/api/inventory/opening-balance/:ticketIdTrả về dòng đã parse + lỗi từng dòng
3 ConfirmPOST /v1/api/inventory/opening-balance/:ticketId/confirmChuyển DRAFT → IN_PROGRESS → COMPLETED, post InventoryTracking. Body: { skipBadRows: boolean }
4 ReceiptUI hiện ticket identifier (ITI-…), số dòng, link tới ticket detail

7.4 Quy tắc validation

Kiểm traCấp độ lỗi
material_code tồn tại trong merchant scoperow error
location_code tồn tại với merchantrow error
uom_code convert được về base UoM của materialrow error (kèm gợi ý auto-convert)
qty > 0, ≤ float(4)row error
Trùng (material, location) trong uploadmerge với warning, lấy giá trị cuối
Merchant đã có SaleOrder bất kỳblock cấp upload ("dùng ADJUSTMENT thay vì")
Đã có ticket STOCK_IN với originReferenceType='OpeningBalance' cho cùng (merchantId, locationId)block cấp upload trừ khi ticket trước đã bị cancel

8. UX

Trạng tháiHành vi
EmptyCTA "Lấy template" + drop zone + link "Vì sao tôi bị khóa?"
Đang parseSpinner + đếm dòng
Preview sạchTable, banner xanh "Sẵn sàng để post", CTA Confirm
Preview có lỗiTooltip mỗi dòng; toggle "Bỏ qua dòng lỗi" mở nút Confirm
Đang postProgress bar, label theo bước (validate → write tracking → finalize)
Thành côngCard receipt: ticket identifier, X dòng đã post, link tới detail
LockedCard mờ giải thích SaleOrder đầu tiên đã tồn tại, deep link sang flow ADJUSTMENT

Wireframes: design pass với Thuong trong WK20 (Figma).

9. Kỹ thuật

9.1 Schema

  • Zero bảng mới, zero cột mới. Tái sử dụng InventoryTicket + InventoryTicketItem.
  • Constant mới trong inventory package: OPENING_BALANCE_ORIGIN_TYPE = 'OpeningBalance' (dùng cho discriminator).

9.2 Service layer

  • Service mới: OpeningBalanceServicepackages/inventory/src/services/opening-balance.service.ts.
  • Kế thừa MerchantScopedService (BANA-1041).
  • Delegate sang InventoryTicketService có sẵn cho lifecycle ticket — không implement song song.
  • Posting ghi qua đúng đường InventoryTrackingService.complete như một STOCK_IN bình thường.

9.3 Worker / parser

  • Parse xlsx bằng stream reader để xử lý 10k dòng không OOM.
  • Validation: batch-fetch material code + location code bằng query IN (...) (repo có sẵn).
  • Transactional: validate ngoài TX, post trong một TX (cập nhật ticket + N tracking insert). Partial post là không thể.

9.4 Idempotency

  • Upload key: opening_balance:{merchantId}:{sha256(file)} → upload lại cùng file trả về cùng ticket.
  • Confirm key: idempotent — confirm lần hai trả về ticket COMPLETED đã có.

9.5 Observability

  • Log merchantId, ticketId, rowCount, errorCount, durationMs (theo key: %s của IGNIS).
  • Emit Kafka inventory.opening-balance.posted post-commit; downstream listener: signal, reporting.
  • BO subscribe observation/inventory/inventory-ticket (topic có sẵn) để live-update trạng thái preview.

9.6 Mục tiêu performance

  • 1k dòng → <5 giây end-to-end trên staging.
  • 10k dòng → <30 giây với stream parser.

9.7 Security

  • Casbin permission: Inventory.openingBalance (perm fine-grained mới, gán mặc định cho role OPERATOR).
  • Endpoint scoped qua MerchantScopedService — không có init cross-merchant.
  • Audit: createdBy / updatedBy ghi bởi generateUserAuditColumnDefs (đã có trên schema).

10. Rollout

StageCửa sổĐối tượngGate
InternalWK20Sandbox merchant NEXPANDO, flag ONOnboarding ops làm xong init 100 dòng trong <15 phút
PilotWK211 merchant pilot VNPAY0 bug critical sau 1 tuần
GAWK22+Default ON cho merchant mới; opt-in cho merchant cũStockout / re-init metric đạt mục tiêu

Feature flag: inventory.openingBalance (boolean per-merchant trong merchant config).

11. Câu hỏi mở

  • Chính sách khóa: block init sau SaleOrder đầu tiên hay sau bất kỳ dòng InventoryTracking non-INIT nào? (draft hiện tại = SaleOrder đầu tiên)
  • Cho phép re-init trước sale (cancel cũ + post mới) hay strict one-shot? (draft hiện tại = re-init OK trước sale)
  • Multi-warehouse: một upload bao tất cả địa điểm, hay một upload mỗi địa điểm? (draft hiện tại = một upload, cột location_code)
  • Format export POS365 — match cột với cột, hay chúng ta cung cấp converter?
  • Trộn UoM trong cùng dòng: chấp nhận nếu convert được, hay bắt buộc base UoM? (draft hiện tại = chấp nhận nếu convert được)
  • Số dư đầu kỳ nên hiển thị như một filter riêng trên list InventoryTicket, hay chỉ qua query originReferenceType?

12. Phụ lục

  • Schema có sẵn: packages/core/src/models/schemas/inventory/inventory-ticket/{schema,constants}.ts
  • Service có sẵn: packages/inventory/src/services/inventory-ticket.service.ts
  • Tracking layer: packages/inventory/src/services/inventory-tracking.service.ts
  • Công việc WK19 liên quan: BANA-1040 (material aggregate), BANA-1041 (merchant scope), BANA-1042 (zod hardening)
  • Tham khảo POS365: "Khởi tạo tồn kho" (Operations → Inventory)

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