Skip to content

QA Test Plan: Kho vật tư

Feature slug: material-warehouseVersion: 3.0 Ngày: 2026-03-27 Complexity: L (Large)


D1) Coverage Summary

LoạiSố lượngChi tiết
FR27FR-MW-001 ~ FR-MW-003, FR-PC-001 ~ FR-PC-003, FR-UU-001 ~ FR-UU-002, FR-SI-001 ~ FR-SI-002, FR-SM-001 ~ FR-SM-003, FR-AD-001 ~ FR-AD-002, FR-SK-001, FR-AL-001, FR-FI-001, FR-BATCH-001 ~ FR-BATCH-002, FR-HSD-001 ~ FR-HSD-002, FR-FIFO-001 ~ FR-FIFO-003, FR-DISPOSE-001, FR-022
BR13BR-01 ~ BR-13
Edge Cases15EC-01 ~ EC-15
Formulas6FORMULA-001 ~ FORMULA-006
Test Cases85TC-MW-001~005, TC-PC-001~005, TC-UU-001~003, TC-SI-001~003, TC-SM-001~005, TC-AD-001~005, TC-SK-001~003, TC-AL-001~002, TC-FI-001~002, TC-PM-001~003, TC-EC-001~007, TC-FML-001~006, TC-BATCH-001~002, TC-FIFO-001~003, TC-HSD-001~003, TC-DISPOSE-001, TC-REVERSE-001~002, TC-PRICE-001~004, TC-TRF-001~017, TC-EXP-001~004
Seed Data12DS-001 ~ DS-009, DS-BATCH-001, DS-BATCH-002, DS-TRF-001
Coverage100%Mỗi FR có >=1 TC, mỗi EC có >=1 TC, mỗi Formula có >=1 TC

D2) Requirements x TC Matrix

FR Naming Mapping (qa-test-plan → prd.md)

qa-test-plan dùng prefix rõ domain (FR-MW, FR-PC, ...). prd.md dùng FR-001~FR-021. Bảng dưới mapping 1:1.

qa-test-plan IDprd.md IDTên (khớp prd.md)
FR-MW-001FR-001Tạo kho vật tư (auto-create)
FR-MW-002FR-002Xem danh sách kho vật tư
FR-MW-003FR-003Quản lý vật tư — thêm/ẩn
FR-PC-001FR-004Cấu hình giá vật tư per product
FR-PC-002FR-005Cập nhật giá (versioning)
FR-UU-001FR-006Cấu hình đơn vị quy đổi
FR-SI-001FR-007Nhập tồn kho manual (tạo lô)
FR-SI-002FR-008Nhập tồn kho bằng Excel (tạo lô per dòng)
FR-SM-001FR-009Chọn ĐVT khi thêm vật tư subtask
FR-SM-002FR-010Two-Phase Pricing (ước tính + finalize)
FR-SM-003FR-011Ghi nhận hao hụt kỹ thuật
FR-AD-001FR-012Auto-deduct khi subtask done (FIFO)
FR-AD-002FR-013Auto-reverse khi undo done
FR-SK-001FR-014Kiểm kê + điều chỉnh
FR-AL-001FR-015Cảnh báo tồn kho thấp
FR-FI-001FR-016Chi phí vật tư sidebar tài chính
FR-FI-002FR-017Import cấu hình Excel
FR-BATCH-001FR-018Quản lý lô hàng (batch lifecycle)
FR-BATCH-002FR-018Nhập cùng giá cùng ngày tách lô
FR-HSD-001FR-019Auto-lock lô hết hạn
FR-HSD-002FR-019Cảnh báo 90 ngày HSD
FR-FIFO-001FR-020FIFO auto-deduct 1 lô
FR-FIFO-002FR-020FIFO split 2+ lô
FR-FIFO-003FR-020Block khi không đủ tồn
FR-DISPOSE-001FR-021Hủy lô (disposal)
FR-TRF-001FR-022Chuyển kho chi nhánh → Kho vật tư
PRD FRQA TC-IDsPriority
FR-001 Tạo kho vật tưTC-MW-001, TC-MW-002Critical
FR-002 Xem danh sáchTC-MW-003High
FR-003 Quản lý vật tư — thêm/ẩnTC-MW-004, TC-MW-005High
FR-004 Cấu hình giáTC-PC-001, TC-PC-002, TC-PC-005Critical
FR-005 Cập nhật giá (versioning)TC-PC-003, TC-PC-004Critical
FR-006 Cấu hình ĐVT quy đổiTC-UU-001, TC-UU-002, TC-UU-003Critical
FR-007 Nhập kho manual (tạo lô)TC-SI-001, TC-SI-002Critical
FR-008 Import Excel (tạo lô per dòng)TC-SI-003High
FR-009 Chọn ĐVT subtaskTC-SM-001, TC-SM-003Critical
FR-010 Two-Phase PricingTC-SM-002, TC-PRICE-001, TC-PRICE-002, TC-PRICE-003, TC-PRICE-004Critical
FR-011 Hao hụt kỹ thuậtTC-SM-004High
FR-012 Auto-deduct (FIFO)TC-AD-001, TC-AD-002, TC-FIFO-001, TC-FIFO-002, TC-FIFO-003Critical
FR-013 Auto-reverseTC-AD-003, TC-AD-004, TC-AD-005, TC-REVERSE-001, TC-REVERSE-002Critical
FR-014 Kiểm kê + điều chỉnhTC-SK-001, TC-SK-002, TC-SK-003High
FR-015 Cảnh báo tồn khoTC-AL-001, TC-AL-002High
FR-016 Sidebar tài chínhTC-FI-001, TC-FI-002High
FR-017 Import cấu hình ExcelTC-CFG-001High
FR-018 Quản lý lô hàngTC-BATCH-001, TC-BATCH-002Critical
FR-019 HSD — cảnh báo + auto-lockTC-HSD-001, TC-HSD-002, TC-HSD-003Critical
FR-020 FIFO xuất khoTC-FIFO-001, TC-FIFO-002, TC-FIFO-003Critical
FR-021 Hủy lô (disposal)TC-DISPOSE-001Critical
FR-022 Chuyển kho chi nhánh → Kho vật tưTC-TRF-001~017Critical
BR-01 ~ BR-19TC-EC-001~005 (covered within edge cases + formula tests)High
EC-01 ~ EC-21TC-EC-001~005, TC-AD-003~005, TC-PM-001~003High
FORMULA-001 ~ 006TC-FML-001~006Critical

D3) Seed Data

DS-001: Kho vật tư chi nhánh

sql
-- Tạo kho vật tư cho chi nhánh Quận 1
INSERT INTO material_warehouse (id, branch_id, name, is_active, created_by)
VALUES (
  'mw-test-001',
  'branch-q1-001',
  'Kho vật tư Quận 1',
  true,
  'admin-test'
);

DS-002: Material Price Config — Serum Laser X

sql
INSERT INTO material_price_config (
  id, product_id, branch_id,
  stock_unit, source_price, source_quantity, stock_unit_price,
  wastage_rate, costing_method, min_stock,
  effective_from, created_by
) VALUES (
  'mpc-serum-001',
  'prod-serum-001',   -- Serum Laser X (SP001)
  NULL,               -- global
  'ml',
  2000000,            -- 2,000,000đ / chai
  500,                -- 500ml / chai
  4081.6327,          -- = 2,000,000 / (500 × 0.98)
  0.02,               -- 2% hao hụt
  'fifo',
  50,                 -- cảnh báo khi ≤ 50ml
  '2026-01-01',
  'admin-test'
);

DS-003: Material Price Config — Gel làm mát, Mask collagen, Bông tẩy trang

sql
-- Gel làm mát (SP042)
INSERT INTO material_price_config (
  id, product_id, branch_id,
  stock_unit, source_price, source_quantity, stock_unit_price,
  wastage_rate, min_stock, effective_from, created_by
) VALUES (
  'mpc-gel-001', 'prod-gel-042', NULL,
  'g', 200000, 100, 2000.0000,
  0, 30, '2026-01-01', 'admin-test'
);

-- Mask collagen (SP018) — discrete
INSERT INTO material_price_config (
  id, product_id, branch_id,
  stock_unit, source_price, source_quantity, stock_unit_price,
  wastage_rate, min_stock, effective_from, created_by
) VALUES (
  'mpc-mask-001', 'prod-mask-018', NULL,
  'miếng', 300000, 10, 30000.0000,
  0, 50, '2026-01-01', 'admin-test'
);

-- Bông tẩy trang (SP023) — discrete
INSERT INTO material_price_config (
  id, product_id, branch_id,
  stock_unit, source_price, source_quantity, stock_unit_price,
  wastage_rate, min_stock, effective_from, created_by
) VALUES (
  'mpc-bong-001', 'prod-bong-023', NULL,
  'miếng', 50000, 100, 500.0000,
  0, 100, '2026-01-01', 'admin-test'
);

DS-004: Usage Units — Serum Laser X (giọt, muỗng, ml)

sql
INSERT INTO material_usage_unit (
  id, config_id, usage_unit_name, to_stock_factor, usage_unit_price,
  uom_category, is_discrete, is_default, sort_order, created_by
) VALUES
  ('muu-serum-giot', 'mpc-serum-001', 'giọt', 0.05, 204.0816, 'volume', false, true, 1, 'admin-test'),
  ('muu-serum-muong', 'mpc-serum-001', 'muỗng', 5, 20408.1633, 'volume', false, false, 2, 'admin-test'),
  ('muu-serum-ml', 'mpc-serum-001', 'ml', 1, 4081.6327, 'volume', false, false, 3, 'admin-test');

-- Mask — discrete (miếng)
INSERT INTO material_usage_unit (
  id, config_id, usage_unit_name, to_stock_factor, usage_unit_price,
  uom_category, is_discrete, is_default, sort_order, created_by
) VALUES
  ('muu-mask-mieng', 'mpc-mask-001', 'miếng', 1, 30000.0000, 'count', true, true, 1, 'admin-test');

-- Gel — tube (1 tube = 15g)
INSERT INTO material_usage_unit (
  id, config_id, usage_unit_name, to_stock_factor, usage_unit_price,
  uom_category, is_discrete, is_default, sort_order, created_by
) VALUES
  ('muu-gel-tube', 'mpc-gel-001', 'tube', 15, 30000.0000, 'weight', false, true, 1, 'admin-test'),
  ('muu-gel-g', 'mpc-gel-001', 'g', 1, 2000.0000, 'weight', false, false, 2, 'admin-test');

DS-005: Stock Movements — Nhập kho ban đầu

sql
INSERT INTO material_stock_movement (
  id, warehouse_id, product_id,
  quantity_change, running_balance, stock_unit,
  source_type, note, created_by
) VALUES
  ('msm-init-001', 'mw-test-001', 'prod-serum-001', 5000, 5000, 'ml', 'manual', 'Kiểm kê đầu kỳ tháng 3', 'admin-test'),
  ('msm-init-002', 'mw-test-001', 'prod-gel-042', 800, 800, 'g', 'manual', 'Kiểm kê đầu kỳ tháng 3', 'admin-test'),
  ('msm-init-003', 'mw-test-001', 'prod-mask-018', 49, 49, 'miếng', 'manual', 'Kiểm kê đầu kỳ tháng 3', 'admin-test'),
  ('msm-init-004', 'mw-test-001', 'prod-bong-023', 200, 200, 'miếng', 'manual', 'Kiểm kê đầu kỳ tháng 3', 'admin-test');

DS-006: Subtask với materials (cho test auto-deduct)

sql
-- Task cha
INSERT INTO project_task (id, project_id, name, status_id, created_by)
VALUES ('task-mw-parent-001', 'proj-test-001', 'Laser Q-switch tàn nhang - Buổi 1 - KH Nguyễn Thị Hoa', 'new_branch', 'test-user');

-- 2 subtask
INSERT INTO project_task (id, project_id, parent_id, name, status_id, created_by)
VALUES
  ('task-mw-sub-001', 'proj-test-001', 'task-mw-parent-001', 'BS - Laser sắc tố nám', 'new_branch', 'test-user'),
  ('task-mw-sub-002', 'proj-test-001', 'task-mw-parent-001', 'Chăm sóc da cơ bản', 'new_branch', 'test-user');

-- Materials cho subtask 1 (có giá snapshot)
INSERT INTO project_task_material (
  task_id, product_id, product_name, product_sku, product_unit, quantity,
  usage_unit, unit_price, amount, to_stock_factor, stock_equivalent,
  total_stock_equivalent, config_version_id, created_by
) VALUES
  ('task-mw-sub-001', 'prod-serum-001', 'Serum Laser X', 'SP001', 'ml', 3,
   'giọt', 204.0816, 612, 0.05, 0.15, 0.15, 'mpc-serum-001', 'test-user'),
  ('task-mw-sub-001', 'prod-gel-042', 'Gel làm mát', 'SP042', 'g', 1,
   'tube', 30000.0000, 30000, 15, 15, 15, 'mpc-gel-001', 'test-user');

-- Materials cho subtask 2
INSERT INTO project_task_material (
  task_id, product_id, product_name, product_sku, product_unit, quantity,
  usage_unit, unit_price, amount, to_stock_factor, stock_equivalent,
  total_stock_equivalent, config_version_id, created_by
) VALUES
  ('task-mw-sub-002', 'prod-serum-001', 'Serum Laser X', 'SP001', 'ml', 3,
   'giọt', 204.0816, 612, 0.05, 0.15, 0.15, 'mpc-serum-001', 'test-user'),
  ('task-mw-sub-002', 'prod-mask-018', 'Mask dưỡng da', 'SP018', 'miếng', 1,
   'miếng', 30000.0000, 30000, 1, 1, 1, 'mpc-mask-001', 'test-user');

DS-007: Product disabled (cho edge case test)

sql
-- Giả sử product 'prod-serum-001' bị disable sau khi đã config
-- Chạy trên ecommerce DB:
UPDATE product SET disabled = true WHERE id = 'prod-serum-001';

DS-009: Đơn hàng cũ có inventory_document (backward compat test)

sql
-- Task cha liên kết order cũ (trước deploy material-warehouse)
-- Order này dùng inventory_document capture (flow cũ)
INSERT INTO project_task (id, project_id, name, status_id, created_by)
VALUES ('task-mw-old-001', 'proj-test-001', 'Chăm sóc da cơ bản - KH cũ', 'done_branch', 'test-user');

-- Giả sử inventory_document đã có data capture cho task này
-- (table inventory_document thuộc module cũ, chỉ cần verify SELECT hoạt động)
-- Dùng để test TC-FI-002: sidebar tài chính backward compat
-- Nếu test env không có inventory_document data → hỏi DevOps import từ staging snapshot

DS-BATCH-001: Lô vật tư Serum — 2 lô FIFO

sql
-- Lô A: nhập 01/01, 500ml, giá 5,000đ/ml, HSD 30/06/2026
INSERT INTO material_batch (
  id, warehouse_id, product_id, batch_code,
  purchase_price, quantity, remaining_qty, stock_unit,
  expiry_date, status, created_at, created_by
) VALUES (
  'mb-serum-lot-a', 'mw-test-001', 'prod-serum-001', 'A01',
  5000.0000, 500, 500, 'ml',
  '2026-06-30', 'active', '2026-01-01 00:00:00+07', 'admin-test'
);

-- Lô B: nhập 01/02, 300ml, giá 6,000đ/ml, HSD 31/12/2026
INSERT INTO material_batch (
  id, warehouse_id, product_id, batch_code,
  purchase_price, quantity, remaining_qty, stock_unit,
  expiry_date, status, created_at, created_by
) VALUES (
  'mb-serum-lot-b', 'mw-test-001', 'prod-serum-001', 'B01',
  6000.0000, 300, 300, 'ml',
  '2026-12-31', 'active', '2026-02-01 00:00:00+07', 'admin-test'
);

DS-BATCH-002: Lô hết hạn — Mask Collagen

sql
-- Lô X: HSD = yesterday, remaining = 50 miếng
INSERT INTO material_batch (
  id, warehouse_id, product_id, batch_code,
  purchase_price, quantity, remaining_qty, stock_unit,
  expiry_date, status, created_at, created_by
) VALUES (
  'mb-mask-lot-x', 'mw-test-001', 'prod-mask-018', 'X01',
  30000.0000, 50, 50, 'miếng',
  (CURRENT_DATE - INTERVAL '1 day'), 'active', '2026-01-15 00:00:00+07', 'admin-test'
);

DS-TRF-001: Kho chi nhánh Q1 — 3 SP, mỗi SP 2 lô (cho test chuyển kho)

sql
-- Kho chi nhánh Quận 1 đã có 3 sản phẩm, mỗi SP có 2 lô (khác giá, khác HSD)
-- Giả sử product_supplying lưu lô hàng kho chi nhánh

-- SP001 Serum Laser X: Lô A (giá 500,000đ, HSD 06/2027, tồn 8 chai)
INSERT INTO product_supplying (
  id, branch_id, product_id, batch_code,
  price, quantity, remaining_qty, unit,
  expiry_date, status, created_at, created_by
) VALUES (
  'ps-serum-lot-a', 'branch-q1-001', 'prod-serum-001', 'BRA01',
  500000, 8, 8, 'chai',
  '2027-06-30', 'active', '2026-01-15 00:00:00+07', 'admin-test'
);

-- SP001 Serum Laser X: Lô B (giá 550,000đ, HSD 12/2027, tồn 5 chai)
INSERT INTO product_supplying (
  id, branch_id, product_id, batch_code,
  price, quantity, remaining_qty, unit,
  expiry_date, status, created_at, created_by
) VALUES (
  'ps-serum-lot-b', 'branch-q1-001', 'prod-serum-001', 'BRB01',
  550000, 5, 5, 'chai',
  '2027-12-31', 'active', '2026-02-10 00:00:00+07', 'admin-test'
);

-- SP042 Gel làm mát: Lô A (giá 200,000đ, HSD 09/2027, tồn 10 chai)
INSERT INTO product_supplying (
  id, branch_id, product_id, batch_code,
  price, quantity, remaining_qty, unit,
  expiry_date, status, created_at, created_by
) VALUES (
  'ps-gel-lot-a', 'branch-q1-001', 'prod-gel-042', 'BRA02',
  200000, 10, 10, 'chai',
  '2027-09-30', 'active', '2026-01-20 00:00:00+07', 'admin-test'
);

-- SP042 Gel làm mát: Lô B (giá 220,000đ, HSD 03/2028, tồn 6 chai)
INSERT INTO product_supplying (
  id, branch_id, product_id, batch_code,
  price, quantity, remaining_qty, unit,
  expiry_date, status, created_at, created_by
) VALUES (
  'ps-gel-lot-b', 'branch-q1-001', 'prod-gel-042', 'BRB02',
  220000, 6, 6, 'chai',
  '2028-03-31', 'active', '2026-03-01 00:00:00+07', 'admin-test'
);

-- SP018 Mask Collagen: Lô A (giá 1,500,000đ, HSD 06/2027, tồn 4 hộp)
INSERT INTO product_supplying (
  id, branch_id, product_id, batch_code,
  price, quantity, remaining_qty, unit,
  expiry_date, status, created_at, created_by
) VALUES (
  'ps-mask-lot-a', 'branch-q1-001', 'prod-mask-018', 'BRA03',
  1500000, 4, 4, 'hộp',
  '2027-06-30', 'active', '2026-02-01 00:00:00+07', 'admin-test'
);

-- SP018 Mask Collagen: Lô B (giá 1,600,000đ, HSD 12/2027, tồn 3 hộp)
INSERT INTO product_supplying (
  id, branch_id, product_id, batch_code,
  price, quantity, remaining_qty, unit,
  expiry_date, status, created_at, created_by
) VALUES (
  'ps-mask-lot-b', 'branch-q1-001', 'prod-mask-018', 'BRB03',
  1600000, 3, 3, 'hộp',
  '2027-12-31', 'active', '2026-03-10 00:00:00+07', 'admin-test'
);

-- Quy đổi đơn vị cho Serum: 1 chai = 500ml (cần có trong material_price_config)
-- Giả sử purchase_to_stock_factor = 500 (1 chai → 500ml) đã config trong material_price_config

DS-008: Subtask done (cho auto-deduct / readonly test)

sql
-- Mark subtask 1 done → trigger auto-deduct
UPDATE project_task
SET status_id = 'done_branch', done_at = now()
WHERE id = 'task-mw-sub-001';

-- Verify: 2 stock movements auto_deduct phải được tạo
-- SELECT * FROM material_stock_movement
--   WHERE source_type = 'auto_deduct'
--   AND source_reference_id IN (ptm IDs of task-mw-sub-001);

D4) Test Cases

Nhóm 1: Kho vật tư CRUD (TC-MW-001 ~ TC-MW-005)

TC-IDNhómMô tảPreconditionStepsExpectedPriorityFR-ref
TC-MW-001WarehouseTạo kho vật tư cho chi nhánhLogin Admin. Chi nhánh Quận 1 chưa có kho vật tư.1. Vào Kho > Kho vật tư 2. Click [+ Tạo kho] 3. Chọn chi nhánh "Quận 1" 4. Nhập tên "Kho vật tư Quận 1" 5. Bấm [Lưu]Record material_warehouse tạo thành công. branch_id = Quận 1, is_active = true. Hiển thị trong danh sách.CriticalFR-MW-001
TC-MW-002WarehouseTạo kho trùng chi nhánh — bị chặnDS-001 (đã có kho cho Quận 1)1. Vào Kho > Kho vật tư 2. Click [+ Tạo kho] 3. Chọn chi nhánh "Quận 1" 4. Bấm [Lưu]Error: "Chi nhánh đã có kho vật tư". UNIQUE(branch_id) constraint block.HighFR-MW-001, BR-01
TC-MW-003WarehouseXem danh sách vật tư + tìm kiếmDS-001 + DS-002 + DS-005 (4 vật tư có tồn kho)1. Vào Kho > Kho vật tư 2. Verify bảng hiện 4 dòng: Serum, Gel, Mask, Bông 3. Gõ "Serum" vào ô tìm kiếm 4. Verify chỉ còn 1 dòngBảng hiện đủ cột: #, Vật tư, Mã, ĐV kho, Giá/ĐV, Tồn kho, TT. Tìm kiếm lọc đúng theo tên/mã. Mask hiện badge đỏ (tồn 49 <= min 50).HighFR-MW-002
TC-MW-004WarehouseVô hiệu kho vật tưDS-001 (kho active)1. Vào chi tiết kho 2. Click [Vô hiệu] 3. Confirm dialogis_active = false. Không cho nhập kho, kiểm kê nữa. Subtask vẫn hiện data cũ (readonly).HighFR-MW-003
TC-MW-005WarehouseKho vật tư hiển thị đúng branch scope cho ManagerDS-001. Login Manager chi nhánh Quận 1.1. Vào Kho > Kho vật tư 2. Verify dropdown chi nhánhManager chỉ thấy kho của branch mình. Admin thấy tất cả + dropdown chọn branch.HighFR-MW-002, Permission
TC-EXP-001ExpandableSCR-01 expandable row hiện ĐVT sử dụng + giáSP có 3 ĐVT sử dụng (giọt, muỗng, ml), có ≥1 lô active1. Mở SCR-01 2. Click ▶ expand dòng SPHiện 3 ĐVT với giá tính từ lô FIFO cũ nhất: giọt = batch.unit_price × 0.05, muỗng = batch.unit_price × 5, ml = batch.unit_price × 1MediumFR-002
TC-EXP-002ExpandableSCR-01 expandable row hiện danh sách lô + FIFO indicatorSP có 3 lô active (Lô A cũ nhất, Lô B, Lô C)1. Mở SCR-01 2. Click ▶ expand 3. Xem danh sách lôHiện 3 lô: tên, tồn, giá/stock_unit, HSD. Lô A có indicator "► đang xuất (FIFO)"MediumFR-002

Nhóm 2: Price Config + Versioning (TC-PC-001 ~ TC-PC-005)

TC-IDNhómMô tảPreconditionStepsExpectedPriorityFR-ref
TC-PC-001PriceConfigThêm cấu hình giá mới cho sản phẩmDS-001. Product "Serum Laser X" chưa có config.1. Vào Kho vật tư > Click [+ Thêm vật tư] 2. Tìm chọn "Serum Laser X" 3. Nhập ĐV kho = "ml" 4. Nhập Giá nhập = 2,000,000đ 5. Nhập SL/đơn vị mua = 500 6. Nhập Hao hụt = 2% 7. Nhập Ngưỡng cảnh báo = 50ml 8. Bấm [Lưu]Record material_price_config tạo thành công. stock_unit_price = 4,081.6327 (auto-calc). effective_from = now(), effective_to = NULL. Hiển thị "Giá/ml: 4,081.63đ" trên form.CriticalFR-PC-001, FORMULA-001
TC-PC-002PriceConfigThêm config trùng product active — bị chặnDS-002 (Serum đã có config active)1. Click [+ Thêm vật tư] 2. Tìm chọn "Serum Laser X" 3. Nhập thông tin 4. Bấm [Lưu]Error: "Sản phẩm đã có cấu hình giá. Vui lòng cập nhật giá." DB unique index block.HighFR-PC-001, BR-01
TC-PC-003PriceConfigCập nhật giá (versioning)DS-002 (Serum config active, giá 2,000,000đ)1. Mở chi tiết Serum 2. Click [Cập nhật giá] 3. Sửa Giá nhập = 2,500,000đ 4. Bấm [Lưu]Config cũ: effective_to = now(). Config mới: effective_from = now(), effective_to = NULL. stock_unit_price mới = 2,500,000 / (500 x 0.98) = 5,102.0408. Lịch sử giá hiện 2 dòng.CriticalFR-PC-002, DEC-D02
TC-PC-004PriceConfigSubtask cũ giữ snapshot giá cũ sau cập nhậtDS-006 (subtask có materials + giá snapshot). Sau đó chạy TC-PC-003.1. Mở subtask "BS - Laser sắc tố nám" 2. Verify giá Serumunit_price vẫn = 204.0816đ/giọt (giá cũ snapshot). amount vẫn = 612đ. KHÔNG bị ảnh hưởng bởi giá mới.CriticalFR-PC-002, BR-03, DEC-D05
TC-PC-005PriceConfigHao hụt hệ thống ảnh hưởng giáDS-001 (chưa có config)1. Thêm config: Giá 2,000,000, SL 500, Hao hụt 0% 2. Verify: stock_unit_price = 4,000.0000 3. Sửa Hao hụt = 5% 4. Verify: stock_unit_price = 4,210.52630%: 2,000,000 / (500 x 1.00) = 4,000.0000. 5%: 2,000,000 / (500 x 0.95) = 4,210.5263. Hao hụt cao → giá per unit cao hơn.HighFR-PC-003, FORMULA-001, DEC-D03

Nhóm 3: Usage Unit quy đổi (TC-UU-001 ~ TC-UU-003)

TC-IDNhómMô tảPreconditionStepsExpectedPriorityFR-ref
TC-UU-001UsageUnitThêm đơn vị sử dụngDS-002 (Serum config active, stock_unit = ml)1. Mở form cấu hình Serum 2. Click [+ Thêm đơn vị] 3. Nhập Tên = "giọt" 4. Nhập Hệ số = 0.05 (1 giọt = 0.05ml) 5. Check "Mặc định" 6. Bấm [Lưu]Record material_usage_unit tạo thành công. usage_unit_price = 4,081.6327 x 0.05 = 204.0816đ (auto-calc). is_default = true. Hiển thị "Giá/giọt: 204.08đ".CriticalFR-UU-001, FORMULA-002
TC-UU-002UsageUnitThêm đơn vị discrete (miếng)DS-003 (Mask config active, stock_unit = miếng)1. Mở form cấu hình Mask 2. Click [+ Thêm đơn vị] 3. Nhập Tên = "miếng", Hệ số = 1 4. Check "Rời" (is_discrete = true) 5. Bấm [Lưu]is_discrete = true. Sau đó ở subtask: nhập SL = 1.5 → validation error "SL phải là số nguyên".CriticalFR-UU-001, BR-04, DEC-D07
TC-UU-003UsageUnitAuto-calc giá ĐVT khi sửa source_priceDS-004 (3 usage units cho Serum)1. Mở config Serum 2. Sửa Giá nhập 2,000,000 → 3,000,000 3. Verify giá ĐVT recalc realtime trên formstock_unit_price mới = 3,000,000 / (500 x 0.98) = 6,122.4490. giọt: 6,122.4490 x 0.05 = 306.1225đ. muỗng: 6,122.4490 x 5 = 30,612.2449đ. ml: 6,122.4490 x 1 = 6,122.4490đ.CriticalFR-UU-002, BR-07, FORMULA-001, FORMULA-002

Nhóm 4: Stock Import — Nhập kho (TC-SI-001 ~ TC-SI-003)

TC-IDNhómMô tảPreconditionStepsExpectedPriorityFR-ref
TC-SI-001StockImportNhập kho tay — 1 sản phẩm (tạo lô)DS-001 + DS-002 (kho + config Serum). Tồn kho Serum = 0.1. Vào kho vật tư Quận 1 2. Click [Nhập kho] 3. Tìm chọn "Serum Laser X" 4. Nhập SL = 5000, Giá nhập = 500,000đ, HSD = 30/06/2026, Mã lô = A01, Ghi chú = "Kiểm kê đầu kỳ" 5. Click [Xác nhận nhập]material_batch record tạo: purchase_price=500000, purchase_quantity=5000, unit_price=100, initial_qty=5000, remaining_qty=5000, expiry_date=2026-06-30, batch_code='A01', status='active'. material_stock_movement record tạo: quantity_change=+5000, source_type='stock_in', batch_id=lô vừa tạo. Tồn kho Serum hiện 5,000ml trên SCR-01.CriticalFR-007, FR-018
TC-SI-002StockImportNhập kho tay — nhiều sản phẩm (mỗi dòng = 1 lô riêng)DS-001 + DS-002 + DS-003. Tồn kho = 0.1. Click [Nhập kho] 2. Thêm 3 dòng: Serum 5000ml giá 500k HSD 30/06, Gel 800g giá 200k HSD 31/12, Mask 49 miếng giá 1.5M (không HSD) 3. [Xác nhận nhập]3 material_batch records tạo (mỗi dòng = 1 lô). 3 material_stock_movement records (source_type='stock_in', mỗi record gắn batch_id riêng). Tồn kho: Serum 5,000, Gel 800, Mask 49.CriticalFR-007, FR-018, DEC-D25
TC-SI-003StockImportImport Excel nhập kho (tạo lô per dòng)DS-001 + DS-002. File Excel template: [Mã SP | SL nhập | Giá nhập | HSD | Mã lô | Ghi chú].1. Click [Nhập kho] > [Import Excel] 2. Tải template mẫu — verify có cột Giá nhập, HSD, Mã lô 3. Điền 4 dòng: Serum 5000 giá 500k HSD 30/06 lô A01, Gel 800 giá 200k (không HSD), Mask 49 giá 1.5M HSD 31/12 lô M01, Bông 200 giá 50k (không HSD không mã lô) 4. Upload file 5. Verify preview: 4 dòng xanh 6. [Xác nhận]4 material_batch + 4 material_stock_movement (source_type='stock_in'). Mỗi batch có purchase_price, expiry_date (NULL nếu trống), batch_code (NULL nếu trống). SKU không khớp: dòng đó hiện lỗi đỏ, cho skip.HighFR-008, FR-018, DEC-D13

Nhóm 5: Subtask Material — Thêm, ĐVT, Giá, Hao hụt (TC-SM-001 ~ TC-SM-005)

TC-IDNhómMô tảPreconditionStepsExpectedPriorityFR-ref
TC-SM-001SubtaskMatThêm vật tư vào subtask + auto-fill giáDS-002 + DS-004 (Serum có config + usage units). Subtask "BS - Laser" ở trạng thái mới.1. Mở subtask "BS - Laser" 2. Ở section VẬT TƯ, tìm kiếm "Serum" 3. Chọn "Serum Laser X" 4. Verify auto-fill: ĐVT mặc định "giọt", Đơn giá 204.08đ 5. Nhập SL = 3 6. Verify thành tiền = 612đ 7. Bấm [Lưu]Record project_task_material tạo: usage_unit = 'giọt', unit_price = 204.0816, amount = 612, to_stock_factor = 0.05, stock_equivalent = 0.15. config_version_id = mpc-serum-001. price_snapshot_at ghi thời điểm thêm.CriticalFR-SM-001, FR-SM-002, FORMULA-003
TC-SM-002SubtaskMatThêm vật tư chưa có config — hiện "(chưa có giá)"Product "Tinh dầu ABC" chưa có material_price_config.1. Mở subtask, tìm kiếm "Tinh dầu ABC" 2. Chọn sản phẩm 3. Verify hiển thịThêm được vào bảng. Cột ĐVT: dropdown rỗng, hiện "(chưa cấu hình)". Cột Đơn giá: hiện "(chưa có giá)". Cột Thành tiền: hiện "—". amount = NULL, stock_equivalent = NULL.HighFR-SM-001, BR-06, DEC-D11
TC-SM-003SubtaskMatĐổi ĐVT → giá + stock_equivalent thay đổiDS-006 (subtask 1 có Serum 3 giọt). Usage units: giọt, muỗng, ml.1. Mở sửa subtask 1 2. Dòng Serum: đổi ĐVT từ "giọt" sang "muỗng" 3. Verify giá đổi: 204.08đ → 20,408.16đ 4. Verify thành tiền: 3 x 20,408.16 = 61,224đ 5. Bấm [Lưu]unit_price = 20,408.1633. amount = 61224 (ROUND). to_stock_factor = 5. stock_equivalent = 3 x 5 = 15ml. total_stock_equivalent = 15ml (no wastage).CriticalFR-SM-002, FORMULA-002, FORMULA-003, FORMULA-004
TC-SM-004SubtaskMatGhi nhận hao hụt kỹ thuậtDS-006. Subtask chưa done.1. Mở sửa subtask 1 2. Dòng Serum: SL = 3 giọt 3. Mở section hao hụt, nhập wastage_quantity = 1 giọt, lý do = "Đổ bỏ do rơi" 4. Bấm [Lưu]wastage_quantity = 1. total_stock_equivalent = (3 + 1) x 0.05 = 0.20ml. stock_equivalent (sử dụng) = 0.15ml. Khi done: trừ kho 0.20ml (bao gồm hao hụt).HighFR-SM-003, FORMULA-004, FORMULA-006
TC-SM-005SubtaskMatAggregate task cha — tổng chi phí + SLDS-006 (2 subtask, mỗi subtask có materials)1. Mở task cha "Laser Q-switch" 2. Xem section "VẬT TƯ DỰ KIẾN" 3. Verify bảng aggregateSerum: 6 giọt (3+3), tổng tiền = 1,224đ (612+612). Gel: 1 tube, tổng tiền = 30,000đ. Mask: 1 miếng, tổng tiền = 30,000đ. Tổng chi phí: 61,224đ. Lưu ý: SUM(amount), KHÔNG phải SUM(qty) x giá.CriticalFR-SM-003, FORMULA-005

Nhóm 6: Auto-deduct + Reverse (TC-AD-001 ~ TC-AD-005)

TC-IDNhómMô tảPreconditionStepsExpectedPriorityFR-ref
TC-AD-001AutoDeductSubtask done → auto-deduct khoDS-005 (tồn kho: Serum 5000ml, Gel 800g) + DS-006 (subtask 1 có 3 giọt Serum + 1 tube Gel)1. Chuyển subtask 1 status → "done" 2. Đợi event trigger chạy (< 5s) 3. Verify material_stock_movement 4. Verify tồn kho trên SCR-012 records movement tạo: Serum -0.15ml (running_balance = 4999.85), Gel -15g (running_balance = 785). source_type = 'auto_deduct'. source_reference_id = ptm IDs.CriticalFR-AD-001, DEC-D14, BR-09
TC-AD-002AutoDeductAuto-deduct với hao hụt kỹ thuậtSubtask có Serum 3 giọt + wastage 1 giọt (total_stock_equivalent = 0.20ml). Tồn kho Serum = 5000ml.1. Chuyển subtask done 2. Verify movementquantity_change = -0.20ml (bao gồm hao hụt). running_balance = 4999.80ml. Trừ total_stock_equivalent, KHÔNG phải stock_equivalent.CriticalFR-AD-001, FORMULA-004
TC-AD-003AutoDeductUndo subtask done → reverse stockDS-008 (subtask 1 done, đã có 2 auto_deduct movements)1. Chuyển subtask 1 từ "done" → "in_progress" 2. Đợi event trigger chạy (< 5s) 3. Verify material_stock_movement2 records movement mới tạo: Serum +0.15ml, Gel +15g. source_type = 'auto_reverse'. running_balance cộng lại: Serum = 5000ml, Gel = 800g. Movement cũ (auto_deduct) KHÔNG bị xóa/sửa (immutable, BR-13).CriticalFR-AD-002, DEC-D18, EC-01
TC-AD-004AutoDeductSubtask canceled (chưa done) — KHÔNG trừ khoDS-006 (subtask 2 ở trạng thái new/in_progress)1. Chuyển subtask 2 → "canceled" 2. Verify material_stock_movementKHÔNG có movement mới tạo. Tồn kho KHÔNG thay đổi. Subtask canceled chưa bao giờ done = chưa bao giờ trừ kho.HighEC-02, BR-09
TC-AD-005AutoDeductSubtask canceled (sau khi đã done) → reverseDS-008 (subtask 1 done + đã trừ kho)1. Chuyển subtask 1 → "canceled" 2. Verify movement2 records 'auto_reverse' tạo (giống TC-AD-003). running_balance cộng lại. EC-03: canceled sau done = phải reverse.CriticalFR-AD-002, EC-03
TC-EXP-003ExpandableSCR-04 done — [chi tiết] hiện lô đã xuất (đơn lô)Subtask done, vật tư dùng 3 giọt Serum, chỉ xuất từ 1 lô1. Mở subtask done 2. Click [chi tiết] expand dòng Serum1 dòng chi tiết: Lô A, 3 giọt (0.15ml), giá 5,000đ/ml, lô còn 497.85mlHighFR-010, DEC-D28
TC-EXP-004ExpandableSCR-04 done — [chi tiết] hiện FIFO split (2 lô)Subtask done, 40 giọt Serum, Lô A chỉ còn 1ml (20 giọt), Lô B lấy 1ml (20 giọt), giá khác nhau1. Mở subtask done 2. Click [chi tiết] expand2 dòng: Lô A (20 giọt × 250đ = 5,000đ, depleted), Lô B (20 giọt × 300đ = 6,000đ, còn 299ml). Đơn giá bình quân = 275đ/giọtHighFR-010, DEC-D27, DEC-D28

Nhóm 7: Kiểm kê + Điều chỉnh (TC-SK-001 ~ TC-SK-003)

TC-IDNhómMô tảPreconditionStepsExpectedPriorityFR-ref
TC-SK-001StocktakeKiểm kê — thực tế = hệ thốngDS-005 (tồn kho Serum 5000ml)1. Vào [Kiểm kê] 2. Bảng hiện tất cả vật tư + tồn hệ thống 3. Nhập Thực tế Serum = 5000 4. Verify cột Chênh = 0 5. [Xác nhận]Không tạo movement adjustment (chênh = 0). Tồn kho giữ nguyên.HighFR-SK-001
TC-SK-002StocktakeKiểm kê — thực tế < hệ thốngDS-005 (Serum tồn 5000ml, Mask tồn 49)1. [Kiểm kê] 2. Nhập: Serum thực tế = 4850, Mask thực tế = 49 3. Cột Chênh: Serum = -150, Mask = 0 4. Nhập Lý do Serum = "Hao hụt bay hơi" 5. [Xác nhận]1 movement: Serum -150ml, source_type = 'adjustment', note = "Hao hụt bay hơi". running_balance = 4850. Mask: không tạo movement (chênh = 0).HighFR-SK-001, EC-13
TC-SK-003StocktakeKiểm kê — thực tế > hệ thốngTồn kho Gel = 785g (sau auto-deduct)1. [Kiểm kê] 2. Nhập Gel thực tế = 800 3. Chênh = +15 4. Lý do = "Nhập bổ sung" 5. [Xác nhận]Movement: Gel +15g, source_type = 'adjustment'. running_balance = 800.HighFR-SK-001, EC-12

Nhóm 8: Cảnh báo tồn kho (TC-AL-001 ~ TC-AL-002)

TC-IDNhómMô tảPreconditionStepsExpectedPriorityFR-ref
TC-AL-001AlertCảnh báo khi tồn <= min_stockDS-005 (Mask tồn 49, min_stock = 50). DS-002.1. Verify SCR-01: Mask hiện badge đỏ 2. Verify notification in-app: "⚠️ [Quận 1] Mask collagen còn 49 miếng, dưới ngưỡng 50" 3. Người nhận: Manager Quận 1 + AdminBadge đỏ trên danh sách. Notification gửi đúng template. Chỉ gửi 1 lần/ngày (dedupe).HighFR-AL-001, BR-11
TC-AL-002AlertDedupe — không gửi trùng trong ngàyTC-AL-001 đã trigger notification hôm nay.1. Thực hiện thêm 1 action trừ kho Mask (VD: subtask done) 2. Tồn giảm từ 49 → 48 (vẫn <= 50) 3. Verify notificationKHÔNG gửi notification mới. Check notification table: chỉ 1 record trong ngày cho Mask ở kho Quận 1.HighFR-AL-001, EC-15

Nhóm 9: Kết nối tài chính đơn hàng (TC-FI-001 ~ TC-FI-002)

TC-IDNhómMô tảPreconditionStepsExpectedPriorityFR-ref
TC-FI-001FinanceĐơn mới — chi phí vật tư hiển thị trong sidebarDS-006 (2 subtask có materials). Order liên kết task cha.1. Mở đơn hàng liên kết task cha 2. Xem sidebar Tổng hợp tài chính 3. Verify dòng "Chi phí vật tư"Chi phí vật tư = SUM(amount) = 612 + 30,000 + 612 + 30,000 = 61,224đ (FORMULA-005). Hiển thị trong sidebar tài chính.HighFR-FI-001, FORMULA-005
TC-FI-002FinanceĐơn cũ — backward compat dùng inventory_documentTask cha có order liên kết + inventory_document.capture data cũ (trước deploy). Không có project_task_material.1. Mở đơn hàng cũ 2. Xem sidebar tài chính 3. Verify dòng "Chi phí vật tư"Chi phí = SUM(capture.Price x capture.Quantity) từ inventory_document. Không hiện 0đ. Backward compat hoạt động.HighFR-FI-001, FORMULA-005 (backward)

Nhóm 10: Permission / RBAC (TC-PM-001 ~ TC-PM-003)

TC-IDNhómMô tảPreconditionStepsExpectedPriorityFR-ref
TC-PM-001PermissionStaff (KTV) — ẩn menu Kho vật tư, ẩn cột giáLogin Staff. Subtask của mình có materials.1. Verify sidebar: KHÔNG hiện menu "Kho vật tư" 2. Mở subtask của mình 3. Verify bảng vật tư: KHÔNG hiện cột Đơn giá, Thành tiền 4. Có thể thêm/sửa vật tư (SL, ĐVT)Menu Kho vật tư: ẩn hoàn toàn. Cột giá: ẩn. Thêm/sửa vật tư trên subtask mình: cho phép. Subtask người khác: KHÔNG truy cập.CriticalPermission, DEC-D10
TC-PM-002PermissionManager — branch scoped, readonly configLogin Manager Quận 1.1. Vào Kho vật tư: chỉ thấy kho Quận 1 2. Xem chi tiết cấu hình: readonly (không sửa giá, ĐVT) 3. Nhập kho: cho phép (branch mình) 4. Kiểm kê: cho phép (branch mình)Danh sách: chỉ kho branch mình. Config: readonly. Nhập kho/Kiểm kê: cho phép. Subtask: xem giá + thành tiền.HighPermission
TC-PM-003PermissionAdmin — full access + cross-branchLogin Admin.1. Vào Kho vật tư: thấy tất cả kho 2. Chọn dropdown chi nhánh khác: hiện kho chi nhánh đó 3. Thêm/sửa config, nhập kho, kiểm kê: OK 4. Sidebar tài chính: thấy chi phí vật tưFull access tất cả chức năng. Dropdown chi nhánh hiện tất cả. Sidebar tài chính: hiện chi phí.HighPermission

Nhóm 11: Edge Cases (TC-EC-001 ~ TC-EC-005)

TC-IDNhómMô tảPreconditionStepsExpectedPriorityFR-ref
TC-EC-001EdgeCaseKhông đủ tồn kho — block auto-deductTồn kho Mask = 2 miếng. Subtask dùng 3 miếng.1. Thêm vật tư Mask = 3 miếng vào subtask 2. Chuyển subtask done 3. Verify kết quảBlock toàn bộ auto-deduct (không cho phép tồn kho âm). KHÔNG partial deduct. Error notification gửi cho KTV + Manager. Subtask KHÔNG chuyển done, yêu cầu nhập kho bổ sung hoặc giảm SL.HighEC-04, BR-08
TC-EC-002EdgeCaseRace condition — 2 KTV cùng trừ kho đồng thời2 subtask cùng dùng Serum, tồn 100ml (FIFO: Lô A 60ml, Lô B 40ml). Subtask A: 50ml, Subtask B: 60ml.1. Đồng thời chuyển cả 2 subtask → done 2. Verify movements + remaining_qtypg_advisory_xact_lock serialize per (warehouse, product). Subtask A: trừ Lô A 50ml (còn 10ml). Subtask B: trừ Lô A 10ml + Lô B 40ml = chỉ đủ 50ml, thiếu 10ml → block Subtask B (không cho phép tồn kho âm). KHÔNG có lost update / data corruption.CriticalEC-05
TC-EC-003EdgeCaseĐơn vị discrete — SL phải nguyênMask is_discrete = true.1. Mở subtask, thêm Mask 2. Nhập SL = 1.5 3. Bấm LưuFE validation error: "SL phải là số nguyên cho đơn vị rời". KHÔNG cho save. Nhập SL = 2 → OK.HighEC-07, BR-04, DEC-D07
TC-EC-004EdgeCaseProduct disabled — badge + block thêm mớiDS-007 (Serum disabled). Subtask đã có Serum (data cũ).1. Mở subtask mới → tìm kiếm "Serum" 2. Verify kết quả tìm kiếm 3. Mở subtask cũ có Serum 4. Verify hiển thịTìm kiếm: Serum KHÔNG xuất hiện (filter disabled). Subtask cũ: hiện Serum + badge "Ngưng KD". Tồn kho: giữ nguyên (không tự trừ).HighEC-11
TC-EC-005EdgeCaseStock_unit lock khi có referenceDS-004 (Serum có 3 usage_units + materials reference)1. Mở config Serum 2. Thử đổi ĐV kho từ "ml" → "g" 3. VerifyField ĐV kho: disabled/readonly. Tooltip: "Không thể đổi đơn vị kho khi đã có đơn vị sử dụng hoặc dữ liệu vật tư". Muốn đổi → tạo config mới (version mới).HighEC-10, BR-02, DEC-D06
TC-EC-006EdgeCaseProduct disabled + subtask done → auto-deduct VẪN hoạt độngDS-007 (Serum disabled) + subtask có Serum (data cũ, chưa done).1. Verify product Serum disabled = true 2. Chuyển subtask → done 3. Verify material_stock_movementAuto-deduct VẪN chạy (event trigger không check product.disabled). Movement tạo bình thường. Tồn kho trừ đúng. Lý do: disabled chỉ block thêm MỚI, không ảnh hưởng flow đã có.HighEC-16, DEC-D11
TC-EC-007EdgeCaseIdempotent check — retry event trigger không trừ kho 2 lầnDS-005 + DS-006 (subtask 1 có materials).1. Chuyển subtask 1 → done → auto-deduct chạy lần 1 2. Simulate retry event trigger (manual invoke handler với cùng payload) 3. Verify movementsLần 1: 2 movements tạo (Serum -0.15, Gel -15). Lần 2 (retry): handler check existing movements bằng source_reference_id + source_type='auto_deduct' → skip (đã xử lý). Tổng movements VẪN = 2 (không trùng). running_balance không bị trừ lần 2.CriticalDEC-D15, BR-13

Nhóm 12: Formula Verification (TC-FML-001 ~ TC-FML-005)

TC-IDNhómMô tảPreconditionStepsExpectedPriorityFR-ref
TC-FML-001FormulaFORMULA-001 — Giá đơn vị kho + hao hụt1. Tạo config: source_price = 2,000,000, source_quantity = 500, wastage_rate = 0.02 2. Verify stock_unit_pricestock_unit_price = 2,000,000 / (500 x 0.98) = 4,081.6327CriticalFORMULA-001
TC-FML-002FormulaFORMULA-002 — Giá đơn vị sử dụngTC-FML-001 (stock_unit_price = 4,081.6327)1. Thêm usage unit "giọt", factor = 0.05 2. Verify giáusage_unit_price = 4,081.6327 x 0.05 = 204.0816CriticalFORMULA-002
TC-FML-003FormulaFORMULA-003 — Chi phí per subtaskTC-FML-002 (giọt = 204.0816đ)1. Thêm Serum 3 giọt vào subtask 2. Verify amountamount = ROUND(204.0816 x 3) = ROUND(612.2449) = 612CriticalFORMULA-003
TC-FML-004FormulaFORMULA-004 — Quy đổi stock + hao hụtTC-FML-003 + wastage 1 giọt1. Thêm hao hụt 1 giọt 2. Verify stock equivalentsstock_equivalent = 3 x 0.05 = 0.15ml. total_stock_equivalent = (3+1) x 0.05 = 0.20mlCriticalFORMULA-004
TC-FML-005FormulaFORMULA-001 edge — wastage_rate = 0, source_price = 01. Config A: price = 1,000,000, qty = 100, wastage = 0 2. Config B: price = 0, qty = 500, wastage = 0.05A: 1,000,000 / (100 x 1) = 10,000.0000. B: 0 / (500 x 0.95) = 0.0000 (vật tư miễn phí).HighFORMULA-001 edge
TC-FML-006FormulaFORMULA-006 — Chi phí hao hụt kỹ thuậtTC-FML-002 (giọt = 204.0816đ). Subtask có Serum 3 giọt + wastage 1 giọt.1. Verify wastage_cost = ROUND(usage_unit_price × wastage_quantity) 2. wastage_cost = ROUND(204.0816 × 1) = 204đ 3. Verify total cost = amount + wastage_cost = 612 + 204 = 816đwastage_cost = 204đ. Total cost on aggregate = 816đ (bao gồm cả hao hụt). total_stock_equivalent = (3+1) × 0.05 = 0.20ml.CriticalFORMULA-006

Nhóm 13: Batch — Nhập kho tạo lô (TC-BATCH-001 ~ TC-BATCH-002)

TC-IDNhómMô tảPreconditionStepsExpectedPriorityFR-ref
TC-BATCH-001BatchNhập kho tạo lô mớiDS-001 + DS-002 (kho + config Serum).1. Vào kho vật tư Quận 1 2. Click [Nhập kho] 3. Chọn "Serum Laser X" 4. Nhập: SL = 500ml, Giá nhập = 500,000đ, HSD = 2026-06-30, Mã lô = A01 5. [Xác nhận nhập]material_batch tạo: status=active, remaining_qty=500, purchase_price=500,000đ, expiry_date=2026-06-30, batch_code=A01. material_stock_movement tạo: source_type=stock_in, batch_id=lô mới. Tồn kho tổng cập nhật đúng.CriticalFR-BATCH-001
TC-BATCH-002BatchNhập cùng giá cùng ngày → 2 lô riêng biệtDS-BATCH-001 (đã có Lô A).1. Nhập kho Serum lần 2: SL = 200ml, Giá = 5,000đ/ml (cùng giá Lô A), HSD = 2026-06-30 (cùng HSD) 2. [Xác nhận]2 records material_batch riêng biệt (IDs khác nhau). Lô A: remaining_qty vẫn = 500. Lô mới: remaining_qty = 200. KHÔNG merge vào lô cũ.CriticalFR-BATCH-001

Nhóm 14: FIFO Auto-deduct theo lô (TC-FIFO-001 ~ TC-FIFO-003)

TC-IDNhómMô tảPreconditionStepsExpectedPriorityFR-ref
TC-FIFO-001FIFOAuto-deduct FIFO — 1 lô đủDS-BATCH-001 (Lô A 500ml @5,000đ/ml, Lô B 300ml @6,000đ/ml). Subtask dùng 2ml Serum.1. Chuyển subtask → done 2. Đợi event trigger (< 5s) 3. Verify movements + batch remainingTrừ Lô A: 2ml (FIFO — lô cũ nhất trước). Lô A remaining_qty = 498ml. material_stock_movement: batch_id = Lô A, quantity_change = -2, unit_price = 5,000đ/ml. Lô B không bị ảnh hưởng.CriticalFR-012, FR-020
TC-FIFO-002FIFOAuto-deduct FIFO split — vượt 2 lôDS-BATCH-001, nhưng sửa Lô A remaining_qty = 1ml. Subtask dùng 2ml Serum.1. Chuyển subtask → done 2. Verify movements2 movements tạo: (1) Lô A -1ml @5,000đ → Lô A status=depleted, remaining_qty=0. (2) Lô B -1ml @6,000đ → Lô B remaining_qty=299. Tổng amount = 5,000 + 6,000 = 11,000đ. Snapshot unit_price = 11,000 / 2 = 5,500đ/ml.CriticalFR-012, FR-020, DEC-D27
TC-FIFO-003FIFOKhông đủ tồn kho → block toàn bộDS-BATCH-001, Lô A remaining = 1ml, Lô B remaining = 0.5ml (total = 1.5ml). Subtask dùng 2ml.1. Chuyển subtask → done 2. Verify kết quảBlock toàn bộ, KHÔNG partial deduct. Lô A vẫn 1ml, Lô B vẫn 0.5ml. Error notification gửi cho KTV + Manager: "Không đủ tồn kho Serum (cần 2ml, còn 1.5ml)". Subtask KHÔNG chuyển done.CriticalFR-012, FR-020, DEC-D23

Nhóm 15: HSD — Hạn sử dụng (TC-HSD-001 ~ TC-HSD-003)

TC-IDNhómMô tảPreconditionStepsExpectedPriorityFR-ref
TC-HSD-001HSDAuto-lock lô hết hạn (cron)DS-BATCH-002 (Lô X: expiry_date = yesterday, status = active, remaining_qty = 50 miếng).1. Trigger cron material_batch_expiry_check 2. Verify batch status 3. Verify movements 4. Verify notificationLô X: status → locked. Disposal movement tạo: quantity_change = -50, source_type = disposal. Notification gửi Manager + Admin: "Lô X01 Mask Collagen đã hết hạn, đã tự động khóa (50 miếng)".CriticalFR-019, DEC-D24
TC-HSD-002HSDCảnh báo 90 ngày trước HSDLô A: expiry_date = today + 60 ngày (trong vùng cảnh báo 90 ngày).1. Trigger cron material_batch_expiry_check 2. Verify notificationWarning notification gửi Manager + Admin: "Lô A01 Serum Laser X sẽ hết hạn trong 60 ngày (HSD: [date])". Lô A status vẫn = active (chưa hết hạn, chỉ cảnh báo).HighFR-019, DEC-D24
TC-HSD-003HSDFIFO skip lô hết hạn (locked)Lô A: status = locked (hết hạn). Lô B: status = active, remaining_qty = 300ml. Subtask dùng 2ml.1. Chuyển subtask → done 2. Verify movementsTrừ Lô B (skip Lô A vì locked). Movement: batch_id = Lô B, quantity_change = -2. Lô A KHÔNG bị ảnh hưởng. FIFO chỉ xét lô active.CriticalFR-019, FR-020

Nhóm 16: Hủy lô + Reverse lô (TC-DISPOSE-001, TC-REVERSE-001 ~ TC-REVERSE-002)

TC-IDNhómMô tảPreconditionStepsExpectedPriorityFR-ref
TC-DISPOSE-001DisposeHủy lô thủ côngLô A: status = active, remaining_qty = 200ml. Login Manager.1. Vào chi tiết Lô A 2. Click [Hủy lô] 3. Nhập lý do: "Hàng lỗi nhà cung cấp" 4. [Xác nhận]Lô A: status → disposed. Movement tạo: source_type = disposal, quantity_change = -200, note = "Hủy lô: Hàng lỗi nhà cung cấp". Tồn kho tổng giảm 200ml.CriticalFR-021, DEC-D26
TC-REVERSE-001ReverseReverse trả lại lô gốcDS-BATCH-001. Đã auto-deduct từ Lô A: 2ml (Lô A remaining = 498ml).1. Undo subtask done (chuyển done → in_progress) 2. Verify movements + batchLô A: remaining_qty += 2ml → 500ml. Reverse movement tạo: batch_id = Lô A, quantity_change = +2, source_type = auto_reverse.CriticalFR-013
TC-REVERSE-002ReverseReverse lô đã disposed — tái kích hoạtĐã deduct từ Lô A 2ml → Lô A remaining = 498. Sau đó Lô A bị disposed (status = disposed, remaining = 0).1. Undo subtask done 2. Verify batch status + remainingLô A: remaining_qty += 2ml (từ 0 → 2ml). status → active (tái kích hoạt vì có remaining > 0). Reverse movement tạo bình thường.CriticalFR-AD-002

Nhóm 17: Two-Phase Pricing (TC-PRICE-001 ~ TC-PRICE-004)

TC-PRICE-001: Two-Phase Pricing — giá ước tính lúc save

  • Setup: Lô A (500ml, 5,000đ/ml), Lô B (300ml, 6,000đ/ml)
  • Action: KTV save subtask với 3 giọt Serum (= 0.15ml)
  • Expected: unit_price = 250đ/giọt (từ Lô A — FIFO cũ nhất). amount = 750đ. Badge "(ước tính)" hiện.

TC-PRICE-002: Two-Phase Pricing — giá finalize lúc done (1 lô)

  • Setup: Như TC-PRICE-001
  • Action: Subtask done
  • Expected: FIFO deduct Lô A 0.15ml. unit_price vẫn = 250đ (1 lô, không split). Badge "(ước tính)" biến mất. amount = 750đ (final).

TC-PRICE-003: Two-Phase Pricing — giá finalize lúc done (FIFO split)

  • Setup: Lô A (0.10ml, 5,000đ/ml), Lô B (300ml, 6,000đ/ml)
  • Action: Save subtask 3 giọt (0.15ml) → unit_price ước tính = 250đ (từ Lô A). Rồi done.
  • Expected: FIFO split: Lô A 0.10ml (cost=500đ) + Lô B 0.05ml (cost=210đ). amount = 710đ. unit_price = 710/0.15/20 ≈ 236.67đ/giọt (bình quân gia quyền). Giá thay đổi so với ước tính.

TC-PRICE-004: Two-Phase Pricing — lô thay đổi giữa save và done

  • Setup: Lô A (1ml, 5,000đ/ml), Lô B (300ml, 6,000đ/ml). KTV save subtask 0.15ml → ước tính 250đ/giọt.
  • Action: Trước khi done, Lô A bị depleted bởi subtask khác. Rồi subtask done.
  • Expected: FIFO deduct từ Lô B (6,000đ/ml). unit_price final = 300đ/giọt. amount = 900đ. Khác với ước tính ban đầu 750đ.

Nhóm 18: Import cấu hình Excel (TC-CFG-001 ~ TC-CFG-002)

TC-IDNhómMô tảPreconditionStepsExpectedPriorityFR-ref
TC-CFG-001ConfigImportImport danh mục vật tư + giá bằng ExcelDS-001 (kho Quận 1 đã tạo). File Excel template: [Mã SP | ĐV kho | Giá nhập | SL/ĐV mua | Hao hụt % | Ngưỡng cảnh báo]. 3 dòng: Serum ml 2M 500 2 50, Gel g 400k 200 0 30, Mask miếng 30k 1 0 20.1. Vào Kho vật tư > [Import cấu hình] 2. Tải template 3. Điền 3 dòng 4. Upload 5. Verify preview 6. [Xác nhận]3 material_price_config records tạo (status active). stock_unit_price tự tính theo FORMULA-001. Mỗi config có effective_from = now(), effective_to = NULL. Preview hiện 3 dòng xanh.HighFR-017, DEC-D13
TC-CFG-002ConfigImportImport config — SKU trùng (đã có config active)DS-002 (Serum đã có config). File Excel chứa dòng Serum (SKU trùng).1. Upload file có 1 dòng Serum trùng 2. Verify previewDòng Serum hiện đỏ: "Vật tư đã có cấu hình active. Bỏ qua hoặc cập nhật giá." Cho phép skip hoặc tạo version mới (close cũ + tạo mới).HighFR-017, BR-01

Nhóm 19: Chuyển kho chi nhánh → Kho vật tư — FR-022 (TC-TRF-001 ~ TC-TRF-017)

Happy Path

TC-IDNhómMô tảPreconditionStepsExpectedPriorityFR-ref
TC-TRF-001TransferManager chuyển kho happy path — chọn SP + lô nguồn + nhập SL → xác nhậnLogin Manager Quận 1. DS-TRF-001 (kho chi nhánh Q1 có SP001 Serum Lô A 8 chai @500,000đ HSD 06/2027). Serum đã config quy đổi: 1 chai = 500ml.1. Vào Kho vật tư > Click [★ Chuyển kho] 2. Chọn SP "Serum Laser X" 3. Chọn lô nguồn "BRA01" (tồn 8 chai, giá 500,000đ, HSD 06/2027) 4. Nhập SL chuyển = 2 (chai) 5. Verify preview: 2 chai × 500ml = 1,000ml 6. Click [Xác nhận chuyển kho]Kho chi nhánh: product_supplying BRA01 remaining_qty giảm 8 → 6 chai. Kho vật tư: material_batch mới tạo: purchase_quantity=1000ml, purchase_price=500,000đ (giá lô nguồn per chai), unit_price=500,000/(1000×0.98)=510.20đ/ml (DEC-D37b: có tính wastage 2%), expiry_date=2027-06-30, status=active. material_stock_movement tạo: quantity_change=+1000, source_type=transfer. inventory_document tạo: behavior=export_material_warehouse + product_supplying records. Manager tự thực hiện, không cần duyệt (DEC-D31).CriticalFR-022, DEC-D29, DEC-D31, DEC-D32, DEC-D33, DEC-D36
TC-TRF-002TransferChuyển nhiều SP cùng 1 phiếu — 3 SP khác nhauLogin Manager Quận 1. DS-TRF-001 (3 SP: Serum Lô A 8 chai, Gel Lô A 10 chai, Mask Lô A 4 hộp). Cả 3 SP đã config quy đổi.1. Click [★ Chuyển kho] 2. Thêm Serum — chọn Lô BRA01, SL = 2 chai 3. Thêm Gel — chọn Lô BRA02, SL = 3 chai 4. Thêm Mask — chọn Lô BRA03, SL = 1 hộp 5. Click [Xác nhận chuyển kho]3 material_batch records tạo trong kho vật tư (mỗi SP 1 lô mới). 3 material_stock_movement records (source_type=transfer). Kho chi nhánh: Serum BRA01 8→6, Gel BRA02 10→7, Mask BRA03 4→3. 1 inventory_document chung cho cả phiếu.CriticalFR-022, DEC-D33
TC-TRF-003TransferQuy đổi đơn vị đúng — 2 chai × 500ml = 1,000mlLogin Manager Q1. DS-TRF-001. Serum config: purchase_to_stock_factor = 500 (1 chai = 500ml).1. Click [★ Chuyển kho] 2. Chọn Serum, Lô BRA01 3. Nhập SL = 2 (chai — đơn vị mua) 4. Verify hiển thị quy đổi trên form 5. [Xác nhận]Form hiện: "2 chai = 1,000ml". material_batch tạo: purchase_quantity = 1,000 (ml — stock unit). Nhập liệu bằng đơn vị mua (chai), hệ thống auto-convert sang stock unit (ml). DEC-D32.CriticalFR-022, DEC-D32, DEC-D34
TC-TRF-004TransferGiá + HSD kế thừa từ lô nguồnLogin Manager Q1. DS-TRF-001 (Serum Lô A: giá 500,000đ, HSD 06/2027. Lô B: giá 550,000đ, HSD 12/2027).1. Click [★ Chuyển kho] 2. Chọn Serum, chọn Lô BRA01 (giá 500,000đ, HSD 06/2027) 3. Nhập SL = 1 4. Verify preview: Giá = 500,000đ, HSD = 30/06/2027 5. Đổi sang Lô BRB01 (giá 550,000đ, HSD 12/2027) 6. Verify preview cập nhật: Giá = 550,000đ, HSD = 31/12/2027 7. [Xác nhận]material_batch tạo: purchase_price = product_supplying.price (550,000đ từ Lô B), expiry_date = lô nguồn expiry_date (2027-12-31). Giá và HSD auto-fill từ lô nguồn, KHÔNG cho sửa thủ công.CriticalFR-022, DEC-D36

Validation

TC-IDNhómMô tảPreconditionStepsExpectedPriorityFR-ref
TC-TRF-005TransferSL chuyển > tồn lô → blockLogin Manager Q1. DS-TRF-001 (Serum Lô A tồn 8 chai).1. Click [★ Chuyển kho] 2. Chọn Serum, Lô BRA01 (tồn 8 chai) 3. Nhập SL = 10 (> 8) 4. Click [Xác nhận]Block submit. Error message: "Tồn lô không đủ: cần 10, lô chỉ còn 8". SL input highlight đỏ. Không có thay đổi DB.CriticalFR-022, DEC-D36
TC-TRF-006TransferSP chưa config quy đổi → block + link SCR-02Login Manager Q1. Product "Tinh dầu ABC" có tồn kho chi nhánh nhưng chưa có material_price_config (chưa config quy đổi đơn vị).1. Click [★ Chuyển kho] 2. Chọn "Tinh dầu ABC" 3. Verify kết quảBlock: "Vui lòng cấu hình quy đổi đơn vị trước khi chuyển kho" + link dẫn tới SCR-02 (màn hình cấu hình). Không cho nhập SL, không cho xác nhận. DEC-D34: one-time unit conversion config bắt buộc.CriticalFR-022, DEC-D34
TC-TRF-007TransferChưa chọn lô nguồn → blockLogin Manager Q1. DS-TRF-001.1. Click [★ Chuyển kho] 2. Chọn Serum 3. KHÔNG chọn lô nguồn 4. Nhập SL = 2 5. Click [Xác nhận]Block submit. Error message: "Chọn mã lô hàng cho vật tư!". DEC-D36: manual lot selection bắt buộc.HighFR-022, DEC-D36
TC-TRF-008TransferGiá nhập = 0/NULL → warning vàng nhưng cho phép tiếp tụcLogin Manager Q1. product_supplying có lô với price = 0 (hàng tặng).1. Click [★ Chuyển kho] 2. Chọn SP có lô giá = 0 3. Chọn lô, nhập SL = 1 4. Verify warning 5. Click [Xác nhận]Warning banner vàng: "Giá nhập = 0" nhưng nút [Xác nhận] vẫn enabled. Cho phép chuyển kho. material_batch tạo với purchase_price = 0.HighFR-022

Permission

TC-IDNhómMô tảPreconditionStepsExpectedPriorityFR-ref
TC-TRF-009TransferStaff (KTV) → ẩn nút [★ Chuyển kho]Login Staff (KTV). Vào menu Kho vật tư (nếu có quyền xem).1. Vào Kho vật tư 2. Verify toolbar/action buttonsNút [★ Chuyển kho] KHÔNG hiển thị. Không có route truy cập SCR-08 (transfer screen). Nếu truy cập trực tiếp URL → redirect hoặc 403.HighFR-022, DEC-D31, Permission
TC-TRF-010TransferManager branch A → chỉ thấy SP kho chi nhánh ALogin Manager Quận 1. DS-TRF-001 (SP kho chi nhánh Q1). Branch Q2 cũng có SP riêng.1. Click [★ Chuyển kho] 2. Verify dropdown SP 3. Verify dropdown lôDropdown SP chỉ hiện SP có tồn trong kho chi nhánh Quận 1. KHÔNG hiện SP từ kho chi nhánh Q2. Lô nguồn filter theo branch_id = Quận 1. Branch scoping qua X-Hasura-Branch-Id.HighFR-022, Permission
TC-TRF-011TransferAdmin → thấy tất cả chi nhánhLogin Admin. Nhiều chi nhánh có kho.1. Click [★ Chuyển kho] 2. Verify dropdown chi nhánh 3. Chọn chi nhánh Q2 4. Verify SP hiệnAdmin thấy dropdown chọn chi nhánh. Chọn chi nhánh nào → hiện SP kho chi nhánh đó. Có thể thực hiện chuyển kho cho bất kỳ chi nhánh.HighFR-022, Permission

Concurrency & Edge Cases

TC-IDNhómMô tảPreconditionStepsExpectedPriorityFR-ref
TC-TRF-012TransferRace condition — 2 Manager chuyển cùng SP cùng lô cùng lúcLogin 2 Manager Q1 session song song. DS-TRF-001 (Serum Lô A tồn 8 chai). Cả 2 nhập SL = 5.1. Session 1: Chọn Serum, Lô BRA01, SL = 5, click [Xác nhận] 2. Session 2: Đồng thời chọn cùng Lô BRA01, SL = 5, click [Xác nhận]pg_advisory_xact_lock serialize: first-come-first-served. Session 1: thành công, Lô A 8→3 chai. Session 2: reload tồn lô, thấy còn 3 chai, block "Tồn lô không đủ: cần 5, lô chỉ còn 3". Không có lost update.CriticalFR-022, DEC-D36
TC-TRF-013TransferSP không có tồn trong kho chi nhánh → ẩn khỏi dropdownLogin Manager Q1. "Tinh chất ABC" tồn kho chi nhánh = 0 (tất cả lô remaining_qty = 0).1. Click [★ Chuyển kho] 2. Tìm kiếm "Tinh chất ABC" trong dropdown SPSP không xuất hiện trong dropdown (filter: chỉ hiện SP có remaining_qty > 0 ở ít nhất 1 lô).HighFR-022
TC-TRF-014TransferHủy phiếu chuyển khi batch chưa dùng → reverse cả 2 bênTC-TRF-001 đã thực hiện (Serum chuyển 2 chai = 1,000ml). Batch mới tạo trong kho vật tư chưa bị auto-deduct.1. Vào lịch sử phiếu chuyển kho 2. Tìm phiếu vừa tạo 3. Click [Hủy phiếu] 4. Nhập lý do: "Chuyển nhầm" 5. [Xác nhận hủy]Kho vật tư: material_batch.status = 'disposed', remaining_qty = 0. material_stock_movement tạo: source_type = 'disposal', quantity_change = -1,000ml. Tồn kho tổng giảm 1,000ml. Kho chi nhánh: product_supplying reversed (type='release'), remaining_qty +2 chai (6→8, khôi phục). inventory_document.status_id = 'inventory_canceled'. Reverse cả 2 bên.CriticalFR-022
TC-TRF-015TransferHủy phiếu chuyển khi batch đã auto-deduct → block hủyTC-TRF-001 (batch tạo từ chuyển kho). Sau đó subtask done → auto-deduct đã trừ từ batch này.1. Vào lịch sử phiếu chuyển kho 2. Tìm phiếu 3. Click [Hủy phiếu]Block hủy. Error message: "Không thể hủy: lô đã được sử dụng". Nút [Hủy phiếu] disabled hoặc ẩn khi batch đã có deduct movements.CriticalFR-022

Integration

TC-IDNhómMô tảPreconditionStepsExpectedPriorityFR-ref
TC-TRF-016TransferSau chuyển kho → subtask done → auto-deduct FIFO từ batch mớiTC-TRF-001 (batch Serum 1,000ml tạo từ chuyển kho, purchase_price = 500,000đ). Subtask có Serum 3 giọt = 0.15ml.1. Verify batch mới có status = active 2. Chuyển subtask → done 3. Verify FIFO deduct 4. Verify giáFIFO deduct từ batch mới tạo (nếu lô cũ nhất theo created_at). material_stock_movement: batch_id = batch mới, quantity_change = -0.15ml, unit_price = giá lô nguồn (500,000đ / 500ml = 1,000đ/ml). Giá kế thừa từ kho chi nhánh.CriticalFR-022, FR-012, FR-020
TC-TRF-017Transferinventory_document tạo đúng behavior + product_supplying recordsTC-TRF-001 (chuyển kho thành công).1. Query inventory_document WHERE liên kết phiếu chuyển kho 2. Verify behavior 3. Verify product_supplying records gắn inventory_documentinventory_document tạo: behavior = export_material_warehouse. Có product_supplying records tương ứng (snapshot thông tin lô nguồn). DEC-D33: inventory_document behaviors mới cho transfer flow.HighFR-022, DEC-D33

D5) Entry / Exit Criteria

Entry Criteria

  • [ ] Migration 6 bảng mới + 1 ALTER đã apply thành công trên test env:
    • material_warehouse
    • material_price_config (+ unique indexes)
    • material_usage_unit
    • material_stock_movement (+ indexes)
    • material_batch (+ indexes on warehouse_id, product_id, status, expiry_date)
    • material_stock_balance (view)
    • ALTER project_task_material (thêm 10 cột)
  • [ ] Hasura metadata reload thành công — schema + permissions cho 6 bảng
  • [ ] Event trigger subtask_status_change hoạt động (test 1 subtask done → có movement)
  • [ ] Cron material_batch_expiry_check registered + tested trên test env
  • [ ] pnpm codegen pass (TypeScript types generated cho bảng mới)
  • [ ] FE: SCR-01, SCR-02, SCR-03, SCR-04, SCR-06, SCR-07, SCR-08 render được (không crash)
  • [ ] Test accounts sẵn sàng:
    • Staff (KTV): có assign vào ít nhất 1 subtask
    • Manager Quận 1: có quyền quản lý project + kho
    • Admin (IT Leader): full access
  • [ ] Seed data DS-001 ~ DS-006, DS-BATCH-001, DS-BATCH-002, DS-TRF-001 đã tạo trên test env
  • [ ] Kho chi nhánh Q1 có product_supplying data (3 SP × 2 lô) cho test chuyển kho

Exit Criteria

  • [ ] Critical Path (24 TCs): TC-MW-001, TC-PC-001, TC-PC-003, TC-UU-001, TC-SI-001, TC-SM-001, TC-AD-001, TC-AD-003, TC-SM-005, TC-EC-002, TC-BATCH-001, TC-FIFO-001, TC-FIFO-002, TC-FIFO-003, TC-HSD-001, TC-REVERSE-001, TC-TRF-001, TC-TRF-002, TC-TRF-003, TC-TRF-005, TC-TRF-006, TC-TRF-012, TC-TRF-014, TC-TRF-015 — 24/24 pass
  • [ ] All High Priority (37 TCs): 37/37 pass
  • [ ] All Medium (10 TCs): 10/10 pass
  • [ ] Formula Tests: TC-FML-001 ~ TC-FML-006 — 6/6 pass
  • [ ] Permission Tests: TC-PM-001 ~ TC-PM-003 — 3/3 pass
  • [ ] Batch/FIFO Tests: TC-BATCH-001~002, TC-FIFO-001~003, TC-HSD-001~003, TC-DISPOSE-001, TC-REVERSE-001~002 — 11/11 pass
  • [ ] Transfer Tests (FR-022): TC-TRF-001~017 — 17/17 pass
  • [ ] Backward compat: Đơn cũ (inventory_document) hiển thị đúng (TC-FI-002)
  • [ ] Performance: Event trigger auto-deduct < 5s, Danh sách kho vật tư < 500ms
  • [ ] Tồn kho chính xác: Sau full test, SUM(quantity_change) = running_balance cuối cho mỗi (warehouse, product)
  • [ ] No P1/P2 bugs open
  • [ ] Tiền tour / Commission / Lương KHÔNG bị ảnh hưởng (verify trên 1 subtask có materials + tour_money)

Changelog

VersionNgàyThay đổiTác giả
1.02026-03-26Khởi tạo QA Test Plan: 12 nhóm, 46 TCs, 8 seed data scripts, entry/exit criteriaPO/BA + AI
1.12026-03-27D2: Thêm FR naming mapping table (FR-MW-xxx → FR-001~017 khớp prd.md). Sửa "9 cột" → "10 cột". +TC-FML-006 (FORMULA-006 wastage_cost). +TC-EC-006 (product disabled + auto-deduct). +TC-EC-007 (idempotent event trigger). +DS-009 (inventory_document backward compat). D1: update count 46→49 TCs, +seed data row.PO/BA + AI
2.02026-03-27Design v2 — Batch-based FIFO. +11 TCs mới: TC-BATCH-001~002 (nhập kho tạo lô), TC-FIFO-001~003 (FIFO auto-deduct), TC-HSD-001~003 (HSD + auto-lock cron), TC-DISPOSE-001 (hủy lô), TC-REVERSE-001~002 (reverse trả lô). +DS-BATCH-001 (2 lô Serum FIFO), +DS-BATCH-002 (lô hết hạn Mask). +FR-018~021 mapping. Sửa TC-EC-001: "tồn kho âm" → "block, không cho phép tồn kho âm". Sửa costing_method "latest_price" → "fifo". D1: 49→60 TCs, 9→11 seed data, 18→22 FRs. Entry: +material_batch table, +cron check. Exit: +batch/FIFO critical path.PO/BA + AI
2.12026-03-27Fix issues. D2: Rewrite FR naming mapping khớp 100% PRD FR-001~021. Coverage matrix dùng PRD FR numbers trực tiếp. Sửa FR-ref trên TC-FIFO/HSD/DISPOSE (FR-BATCH-002→FR-020, FR-HSD-001→FR-019, FR-DISPOSE→FR-021). TC-SI-001~003 rewrite: batch contract (Giá nhập, HSD, Mã lô, source_type='stock_in'). +Nhóm 17: TC-PRICE-001~004 (Two-Phase Pricing). +Nhóm 18: TC-CFG-001~002 (FR-017 import cấu hình Excel). D1: 60→66 TCs, 22→26 FRs.PO/BA + AI
3.02026-03-27Design v3 — Chuyển kho chi nhánh → Kho vật tư (FR-022). +Nhóm 19: TC-TRF-001~017 (17 TCs mới): happy path (4), validation (4), permission (3), concurrency & edge cases (4), integration (2). +DS-TRF-001 (kho chi nhánh Q1: 3 SP × 2 lô, khác giá khác HSD). FR-022 mapping: DEC-D29 (kết nối kho chi nhánh), DEC-D31 (Manager tự thực hiện), DEC-D32 (nhập ĐV mua auto-convert), DEC-D33 (inventory_document behaviors), DEC-D34 (one-time unit conversion config), DEC-D36 (manual lot selection). Entry: +SCR-08, +DS-TRF-001. Exit: +transfer critical path (8 TCs), +transfer test group (17/17). D1: 66→85 TCs (+17 transfer, +4 export, -2 CFG cleanup), 26→27 FRs, 11→12 seed data.PO/BA + AI