Appearance
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ại | Số lượng | Chi tiết |
|---|---|---|
| FR | 27 | FR-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 |
| BR | 13 | BR-01 ~ BR-13 |
| Edge Cases | 15 | EC-01 ~ EC-15 |
| Formulas | 6 | FORMULA-001 ~ FORMULA-006 |
| Test Cases | 85 | TC-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 Data | 12 | DS-001 ~ DS-009, DS-BATCH-001, DS-BATCH-002, DS-TRF-001 |
| Coverage | 100% | 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 ID | prd.md ID | Tên (khớp prd.md) |
|---|---|---|
| FR-MW-001 | FR-001 | Tạo kho vật tư (auto-create) |
| FR-MW-002 | FR-002 | Xem danh sách kho vật tư |
| FR-MW-003 | FR-003 | Quản lý vật tư — thêm/ẩn |
| FR-PC-001 | FR-004 | Cấu hình giá vật tư per product |
| FR-PC-002 | FR-005 | Cập nhật giá (versioning) |
| FR-UU-001 | FR-006 | Cấu hình đơn vị quy đổi |
| FR-SI-001 | FR-007 | Nhập tồn kho manual (tạo lô) |
| FR-SI-002 | FR-008 | Nhập tồn kho bằng Excel (tạo lô per dòng) |
| FR-SM-001 | FR-009 | Chọn ĐVT khi thêm vật tư subtask |
| FR-SM-002 | FR-010 | Two-Phase Pricing (ước tính + finalize) |
| FR-SM-003 | FR-011 | Ghi nhận hao hụt kỹ thuật |
| FR-AD-001 | FR-012 | Auto-deduct khi subtask done (FIFO) |
| FR-AD-002 | FR-013 | Auto-reverse khi undo done |
| FR-SK-001 | FR-014 | Kiểm kê + điều chỉnh |
| FR-AL-001 | FR-015 | Cảnh báo tồn kho thấp |
| FR-FI-001 | FR-016 | Chi phí vật tư sidebar tài chính |
| FR-FI-002 | FR-017 | Import cấu hình Excel |
| FR-BATCH-001 | FR-018 | Quản lý lô hàng (batch lifecycle) |
| FR-BATCH-002 | FR-018 | Nhập cùng giá cùng ngày tách lô |
| FR-HSD-001 | FR-019 | Auto-lock lô hết hạn |
| FR-HSD-002 | FR-019 | Cảnh báo 90 ngày HSD |
| FR-FIFO-001 | FR-020 | FIFO auto-deduct 1 lô |
| FR-FIFO-002 | FR-020 | FIFO split 2+ lô |
| FR-FIFO-003 | FR-020 | Block khi không đủ tồn |
| FR-DISPOSE-001 | FR-021 | Hủy lô (disposal) |
| FR-TRF-001 | FR-022 | Chuyển kho chi nhánh → Kho vật tư |
| PRD FR | QA TC-IDs | Priority |
|---|---|---|
| FR-001 Tạo kho vật tư | TC-MW-001, TC-MW-002 | Critical |
| FR-002 Xem danh sách | TC-MW-003 | High |
| FR-003 Quản lý vật tư — thêm/ẩn | TC-MW-004, TC-MW-005 | High |
| FR-004 Cấu hình giá | TC-PC-001, TC-PC-002, TC-PC-005 | Critical |
| FR-005 Cập nhật giá (versioning) | TC-PC-003, TC-PC-004 | Critical |
| FR-006 Cấu hình ĐVT quy đổi | TC-UU-001, TC-UU-002, TC-UU-003 | Critical |
| FR-007 Nhập kho manual (tạo lô) | TC-SI-001, TC-SI-002 | Critical |
| FR-008 Import Excel (tạo lô per dòng) | TC-SI-003 | High |
| FR-009 Chọn ĐVT subtask | TC-SM-001, TC-SM-003 | Critical |
| FR-010 Two-Phase Pricing | TC-SM-002, TC-PRICE-001, TC-PRICE-002, TC-PRICE-003, TC-PRICE-004 | Critical |
| FR-011 Hao hụt kỹ thuật | TC-SM-004 | High |
| FR-012 Auto-deduct (FIFO) | TC-AD-001, TC-AD-002, TC-FIFO-001, TC-FIFO-002, TC-FIFO-003 | Critical |
| FR-013 Auto-reverse | TC-AD-003, TC-AD-004, TC-AD-005, TC-REVERSE-001, TC-REVERSE-002 | Critical |
| FR-014 Kiểm kê + điều chỉnh | TC-SK-001, TC-SK-002, TC-SK-003 | High |
| FR-015 Cảnh báo tồn kho | TC-AL-001, TC-AL-002 | High |
| FR-016 Sidebar tài chính | TC-FI-001, TC-FI-002 | High |
| FR-017 Import cấu hình Excel | TC-CFG-001 | High |
| FR-018 Quản lý lô hàng | TC-BATCH-001, TC-BATCH-002 | Critical |
| FR-019 HSD — cảnh báo + auto-lock | TC-HSD-001, TC-HSD-002, TC-HSD-003 | Critical |
| FR-020 FIFO xuất kho | TC-FIFO-001, TC-FIFO-002, TC-FIFO-003 | Critical |
| FR-021 Hủy lô (disposal) | TC-DISPOSE-001 | Critical |
| FR-022 Chuyển kho chi nhánh → Kho vật tư | TC-TRF-001~017 | Critical |
| BR-01 ~ BR-19 | TC-EC-001~005 (covered within edge cases + formula tests) | High |
| EC-01 ~ EC-21 | TC-EC-001~005, TC-AD-003~005, TC-PM-001~003 | High |
| FORMULA-001 ~ 006 | TC-FML-001~006 | Critical |
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 snapshotDS-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_configDS-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-ID | Nhóm | Mô tả | Precondition | Steps | Expected | Priority | FR-ref |
|---|---|---|---|---|---|---|---|
| TC-MW-001 | Warehouse | Tạo kho vật tư cho chi nhánh | Login 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. | Critical | FR-MW-001 |
| TC-MW-002 | Warehouse | Tạo kho trùng chi nhánh — bị chặn | DS-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. | High | FR-MW-001, BR-01 |
| TC-MW-003 | Warehouse | Xem danh sách vật tư + tìm kiếm | DS-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òng | Bả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). | High | FR-MW-002 |
| TC-MW-004 | Warehouse | Vô hiệu kho vật tư | DS-001 (kho active) | 1. Vào chi tiết kho 2. Click [Vô hiệu] 3. Confirm dialog | is_active = false. Không cho nhập kho, kiểm kê nữa. Subtask vẫn hiện data cũ (readonly). | High | FR-MW-003 |
| TC-MW-005 | Warehouse | Kho vật tư hiển thị đúng branch scope cho Manager | DS-001. Login Manager chi nhánh Quận 1. | 1. Vào Kho > Kho vật tư 2. Verify dropdown chi nhánh | Manager chỉ thấy kho của branch mình. Admin thấy tất cả + dropdown chọn branch. | High | FR-MW-002, Permission |
| TC-EXP-001 | Expandable | SCR-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ô active | 1. Mở SCR-01 2. Click ▶ expand dòng SP | Hiệ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 × 1 | Medium | FR-002 |
| TC-EXP-002 | Expandable | SCR-01 expandable row hiện danh sách lô + FIFO indicator | SP 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)" | Medium | FR-002 |
Nhóm 2: Price Config + Versioning (TC-PC-001 ~ TC-PC-005)
| TC-ID | Nhóm | Mô tả | Precondition | Steps | Expected | Priority | FR-ref |
|---|---|---|---|---|---|---|---|
| TC-PC-001 | PriceConfig | Thêm cấu hình giá mới cho sản phẩm | DS-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. | Critical | FR-PC-001, FORMULA-001 |
| TC-PC-002 | PriceConfig | Thêm config trùng product active — bị chặn | DS-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. | High | FR-PC-001, BR-01 |
| TC-PC-003 | PriceConfig | Cậ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. | Critical | FR-PC-002, DEC-D02 |
| TC-PC-004 | PriceConfig | Subtask cũ giữ snapshot giá cũ sau cập nhật | DS-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á Serum | unit_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. | Critical | FR-PC-002, BR-03, DEC-D05 |
| TC-PC-005 | PriceConfig | Hao 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.5263 | 0%: 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. | High | FR-PC-003, FORMULA-001, DEC-D03 |
Nhóm 3: Usage Unit quy đổi (TC-UU-001 ~ TC-UU-003)
| TC-ID | Nhóm | Mô tả | Precondition | Steps | Expected | Priority | FR-ref |
|---|---|---|---|---|---|---|---|
| TC-UU-001 | UsageUnit | Thêm đơn vị sử dụng | DS-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đ". | Critical | FR-UU-001, FORMULA-002 |
| TC-UU-002 | UsageUnit | Thê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". | Critical | FR-UU-001, BR-04, DEC-D07 |
| TC-UU-003 | UsageUnit | Auto-calc giá ĐVT khi sửa source_price | DS-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 form | stock_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đ. | Critical | FR-UU-002, BR-07, FORMULA-001, FORMULA-002 |
Nhóm 4: Stock Import — Nhập kho (TC-SI-001 ~ TC-SI-003)
| TC-ID | Nhóm | Mô tả | Precondition | Steps | Expected | Priority | FR-ref |
|---|---|---|---|---|---|---|---|
| TC-SI-001 | StockImport | Nhậ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. | Critical | FR-007, FR-018 |
| TC-SI-002 | StockImport | Nhậ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. | Critical | FR-007, FR-018, DEC-D25 |
| TC-SI-003 | StockImport | Import 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. | High | FR-008, FR-018, DEC-D13 |
Nhóm 5: Subtask Material — Thêm, ĐVT, Giá, Hao hụt (TC-SM-001 ~ TC-SM-005)
| TC-ID | Nhóm | Mô tả | Precondition | Steps | Expected | Priority | FR-ref |
|---|---|---|---|---|---|---|---|
| TC-SM-001 | SubtaskMat | Thê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. | Critical | FR-SM-001, FR-SM-002, FORMULA-003 |
| TC-SM-002 | SubtaskMat | Thê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. | High | FR-SM-001, BR-06, DEC-D11 |
| TC-SM-003 | SubtaskMat | Đổi ĐVT → giá + stock_equivalent thay đổi | DS-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). | Critical | FR-SM-002, FORMULA-002, FORMULA-003, FORMULA-004 |
| TC-SM-004 | SubtaskMat | Ghi nhận hao hụt kỹ thuật | DS-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). | High | FR-SM-003, FORMULA-004, FORMULA-006 |
| TC-SM-005 | SubtaskMat | Aggregate task cha — tổng chi phí + SL | DS-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 aggregate | Serum: 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á. | Critical | FR-SM-003, FORMULA-005 |
Nhóm 6: Auto-deduct + Reverse (TC-AD-001 ~ TC-AD-005)
| TC-ID | Nhóm | Mô tả | Precondition | Steps | Expected | Priority | FR-ref |
|---|---|---|---|---|---|---|---|
| TC-AD-001 | AutoDeduct | Subtask done → auto-deduct kho | DS-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-01 | 2 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. | Critical | FR-AD-001, DEC-D14, BR-09 |
| TC-AD-002 | AutoDeduct | Auto-deduct với hao hụt kỹ thuật | Subtask 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 movement | quantity_change = -0.20ml (bao gồm hao hụt). running_balance = 4999.80ml. Trừ total_stock_equivalent, KHÔNG phải stock_equivalent. | Critical | FR-AD-001, FORMULA-004 |
| TC-AD-003 | AutoDeduct | Undo subtask done → reverse stock | DS-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_movement | 2 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). | Critical | FR-AD-002, DEC-D18, EC-01 |
| TC-AD-004 | AutoDeduct | Subtask canceled (chưa done) — KHÔNG trừ kho | DS-006 (subtask 2 ở trạng thái new/in_progress) | 1. Chuyển subtask 2 → "canceled" 2. Verify material_stock_movement | KHÔ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. | High | EC-02, BR-09 |
| TC-AD-005 | AutoDeduct | Subtask canceled (sau khi đã done) → reverse | DS-008 (subtask 1 done + đã trừ kho) | 1. Chuyển subtask 1 → "canceled" 2. Verify movement | 2 records 'auto_reverse' tạo (giống TC-AD-003). running_balance cộng lại. EC-03: canceled sau done = phải reverse. | Critical | FR-AD-002, EC-03 |
| TC-EXP-003 | Expandable | SCR-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 Serum | 1 dòng chi tiết: Lô A, 3 giọt (0.15ml), giá 5,000đ/ml, lô còn 497.85ml | High | FR-010, DEC-D28 |
| TC-EXP-004 | Expandable | SCR-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 nhau | 1. Mở subtask done 2. Click [chi tiết] expand | 2 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ọt | High | FR-010, DEC-D27, DEC-D28 |
Nhóm 7: Kiểm kê + Điều chỉnh (TC-SK-001 ~ TC-SK-003)
| TC-ID | Nhóm | Mô tả | Precondition | Steps | Expected | Priority | FR-ref |
|---|---|---|---|---|---|---|---|
| TC-SK-001 | Stocktake | Kiểm kê — thực tế = hệ thống | DS-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. | High | FR-SK-001 |
| TC-SK-002 | Stocktake | Kiểm kê — thực tế < hệ thống | DS-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). | High | FR-SK-001, EC-13 |
| TC-SK-003 | Stocktake | Kiểm kê — thực tế > hệ thống | Tồ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. | High | FR-SK-001, EC-12 |
Nhóm 8: Cảnh báo tồn kho (TC-AL-001 ~ TC-AL-002)
| TC-ID | Nhóm | Mô tả | Precondition | Steps | Expected | Priority | FR-ref |
|---|---|---|---|---|---|---|---|
| TC-AL-001 | Alert | Cảnh báo khi tồn <= min_stock | DS-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 + Admin | Badge đỏ trên danh sách. Notification gửi đúng template. Chỉ gửi 1 lần/ngày (dedupe). | High | FR-AL-001, BR-11 |
| TC-AL-002 | Alert | Dedupe — không gửi trùng trong ngày | TC-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 notification | KHÔNG gửi notification mới. Check notification table: chỉ 1 record trong ngày cho Mask ở kho Quận 1. | High | FR-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-ID | Nhóm | Mô tả | Precondition | Steps | Expected | Priority | FR-ref |
|---|---|---|---|---|---|---|---|
| TC-FI-001 | Finance | Đơn mới — chi phí vật tư hiển thị trong sidebar | DS-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. | High | FR-FI-001, FORMULA-005 |
| TC-FI-002 | Finance | Đơn cũ — backward compat dùng inventory_document | Task 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. | High | FR-FI-001, FORMULA-005 (backward) |
Nhóm 10: Permission / RBAC (TC-PM-001 ~ TC-PM-003)
| TC-ID | Nhóm | Mô tả | Precondition | Steps | Expected | Priority | FR-ref |
|---|---|---|---|---|---|---|---|
| TC-PM-001 | Permission | Staff (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. | Critical | Permission, DEC-D10 |
| TC-PM-002 | Permission | Manager — branch scoped, readonly config | Login 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. | High | Permission |
| TC-PM-003 | Permission | Admin — full access + cross-branch | Login 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í. | High | Permission |
Nhóm 11: Edge Cases (TC-EC-001 ~ TC-EC-005)
| TC-ID | Nhóm | Mô tả | Precondition | Steps | Expected | Priority | FR-ref |
|---|---|---|---|---|---|---|---|
| TC-EC-001 | EdgeCase | Không đủ tồn kho — block auto-deduct | Tồ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. | High | EC-04, BR-08 |
| TC-EC-002 | EdgeCase | Race condition — 2 KTV cùng trừ kho đồng thời | 2 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_qty | pg_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. | Critical | EC-05 |
| TC-EC-003 | EdgeCase | Đơn vị discrete — SL phải nguyên | Mask is_discrete = true. | 1. Mở subtask, thêm Mask 2. Nhập SL = 1.5 3. Bấm Lưu | FE validation error: "SL phải là số nguyên cho đơn vị rời". KHÔNG cho save. Nhập SL = 2 → OK. | High | EC-07, BR-04, DEC-D07 |
| TC-EC-004 | EdgeCase | Product disabled — badge + block thêm mới | DS-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ừ). | High | EC-11 |
| TC-EC-005 | EdgeCase | Stock_unit lock khi có reference | DS-004 (Serum có 3 usage_units + materials reference) | 1. Mở config Serum 2. Thử đổi ĐV kho từ "ml" → "g" 3. Verify | Field Đ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). | High | EC-10, BR-02, DEC-D06 |
| TC-EC-006 | EdgeCase | Product disabled + subtask done → auto-deduct VẪN hoạt động | DS-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_movement | Auto-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ó. | High | EC-16, DEC-D11 |
| TC-EC-007 | EdgeCase | Idempotent check — retry event trigger không trừ kho 2 lần | DS-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 movements | Lầ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. | Critical | DEC-D15, BR-13 |
Nhóm 12: Formula Verification (TC-FML-001 ~ TC-FML-005)
| TC-ID | Nhóm | Mô tả | Precondition | Steps | Expected | Priority | FR-ref |
|---|---|---|---|---|---|---|---|
| TC-FML-001 | Formula | FORMULA-001 — Giá đơn vị kho + hao hụt | — | 1. Tạo config: source_price = 2,000,000, source_quantity = 500, wastage_rate = 0.02 2. Verify stock_unit_price | stock_unit_price = 2,000,000 / (500 x 0.98) = 4,081.6327 | Critical | FORMULA-001 |
| TC-FML-002 | Formula | FORMULA-002 — Giá đơn vị sử dụng | TC-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.0816 | Critical | FORMULA-002 |
| TC-FML-003 | Formula | FORMULA-003 — Chi phí per subtask | TC-FML-002 (giọt = 204.0816đ) | 1. Thêm Serum 3 giọt vào subtask 2. Verify amount | amount = ROUND(204.0816 x 3) = ROUND(612.2449) = 612 | Critical | FORMULA-003 |
| TC-FML-004 | Formula | FORMULA-004 — Quy đổi stock + hao hụt | TC-FML-003 + wastage 1 giọt | 1. Thêm hao hụt 1 giọt 2. Verify stock equivalents | stock_equivalent = 3 x 0.05 = 0.15ml. total_stock_equivalent = (3+1) x 0.05 = 0.20ml | Critical | FORMULA-004 |
| TC-FML-005 | Formula | FORMULA-001 edge — wastage_rate = 0, source_price = 0 | — | 1. Config A: price = 1,000,000, qty = 100, wastage = 0 2. Config B: price = 0, qty = 500, wastage = 0.05 | A: 1,000,000 / (100 x 1) = 10,000.0000. B: 0 / (500 x 0.95) = 0.0000 (vật tư miễn phí). | High | FORMULA-001 edge |
| TC-FML-006 | Formula | FORMULA-006 — Chi phí hao hụt kỹ thuật | TC-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. | Critical | FORMULA-006 |
Nhóm 13: Batch — Nhập kho tạo lô (TC-BATCH-001 ~ TC-BATCH-002)
| TC-ID | Nhóm | Mô tả | Precondition | Steps | Expected | Priority | FR-ref |
|---|---|---|---|---|---|---|---|
| TC-BATCH-001 | Batch | Nhập kho tạo lô mới | DS-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. | Critical | FR-BATCH-001 |
| TC-BATCH-002 | Batch | Nhập cùng giá cùng ngày → 2 lô riêng biệt | DS-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ũ. | Critical | FR-BATCH-001 |
Nhóm 14: FIFO Auto-deduct theo lô (TC-FIFO-001 ~ TC-FIFO-003)
| TC-ID | Nhóm | Mô tả | Precondition | Steps | Expected | Priority | FR-ref |
|---|---|---|---|---|---|---|---|
| TC-FIFO-001 | FIFO | Auto-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 remaining | Trừ 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. | Critical | FR-012, FR-020 |
| TC-FIFO-002 | FIFO | Auto-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 movements | 2 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. | Critical | FR-012, FR-020, DEC-D27 |
| TC-FIFO-003 | FIFO | Khô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. | Critical | FR-012, FR-020, DEC-D23 |
Nhóm 15: HSD — Hạn sử dụng (TC-HSD-001 ~ TC-HSD-003)
| TC-ID | Nhóm | Mô tả | Precondition | Steps | Expected | Priority | FR-ref |
|---|---|---|---|---|---|---|---|
| TC-HSD-001 | HSD | Auto-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 notification | Lô 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)". | Critical | FR-019, DEC-D24 |
| TC-HSD-002 | HSD | Cảnh báo 90 ngày trước HSD | Lô 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 notification | Warning 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). | High | FR-019, DEC-D24 |
| TC-HSD-003 | HSD | FIFO 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 movements | Trừ 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. | Critical | FR-019, FR-020 |
Nhóm 16: Hủy lô + Reverse lô (TC-DISPOSE-001, TC-REVERSE-001 ~ TC-REVERSE-002)
| TC-ID | Nhóm | Mô tả | Precondition | Steps | Expected | Priority | FR-ref |
|---|---|---|---|---|---|---|---|
| TC-DISPOSE-001 | Dispose | Hủy lô thủ công | Lô 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. | Critical | FR-021, DEC-D26 |
| TC-REVERSE-001 | Reverse | Reverse trả lại lô gốc | DS-BATCH-001. Đã auto-deduct từ Lô A: 2ml (Lô A remaining = 498ml). | 1. Undo subtask done (chuyển done → in_progress) 2. Verify movements + batch | Lô A: remaining_qty += 2ml → 500ml. Reverse movement tạo: batch_id = Lô A, quantity_change = +2, source_type = auto_reverse. | Critical | FR-013 |
| TC-REVERSE-002 | Reverse | Reverse 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 + remaining | Lô 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. | Critical | FR-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-ID | Nhóm | Mô tả | Precondition | Steps | Expected | Priority | FR-ref |
|---|---|---|---|---|---|---|---|
| TC-CFG-001 | ConfigImport | Import danh mục vật tư + giá bằng Excel | DS-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. | High | FR-017, DEC-D13 |
| TC-CFG-002 | ConfigImport | Import 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 preview | Dò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). | High | FR-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-ID | Nhóm | Mô tả | Precondition | Steps | Expected | Priority | FR-ref |
|---|---|---|---|---|---|---|---|
| TC-TRF-001 | Transfer | Manager chuyển kho happy path — chọn SP + lô nguồn + nhập SL → xác nhận | Login 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). | Critical | FR-022, DEC-D29, DEC-D31, DEC-D32, DEC-D33, DEC-D36 |
| TC-TRF-002 | Transfer | Chuyển nhiều SP cùng 1 phiếu — 3 SP khác nhau | Login 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. | Critical | FR-022, DEC-D33 |
| TC-TRF-003 | Transfer | Quy đổi đơn vị đúng — 2 chai × 500ml = 1,000ml | Login 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. | Critical | FR-022, DEC-D32, DEC-D34 |
| TC-TRF-004 | Transfer | Giá + HSD kế thừa từ lô nguồn | Login 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. | Critical | FR-022, DEC-D36 |
Validation
| TC-ID | Nhóm | Mô tả | Precondition | Steps | Expected | Priority | FR-ref |
|---|---|---|---|---|---|---|---|
| TC-TRF-005 | Transfer | SL chuyển > tồn lô → block | Login 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. | Critical | FR-022, DEC-D36 |
| TC-TRF-006 | Transfer | SP chưa config quy đổi → block + link SCR-02 | Login 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. | Critical | FR-022, DEC-D34 |
| TC-TRF-007 | Transfer | Chưa chọn lô nguồn → block | Login 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. | High | FR-022, DEC-D36 |
| TC-TRF-008 | Transfer | Giá nhập = 0/NULL → warning vàng nhưng cho phép tiếp tục | Login 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. | High | FR-022 |
Permission
| TC-ID | Nhóm | Mô tả | Precondition | Steps | Expected | Priority | FR-ref |
|---|---|---|---|---|---|---|---|
| TC-TRF-009 | Transfer | Staff (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 buttons | Nú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. | High | FR-022, DEC-D31, Permission |
| TC-TRF-010 | Transfer | Manager branch A → chỉ thấy SP kho chi nhánh A | Login 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. | High | FR-022, Permission |
| TC-TRF-011 | Transfer | Admin → thấy tất cả chi nhánh | Login 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ện | Admin 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. | High | FR-022, Permission |
Concurrency & Edge Cases
| TC-ID | Nhóm | Mô tả | Precondition | Steps | Expected | Priority | FR-ref |
|---|---|---|---|---|---|---|---|
| TC-TRF-012 | Transfer | Race condition — 2 Manager chuyển cùng SP cùng lô cùng lúc | Login 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. | Critical | FR-022, DEC-D36 |
| TC-TRF-013 | Transfer | SP không có tồn trong kho chi nhánh → ẩn khỏi dropdown | Login 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 SP | SP không xuất hiện trong dropdown (filter: chỉ hiện SP có remaining_qty > 0 ở ít nhất 1 lô). | High | FR-022 |
| TC-TRF-014 | Transfer | Hủy phiếu chuyển khi batch chưa dùng → reverse cả 2 bên | TC-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. | Critical | FR-022 |
| TC-TRF-015 | Transfer | Hủy phiếu chuyển khi batch đã auto-deduct → block hủy | TC-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. | Critical | FR-022 |
Integration
| TC-ID | Nhóm | Mô tả | Precondition | Steps | Expected | Priority | FR-ref |
|---|---|---|---|---|---|---|---|
| TC-TRF-016 | Transfer | Sau chuyển kho → subtask done → auto-deduct FIFO từ batch mới | TC-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. | Critical | FR-022, FR-012, FR-020 |
| TC-TRF-017 | Transfer | inventory_document tạo đúng behavior + product_supplying records | TC-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_document | inventory_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. | High | FR-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_warehousematerial_price_config(+ unique indexes)material_usage_unitmaterial_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_changehoạt động (test 1 subtask done → có movement) - [ ] Cron
material_batch_expiry_checkregistered + tested trên test env - [ ]
pnpm codegenpass (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
| Version | Ngày | Thay đổi | Tác giả |
|---|---|---|---|
| 1.0 | 2026-03-26 | Khởi tạo QA Test Plan: 12 nhóm, 46 TCs, 8 seed data scripts, entry/exit criteria | PO/BA + AI |
| 1.1 | 2026-03-27 | D2: 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.0 | 2026-03-27 | Design 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.1 | 2026-03-27 | Fix 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.0 | 2026-03-27 | Design 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 |