Skip to content

PRD: Kho vật tư — Quản lý định lượng & giá vật tư dịch vụ

Feature slug: material-warehouseVersion: 3.0 Ngày: 2026-03-27 Profile: L (Large) Tác giả: PO/BA Design doc: docs/superpowers/specs/2026-03-26-material-warehouse-design.md


Executive Summary (TL;DR)

  1. Kho vật tư kết nối kho chi nhánh — Tạo module Kho vật tư (Material Warehouse) per chi nhánh, kết nối kho chi nhánh qua phiếu chuyển kho (tận dụng inventory_document hiện tại). Manager chọn SP từ kho chi nhánh → nhập SL (đơn vị mua: chai) → hệ thống tự quy đổi sang đơn vị kho (ml) + tạo lô FIFO. Hỗ trợ nhập tay/Excel làm fallback cho chi nhánh data chưa chuẩn. Kiến trúc 4 cấp: NCC → Kho tổng → Kho chi nhánh → Kho vật tư.
  2. Chi phí vật tư trên subtask + auto-deduct — KTV ghi nhận vật tư thực tế trên subtask (chọn ĐVT, nhập SL), hệ thống tự động tính chi phí (snapshot giá × SL) và trừ tồn kho khi subtask hoàn thành. Undo/cancel tự động reverse.
  3. Tồn kho + kiểm kê + cảnh báo — Kiểm kê + điều chỉnh chênh lệch, cảnh báo tồn kho thấp + HSD sắp hết hạn realtime gửi Manager/Admin. Chi phí vật tư tổng hợp vào sidebar tài chính đơn hàng.

Milestones

MilestoneNội dungTarget DateOwnerStatus
M1DB migration: 5 bảng mới (incl. material_batch) + ALTER project_task_material + view + indexesT+2dBackend DevPending
M2Hasura metadata: 5 bảng + permissions per role + relationshipsT+3dBackend DevPending
M3FE: SCR-01 Trang danh sách kho vật tư + SCR-02 Form cấu hìnhT+7dFrontend DevPending
M4FE: SCR-03 Chi tiết + lịch sử giá/movementT+9dFrontend DevPending
M5aFE: SCR-08 Chuyển kho chi nhánh → vật tư ★ NEW v3.0T+11dFrontend DevPending
M5bFE: SCR-06 Nhập kho manual (fallback) + Excel import + SCR-07 Kiểm kêT+13dFrontend DevPending
M6aBE: Action transferToMaterialWarehouse + Event trigger kết nối inventory_document ★ NEW v3.0T+10dBackend DevPending
M6bBE: Event trigger materialAutoDeduct + materialAutoReverseT+13dBackend DevPending
M7FE: Sửa MaterialForm (SCR-04) + Aggregate (SCR-05) thêm cột giá/thành tiềnT+14dFrontend DevPending
M8BE: Low stock notification + Sidebar tài chính kết nốiT+12dBackend DevPending
M9QA testing (~30-40 test cases)T+17dQAPending
M10Deploy staging → productionT+20dDevOpsPending

Trạng thái Sign-off

DomainNgười reviewStatusNgày
Business (PO)PO/BAApproved2026-03-26
UX/UIPending (cần UI Designer review wireframes SCR-01 → SCR-07)
Technical (Tech Lead)Pending (cần review data model + event trigger)
QAPending (cần QA review test plan)
OpsPending (cần verify migration strategy staging)

Action Items — Unblock Readiness

IDItemOwnerDeadlineStatus
AI-01UI Designer review wireframes (SCR-01 → SCR-08), confirm layout kho vật tư + form chuyển kho (SCR-08 v3.0)UI/UX DesignerT+3dOpen
AI-02Tech Lead review data model (material_price_config versioning, material_stock_movement advisory lock pattern)Tech LeadT+3dOpen
AI-03QA review test plan + confirm seed data cho 30-40 test casesQA LeadT+3dOpen
AI-04DevOps verify migration strategy trên staging (5 bảng mới + ALTER + 1 VIEW)DevOpsT+2dOpen
AI-05PO xác nhận danh sách vật tư mẫu cho seed data (ít nhất 10 sản phẩm ngành spa)POT+3dOpen
AI-06Xác nhận format file Excel import tồn kho (cột, validation rules)PO + Frontend DevT+5dOpen

Pending Decisions

IDQuyết địnhRecommendationOwnerDeadlineStatus
Tất cả PDs đã resolved thành DEC-D01 → DEC-D27 (D04/D17 superseded)All Resolved

RACI

DeliverablePO/BAFrontend DevBackend DevQAUI/UXDevOps
prd.mdR/ACCIC
ui-spec.mdRCIA
dev-spec.mdCRAIC
qa-test-plan.mdCIIR/A
go-live-checklist.mdCIICR/A
DB Migration (5 bảng + ALTER + VIEW)IR/AIC
Hasura MetadataIR/AIC
Event triggers (auto-deduct/reverse)IR/AC
SCR-01 → SCR-03 (Kho vật tư pages)IR/ACC
SCR-06, SCR-07 (Nhập kho, Kiểm kê)IR/ACC
SCR-04, SCR-05 (MaterialForm + Aggregate sửa)IR/AC
Sidebar tài chính kết nốiIR/ACC
Low stock notificationIR/AC
Deploy stagingICCAR
Deploy productionAIICR

R = Responsible, A = Accountable, C = Consulted, I = Informed


Backlog Phase sau

IDItemPriorityDependency
PH2-01Kết nối kho chính → phiếu chuyển khoMoved to Phase 1 (v3.0, DEC-D29). Chuyển kho chi nhánh → kho vật tư đã nằm trong scope chính.High Done
PH2-02FIFO / Weighted Average costingFIFO đã implement v2.0 (DEC-D22). WAC nếu cần sau.Medium Done
PH2-03Override giá per branch (UI form)MediumNhu cầu thực tế phát sinh
PH2-04Material recipe / BOM (pha trộn vật tư)LowSpa có nghiệp vụ pha trộn
PH2-05Chuyển kho vật tư giữa chi nhánhMediumMulti-branch operation
PH2-06Báo cáo chi phí vật tư (theo KTV, dịch vụ, chi nhánh)HighData đủ 1-2 tháng
PH2-07Export báo cáo ExcelMediumPH2-06
PH2-08Partition material_stock_movement nếu > 10M recordsMediumMonitor 6 tháng
PH2-09Optimistic locking cho concurrent editing kiểm kêLowNếu phát sinh conflict
PH2-10Nhắc nhở kiểm kê định kỳ (notification weekly/monthly)HighCần notification-api scheduler
PH2-11Nút "Thay thế vật tư" trên form subtaskHighFR-009 (MaterialForm)

Z) Decision Log

Decision Log LUÔN ĐẶT ĐẦU TIÊN. Mọi section downstream tham chiếu DEC-ID.

Z1 — Business Decisions

IDQuyết địnhLý doNgàyStatus
DEC-D03wastage_rate (hao hụt hệ thống) tính vào stock_unit_price luônKhông tính → báo cáo chi phí luôn thấp hơn thực tế 2-5%. PO ra quyết định sai.2026-03-26Locked
DEC-D09Giá chung toàn hệ thống (branch_id = NULL), field branch_id sẵn cho override per branchChuỗi spa VN mua hàng tập trung (1 supplier). Override per branch khi cần = thêm record branch_id cụ thể.2026-03-26Locked
DEC-D10Ẩn cột giá/thành tiền với Staff (KTV)KTV không cần biết giá vốn vật tư. Giá là thông tin quản lý (Manager/Admin).2026-03-26Locked
DEC-D11Product chưa có config → vẫn thêm vào subtask, hiện "(chưa có giá)"Không block KTV ghi nhận vật tư khi Admin chưa kịp config giá.2026-03-26Locked
DEC-D12Kho vật tư độc lập, KHÔNG kết nối kho chínhSuperseded by DEC-D29 (v3.0). Kết nối kho chi nhánh với dual-input (transfer + manual fallback).2026-03-26Superseded
DEC-D13Nhập tay/Excel là fallback, không phải luồng chính (v3.0)Luồng chính: chuyển kho từ kho chi nhánh. Fallback cho chi nhánh data chưa chuẩn.2026-03-27Locked
DEC-D29Kết nối kho chi nhánh → kho vật tư. Kiến trúc 4 cấp: NCC→Kho tổng→Kho chi nhánh→Kho vật tư. Tận dụng inventory_document hiện tại.Kho chi nhánh đã có sẵn danh mục SP + giá nhập + tồn kho. Config 100-300 SP bằng tay không khả thi. Giảm 80%+ nhập liệu.2026-03-27Locked
DEC-D30Dual-input: (1) Chuyển kho từ kho chi nhánh (primary), (2) Nhập tay/Excel (fallback).Data kho chính chưa chuẩn ở một số chi nhánh. Cả 2 nguồn → không block go-live.2026-03-27Locked
DEC-D31Không cần duyệt chuyển kho chi nhánh → vật tư (cùng chi nhánh). Manager tự thực hiện.Cùng 1 chi nhánh, hàng di chuyển nội bộ. Thêm duyệt = chậm + không cần thiết.2026-03-27Locked
DEC-D35Data cleanup kho chính là prerequisite song song. Chi nhánh data chưa chuẩn → dùng nhập tay.PO xác nhận: data kho chính chưa tốt ở một số chi nhánh. Cần plan dọn data riêng, không block go-live.2026-03-27Locked
DEC-D36Chọn lô nguồn thủ công khi chuyển kho. Manager chọn cụ thể lô nào từ kho chi nhánh (lot_number, expiry, price, qty_remain). Mỗi lô chuyển = 1 material_batch. Reuse ProductLotNumberSelect.Kho chi nhánh có nhiều lô khác giá/HSD. Không chọn rõ → giá vốn + HSD lệch audit. Hệ thống đã có sẵn UI chọn lô.2026-03-27Locked
DEC-D37Transfer transaction strategy: transferToMaterialWarehouse action chạy trong single PostgreSQL transactionecommerce schema (inventory_document, product_supplying) và material schema (material_batch, material_stock_movement) cùng 1 PostgreSQL instance. Go handler dùng 1 DB connection + BEGIN/COMMIT. Nếu tương lai tách DB → chuyển sang Outbox pattern + compensation.Cả 2 bên ghi nhận cùng 1 transaction → không xảy ra trường hợp trừ kho chi nhánh mà không tạo batch, hoặc ngược lại.2026-03-27Locked
DEC-D17Cho phép tồn kho âm + cảnh báo đỏReplaced by DEC-D23KTV ghi nhận trước, Admin nhập kho sau.2026-03-26Superseded
DEC-D19Subtask canceled sau done → exclude khỏi material_cost trong sidebar tài chínhTồn kho đã reverse nhưng chi phí vẫn hiển thị = data misleading cho Manager.2026-03-27Locked
DEC-D21Manager được xem chi phí vật tư trong sidebar tài chính đơn hàng (branch mình)Pain point chính là Manager không biết chi phí → phải giải quyết.2026-03-27Locked
DEC-D23Không cho phép tồn kho âm. remaining_qty >= 0 (CHECK constraint)Team Kho yêu cầu. Muốn xuất phải nhập trước.2026-03-27Locked
DEC-D24Hạn sử dụng (HSD) per lô: cảnh báo 3 tháng trước, auto-lock khi hết hạnTeam Kho đang phải lọc thủ công hàng tháng.2026-03-27Locked
DEC-D25Nhập cùng giá cùng ngày vẫn tách lôCó thể khác mã lô NCC, khác HSD.2026-03-27Locked
DEC-D26Lô lỗi/thu hồi → xuất hủy (disposal) + ghi chúGiữ audit trail, không xóa lô.2026-03-27Locked

Z2 — UX Decisions

IDQuyết địnhLý doNgàyStatus
DEC-D08UI đặt tại Kho > Kho vật tư (trang riêng trong sidebar)Module mới, không phải tab phụ trên product. Admin cần nhìn tổng danh sách vật tư. Phase B mở rộng tự nhiên.2026-03-26Locked
DEC-D20Wastage KTV (wastage_quantity) mặc định ẩn trên form subtask. Admin bật qua Settings nếu cần.KTV không đo được hao hụt chính xác, sẽ ghi 0 → data rác.2026-03-27Locked

Z3 — Technical Decisions

IDQuyết địnhLý doNgàyStatus
DEC-D01NUMERIC(15,4) cho đơn giá nội bộ, BIGINT cho amount cuốiTránh mất precision khi chia (VD: 890,000/30 = 29,666.666đ). Round 1 lần duy nhất ở amount.2026-03-26Locked
DEC-D02Price versioning qua effective_from/to, tạo version mới không sửa cũAudit trail + báo cáo so sánh giá theo thời gian. Admin cần biết "chi phí tăng vì giá lên hay dùng nhiều hơn?"2026-03-26Locked
DEC-D04Phase này dùng Latest Price, field costing_method sẵn cho FIFO/WAReplaced by DEC-D22Lot tracking cần product_supplying integration — chưa làm.2026-03-26Superseded
DEC-D05Two-Phase Pricing (Ref: DEC-D28): Phase 1 (save) = giá ước tính từ lô FIFO cũ nhất (display only). Phase 2 (done) = giá thực tế từ FIFO deduct (final, locked).Batch-based FIFO nên giá chỉ chính xác khi biết lô nào thực sự xuất.2026-03-27Locked
DEC-D06stock_unit lock khi đã có material_usage_unit referenceĐổi stock_unit → tất cả usage_unit + task_material hỏng. Muốn đổi = tạo config mới.2026-03-26Locked
DEC-D07to_stock_factor > 0, is_discrete validate số nguyênNgăn config sai (factor=0 → giá=0, factor<0 → giá âm). Mask = miếng → không dùng 0.5 miếng.2026-03-26Locked
DEC-D14Tự động trừ kho khi subtask status → done (event trigger)Giảm thao tác KTV, data realtime. Giống pattern invoice_complete đã có.2026-03-26Locked
DEC-D15v2.0+: Batch-level tracking. material_batch.remaining_qty = tồn per lô. material_stock_movement.batch_remaining_after = tồn lô SAU giao dịch. Tồn kho tổng = SUM(remaining_qty) qua material_stock_balance VIEW. (v1.x: running_balance global — superseded)Batch FIFO cần track per lô. remaining_qty trên batch là nguồn sự thật. Movement ghi batch_remaining_after cho audit trail.2026-03-27Locked
DEC-D16source_type enum có 'transfer' — dùng ngay cho chuyển kho chi nhánh → kho vật tư (v3.0, DEC-D29)Schema đã sẵn sàng. Không cần migration thêm.2026-03-27Locked
DEC-D32Nhập theo đơn vị mua khi chuyển kho. Manager gõ "2 chai" → hệ thống quy đổi 2 × 500ml = 1,000ml → tạo lô.Thực tế kho: người ta bê nguyên chai/hộp, không đong ml. Kiểm kê khớp: kệ kho thiếu đúng 2 chai.2026-03-27Locked
DEC-D33Behavior mới cho inventory_document: export_material_warehouse + import_material_warehouse. Extend hệ thống hiện tại.inventory_document đã có đầy đủ status flow, product_supplying, notification. Reuse ~60%.2026-03-27Locked
DEC-D34Config quy đổi 1 lần per product: "1 chai = 500ml". Lưu trong material_price_config.source_quantity.Admin config 1 lần, sau đó Manager chỉ nhập số chai. Giá nhập lấy từ kho chi nhánh, không config tay.2026-03-27Locked
DEC-D18Undo subtask done → reverse stock movementData consistency: trừ kho sai phải cộng lại. Tạo movement mới +quantity, không xóa movement cũ.2026-03-26Locked
DEC-D22Batch-based FIFO: mỗi lần nhập = 1 lô, xuất lô cũ nhất trước, giá theo lôTeam Kho xác nhận: kho chính đã quản lý theo lô. FIFO vì dùng lô cũ trước để tránh hết hạn.2026-03-27Locked
DEC-D27FIFO split: 1 lần xuất vượt 2+ lô → tách nhiều movements, mỗi movement gắn 1 lôChi phí chính xác theo giá từng lô.2026-03-27Locked
DEC-D28Two-Phase Pricing: Phase 1 (save) = giá ước tính từ FIFO batch hiện tại; Phase 2 (done) = giá thực tế từ FIFO deduct (bình quân gia quyền nếu split). Sau done = locked.Batch-based FIFO nên giá chỉ chính xác khi biết lô nào thực sự xuất. Giữa save và done, lô có thể thay đổi (lô cũ hết, lô mới nhập).2026-03-27Locked

Z4 — QA Decisions

IDQuyết địnhLý doNgàyStatus
DEC-Q01Test concurrent auto-deduct: 2 KTV trừ kho cùng product cùng lúcVerify pg_advisory_xact_lock serialize đúng. running_balance phải nhất quán.2026-03-26Locked
DEC-Q02Test tồn kho âm: cho phép + hiển thị cảnh báo đỏ → Test không cho phép tồn kho âm: block deduct khi không đủ tồn kho (Ref: DEC-D23, supersede DEC-D17)Verify flow: KTV ghi → trừ kho → không đủ → block + thông báo "Không đủ tồn kho".2026-03-27Locked
DEC-Q03Test price versioning: đơn cũ giữ snapshot, đơn mới lấy giá mớiVerify FORMULA-003: amount snapshot không đổi khi admin cập nhật giá.2026-03-26Locked

A1) Blueprint

FieldValue
Feature NameKho vật tư — Quản lý định lượng & giá vật tư dịch vụ
Feature Slugmaterial-warehouse
TypeNew Module
ProfileL (Large)
PlatformWeb (diva-admin)
Module chínhKho > Kho vật tư (NEW), Projects > TaskForm/TaskDetail (MODIFY)
DB Domainproject (5 bảng mới incl. material_batch, 1 ALTER, 1 VIEW)
Related ServicesHasura Controller, ecommerce-api (product reference), notification-api
Branch Scopematerial_warehouse.branch_id → 1 chi nhánh = 1 kho vật tư
Dependencysubtask-material (feature đã có: project_task_material table)
Effort Estimate~33 man-days (BE 10.5d + FE 16.5d + QA 5d + Hasura 1.5d). v3.0: +4d cho transfer flow (SCR-08, BE action/event, QA transfer TCs)

A2) Context — Tổng quan dễ hiểu

1. Vấn đề hiện tại + hệ quả

Hệ thống spa Diva hiện có feature "Vật tư trên subtask" (subtask-material) cho phép KTV ghi nhận vật tư dự kiến theo từng bước công việc. Tuy nhiên:

  1. Không có giá vốn vật tư — KTV ghi "3 giọt Serum" nhưng hệ thống không biết 1 giọt = bao nhiêu tiền. Manager không tính được chi phí vật tư per đơn hàng.
  2. Không có kho vật tư riêng — Kho chính (kho chi nhánh) quản lý theo đơn vị mua (chai, hộp), không phải đơn vị sử dụng (ml, giọt, miếng). Không kiểm soát được tồn kho vật tư thực tế.
  3. Không tự động trừ kho — Subtask hoàn thành nhưng vật tư không bị trừ khỏi kho. Admin phải kiểm kê thủ công.
  4. Chi phí vật tư thiếu trong tổng hợp tài chính — Sidebar tài chính đơn hàng có tiền tour, commission, nhưng thiếu chi phí vật tư → lợi nhuận hiển thị sai.

Hệ quả: Manager không biết chi phí thực per đơn hàng (sai lệch 5-15%). Admin không biết vật tư nào sắp hết. PO không có data để đàm phán giá supplier.

2. Giải pháp + metric

Tạo module Kho vật tư (Material Warehouse) kết nối kho chi nhánh, quản lý:

  • Kết nối kho chính (v3.0) — Chuyển kho từ kho chi nhánh → kho vật tư, tận dụng inventory_document hiện tại. Nhập tay/Excel làm fallback.
  • Danh mục vật tư + giá versioned — Cấu hình quy đổi đơn vị (chai → ml → giọt), tính hao hụt hệ thống.
  • Tồn kho + nhập/xuất/kiểm kê — Auto-deduct khi subtask done, kiểm kê điều chỉnh.
  • Chi phí vật tư trên subtask — KTV chọn ĐVT, nhập SL, hệ thống tính đơn giá × SL = thành tiền (snapshot giá).
  • Kết nối tổng hợp tài chính — Chi phí vật tư hiển thị trong sidebar tài chính đơn hàng.

Từ không có giá vốn, không có kho vật tư, không tự trừ kho kho vật tư kết nối kho chi nhánh, giá từ FIFO batch, auto-deduct, chi phí vật tư trong tổng hợp tài chính.

Metric mục tiêu: 100% subtask có giá vật tư (sau config), tồn kho chênh lệch < 2% sau 1 tháng, chi phí vật tư hiển thị đúng trong sidebar tài chính.

3. Giao diện mô phỏng

SCR-01: Trang danh sách Kho vật tư:

#Vật tưĐV khoGiá/ĐVTồn khoTT
1Serum Laser XSP001ml4,082đ5,000mlXanh
2Gel làm mátSP042g2,000đ800gXanh
3Mask collagenSP018miếng30,000đ49Đỏ
4Bông tẩy trangSP023miếng500đ200Xanh

SCR-04: Form subtask — Bảng vật tư (nâng cấp có giá):

#Vật tưĐVTSLĐơn giáThành tiền
1Serum Laser XSP001giọt3204đ612đ
2Gel làm mátSP042tube115,000đ15,000đ
3Mask dưỡng daSP018miếng130,000đ30,000đ
Tổng:45,612đ

Cột Đơn giá + Thành tiền: ẩn với Staff (KTV) (Ref: DEC-D10)

4. Cách hoạt động — Quy trình 6 bước (v3.0)

NCC → Kho tổng (HQ) → Kho chi nhánh → Kho vật tư → KTV sử dụng → Auto-deduct

                             ① Chuyển kho (primary, DEC-D29)
                             ② Nhập tay (fallback, DEC-D30)
Admin: Config quy đổi ĐVT (1 lần) → Manager: Chuyển kho từ kho chi nhánh (nhập SL chai)

                                     Hệ thống: Quy đổi chai → ml, tạo lô FIFO, trừ kho chi nhánh

KTV: Ghi vật tư trên subtask → Chọn ĐVT → Nhập SL → Hệ thống tính giá (snapshot từ lô FIFO)

Hệ thống: Subtask done → Auto-deduct tồn kho FIFO → Cảnh báo nếu thấp → Sidebar tài chính
BướcAiHành độngDữ liệuTần suất
1Hệ thốngAuto-create kho vật tư khi chi nhánh chưa cómaterial_warehouse1 lần/branch
2AdminConfig quy đổi đơn vị per product: "1 chai = 500ml", ĐVT sử dụng, hao hụtmaterial_price_config + material_usage_unit1 lần/product, sửa khi cần
3aManagerChuyển kho (primary): Chọn SP từ kho chi nhánh → nhập SL (chai) → hệ thống trừ kho chi nhánh + tạo lô vật tư (ml)inventory_documentmaterial_batch + material_stock_movementKhi cần bổ sung
3bAdmin/ManagerNhập tay (fallback): Nhập trực tiếp khi kho chính chưa có data chuẩnmaterial_batch + material_stock_movement (source_type='stock_in')Fallback
4KTVGhi nhận vật tư trên subtask (chọn ĐVT, nhập SL)project_task_material (+unit_price, amount, stock_equivalent)Mỗi subtask
5Hệ thốngAuto-deduct FIFO khi subtask done, cảnh báo nếu tồn thấp + HSDmaterial_stock_movement (source_type='auto_deduct')Tự động

Quy tắc cốt lõi:

#Quy tắcVí dụRef DEC
11 chi nhánh = 1 kho vật tư"Kho vật tư Quận 1", "Kho vật tư Quận 3"
21 product chỉ có 1 config active (global hoặc per branch)Serum X chỉ 1 giá active. Muốn đổi giá → version mới.DEC-D02
3Snapshot giá khi thêm vật tư vào subtaskTháng 1 giá 4,000đ/ml → subtask tháng 1 giữ 4,000đ dù tháng 3 đổi 5,000đDEC-D05
4Hao hụt hệ thống tính vào đơn giáSerum 2% hao hụt → giá/ml = 4,082đ thay vì 4,000đDEC-D03
5stock_unit lock khi đã có usage unit referenceSerum đã config giọt/muỗng/ml → không đổi ml sang gDEC-D06
6Không cho phép tồn kho âm, block deduct nếu không đủMuốn xuất phải nhập trước. remaining_qty >= 0 (CHECK constraint)DEC-D23
7Lấy giá: branch config → fallback global configChi nhánh có override → dùng. Không có → dùng chung.DEC-D09
8Auto-deduct khi subtask done, reverse khi undodone → trừ kho, undo done → cộng lạiDEC-D14, DEC-D18
9material_stock_movement là immutable logKhông UPDATE, không DELETE. Sai → tạo movement mới điều chỉnh.

5. Ví dụ thực tế

Case 1 — Happy path: Admin setup + KTV dùng + auto-deduct

Bối cảnh: Chi nhánh Quận 1, spa Diva. Admin Nguyễn Thị Mai setup kho vật tư.

BướcAiHành độngChi tiết
SetupAdmin MaiTạo kho "Kho vật tư Quận 1"material_warehouse.branch_id = Q1
SetupAdmin MaiConfig Serum Laser X: giá nhập 2,000,000đ/chai 500ml, hao hụt 2%stock_unit_price = 2,000,000 / (500 × 0.98) = 4,081.63đ/ml
SetupAdmin MaiThêm ĐVT: giọt (0.05ml, 204.08đ), muỗng (5ml, 20,408.16đ)material_usage_unit 3 records
Nhập khoAdmin MaiNhập tồn kho ban đầu: 5,000mlmovement: +5,000ml, balance = 5,000ml
Dịch vụKTV Trần Thúy LinhSubtask "Laser sắc tố" — thêm 3 giọt Serumamount = ROUND(204.08 × 3) = 612đ
Dịch vụKTV LinhSubtask doneauto-deduct: -0.15ml (3 × 0.05), balance = 4,999.85ml
Tài chínhHệ thốngSidebar đơn hàng hiển thịChi phí vật tư: 612đ

Case 2 — Edge case: Không đủ tồn kho + cảnh báo

BướcHành độngKết quả
1Mask collagen tồn kho = 49 miếng (lô L001, remaining = 49), min_stock = 50Hiển thị đỏ "Dưới ngưỡng"
2KTV ghi subtask dùng 3 miếng Maskstock_equivalent = 3 miếng
3Subtask done → auto-deduct từ lô L001 (FIFO)L001.remaining_qty = 49 - 3 = 46 miếng
4Notification gửi Manager Q1 + Admin"[Quận 1] Mask collagen còn 46 miếng, dưới ngưỡng 50"
5KTV ghi subtask dùng 50 miếng → subtask doneBlock: "Không đủ tồn kho Mask collagen (cần 50, còn 46)". Subtask không chuyển done. (Ref: DEC-D23)

Case 3 — Edge case: Admin đổi giá, subtask cũ giữ snapshot

Thời điểmHành độngGiá/ml
01/01/2026Admin config Serum: 2,000,000đ/500ml/2%4,081.63đ
15/01/2026KTV tạo subtask A: 3 giọt × 204.08đ = 612đSnapshot: 204.08đ/giọt
01/03/2026Admin cập nhật giá: 2,500,000đ/500ml/2%5,102.04đ (version mới)
02/03/2026KTV tạo subtask B: 3 giọt × 255.10đ = 765đSnapshot: 255.10đ/giọt
Kết quảSubtask A vẫn 612đ, subtask B là 765đGiá đúng per thời điểm

Case 4 — Edge case: Kiểm kê phát hiện chênh lệch

Khoản mụcGiá trị
Hệ thống: Serum Laser X4,999.75ml
Thực tế (đếm):4,850ml
Chênh lệch:-149.75ml
Admin nhập lý do:"Hao hụt bay hơi + sai đo"
Movement tạo:source_type='adjustment', quantity_change=-149.75, running_balance=4,850

Case 5 — Edge case: Product chưa config giá

BướcHành độngKết quả
1KTV thêm "Kem chống nắng" vào subtaskSản phẩm chưa có material_price_config
2Hiển thịTên SP, ĐVT mặc định, SL — cột giá hiện "(chưa có giá)"
3Không block KTVVẫn lưu được. unit_price = NULL, amount = NULL
4Subtask doneKhông trừ kho (vì total_stock_equivalent IS NULL)
5Admin config giá sauSubtask cũ giữ NULL, subtask mới lấy giá mới

6. Ai thấy gì + lý do

RoleThấy đượcKhông thấyLý doRef DEC
Staff (KTV)Vật tư trên subtask mình (tên, mã, ĐVT, SL)Cột giá/thành tiền, Menu Kho vật tưKTV không cần biết giá vốn. Kho là nghiệp vụ quản lý.DEC-D10
ManagerKho vật tư branch mình, cấu hình readonly, nhập kho, kiểm kê, lịch sử. Cột giá trên subtask.Thêm/sửa config giá (Admin only)Manager kiểm soát branch, nhưng giá do Admin tập trung.DEC-D09
AdminTất cả: config giá, mọi branch, sidebar tài chínhFull access

7. Công thức tóm tắt

Chỉ sốCông thứcVí dụ
Giá/ĐV khosource_price / (source_quantity × (1 - wastage_rate))2,000,000 / (500 × 0.98) = 4,081.63đ/ml
Giá/ĐV sử dụngstock_unit_price × to_stock_factor4,081.63 × 0.05 = 204.08đ/giọt
Chi phí per subtaskROUND(unit_price × quantity)204.08 × 3 = 612đ
Quy đổi trừ kho(quantity + wastage) × to_stock_factor(3 + 1) × 0.05 = 0.20ml
Tổng chi phí đơn hàngSUM(ptm.amount) WHERE subtask → order612 + 15,000 + 30,000 = 45,612đ

Chi tiết: xem section Formulas (C3) trong dev-spec.md

8. Tình huống đặc biệt

Tình huốngHệ thống xử lý
Product chưa config giáThêm được, hiện "(chưa có giá)". unit_price = NULL, không trừ kho. (Ref: DEC-D11)
Product bị disable sau khi đã thêmBadge "Ngưng KD", tồn kho giữ nguyên, không cho thêm mới.
Subtask done → undo doneReverse: tạo movement +quantity. running_balance cộng lại. (Ref: DEC-D18)
Subtask canceled (chưa done)Không trừ kho (chưa done = chưa auto-deduct).
Subtask canceled (đã done)Reverse movement (giống undo done).
2 KTV trừ kho cùng lúc cùng productpg_advisory_xact_lock serialize. running_balance nhất quán.
Admin đổi giá supplierVersion mới, subtask cũ giữ snapshot giá cũ. (Ref: DEC-D02, DEC-D05)
Không đủ tồn khoBlock deduct, hiển thị lỗi "Không đủ tồn kho [tên vật tư]". Muốn xuất phải nhập trước. (Ref: DEC-D23)
Đơn vị rời (Mask = miếng, is_discrete = true)SL phải nguyên. FE block nhập 0.5 miếng. (Ref: DEC-D07)
Kiểm kê: thực tế khác hệ thốngTạo movement source_type='adjustment' với chênh lệch.
Nhập Excel sai formatHiển thị lỗi per dòng. Không import dòng lỗi, import dòng đúng.
Hao hụt kỹ thuật (KTV đổ bỏ)Ghi wastage_quantity per subtask, tính vào total_stock_equivalent khi trừ kho.

9. Lộ trình + tại sao

PhaseNội dungTại sao chiaTrạng thái
Phase 0 (đã xong)subtask-material: ghi nhận vật tư trên subtask (không có giá, không có kho)Giải quyết vấn đề cấp bách nhất: ghi nhận vật tư đúng subtaskDone
Phase 1 (hiện tại)Kho vật tư: giá + quy đổi + tồn kho + auto-deduct + sidebar tài chínhAdmin cần biết chi phí. Manager cần tổng hợp tài chính. KTV cần biết vật tư còn bao nhiêu.In scope
Phase 2 (sau)Kết nối kho chính (đã move Phase 1 v3.0), WAC (nếu cần), báo cáo, export, chuyển kho giữa chi nhánhFIFO đã implement v2.0. Kết nối kho chính đã implement v3.0. Báo cáo cần data 1-2 tháng.Backlog

v3.0 — Tại sao kết nối kho chính ngay? Kho chi nhánh đã có sẵn danh mục 100-300 SP + giá nhập + tồn kho. Config thủ công từng SP không khả thi. Giải pháp: dual-input — chi nhánh data tốt → chuyển kho (primary), chưa tốt → nhập tay (fallback). Song song dọn data kho chính, dần chuyển tất cả sang transfer. (Ref: DEC-D29, DEC-D30, DEC-D35)

10. Giá trị mang lại

Đối tượngTrướcSauSố liệu cụ thể
ManagerKhông biết chi phí vật tư per đơn hàngSidebar tài chính hiển thị chi phí vật tư chính xácTừ 0% → 100% đơn hàng có chi phí vật tư
AdminKhông biết vật tư nào sắp hết, kiểm kê thủ côngCảnh báo realtime + kiểm kê so sánh hệ thống vs thực tếGiảm 80% thời gian kiểm kê (ước tính)
KTVChỉ ghi SL vật tư, không biết giáGhi SL, hệ thống tính giá tự động (KTV không thấy giá)0 thao tác thêm cho KTV
POKhông có data chi phí vật tư để phân tíchData chi phí per dịch vụ/KTV/chi nhánh cho quyết định mua hàngTiết kiệm 2-5% chi phí mua sắm (ước tính)
Tài chínhtotal_cost thiếu chi phí vật tư → sai lệch 5-15%total_cost = tour_money + material_cost + commission + fixed_costLợi nhuận chính xác hơn 5-15%

A3) Goals & Metrics

#GoalMetricTargetCách đo
G1100% vật tư có giá (sau config)% project_task_materialunit_price IS NOT NULL> 90% (sau 2 tuần)SQL query
G2Tồn kho chính xácChênh lệch hệ thống vs kiểm kê thực tế< 2% (sau 1 tháng)ABS(system_balance - actual) / actual × 100
G3Chi phí vật tư hiển thị đúng trong sidebar tài chính% đơn hàng có material_cost > 0 (khi có vật tư)100%SQL query
G4Cảnh báo tồn kho kịp thờiThời gian từ tồn kho ≤ min_stock → notification< 5 phútNotification log timestamp
G5Admin setup kho nhanhThời gian setup kho vật tư cho 1 chi nhánh (50 products)< 2 giờ (Excel import)Tracking

A4) Personas

PersonaVai tròTần suất sử dụngNhu cầu chínhScreens chính
Admin Nguyễn Thị MaiAdmin hệ thống, quản lý giá vốn toàn chuỗiHàng ngày (config), hàng tuần (kiểm kê)Cấu hình giá chính xác, xem lịch sử giá, export báo cáoSCR-01, SCR-02, SCR-03, SCR-06, SCR-07
Manager Lê Văn HùngQuản lý chi nhánh Q1, kiểm soát tồn khoHàng ngày (xem tồn), hàng tuần (nhập kho, kiểm kê)Biết vật tư còn bao nhiêu, chi phí vật tư per đơn hàngSCR-01, SCR-03, SCR-06, SCR-07, SCR-05
KTV Trần Thúy LinhKỹ thuật viên thực hiện dịch vụ, ghi nhận vật tư10-30 subtask/ngàyForm nhanh, chọn ĐVT, nhập SL. Không cần biết giá.SCR-04 (MaterialForm)
PO/Chủ spaXem tổng hợp tài chính, ra quyết định mua hàngHàng tuầnChi phí vật tư per đơn hàng, so sánh chi phí theo thời gianSidebar tài chính đơn hàng

A5) Functional Requirements

Nhóm 1: Kho vật tư per branch (FR-001 → FR-003)

FR-001: Tạo kho vật tư cho chi nhánh

Mô tả: Admin tạo kho vật tư cho mỗi chi nhánh. Mỗi chi nhánh chỉ có tối đa 1 kho vật tư.

AC:

  • [ ] Admin vào Kho > Kho vật tư → hệ thống tự tạo kho cho branch hiện tại nếu chưa có
  • [ ] Tên kho mặc định: "Kho vật tư [tên chi nhánh]", Admin có thể sửa
  • [ ] Constraint: UNIQUE(branch_id) — 1 chi nhánh = 1 kho vật tư
  • [ ] Kho có trạng thái is_active (mặc định true). Khi inactive → ẩn khỏi danh sách, không cho nhập/xuất
  • [ ] Soft delete qua deleted_at

Validation:

  • branch_id bắt buộc, NOT NULL
  • name bắt buộc, max 200 ký tự

Edge cases:

  • Admin truy cập branch chưa có kho → hệ thống tự tạo kho mặc định (tên = 'Kho vật tư [tên chi nhánh]'). Không cần bấm nút.
  • Admin cố tạo kho thứ 2 cho cùng branch → DB reject (unique constraint), hiển thị lỗi "Chi nhánh đã có kho vật tư"
  • Branch bị inactive → kho vật tư vẫn tồn tại nhưng ẩn khỏi navigation
  • Soft delete kho → tất cả data liên quan (movements, configs) giữ nguyên nhưng kho ẩn
  • Restore kho (undelete) → data hiện lại. Admin cần confirm "Khôi phục kho vật tư?"

FR-002: Xem danh sách vật tư trong kho (Ref: DEC-D08, SCR-01)

Mô tả: Admin/Manager xem danh sách tất cả vật tư đã cấu hình trong kho chi nhánh, kèm giá và tồn kho.

AC:

  • [ ] Hiển thị bảng: #, Vật tư (tên), Mã (SKU), ĐV kho, Giá/ĐV, Tồn kho, Trạng thái
  • [ ] Tồn kho lấy từ view material_stock_balance (O(1) query, Ref: DEC-D15)
  • [ ] Trạng thái tồn kho theo 4 mức: Xanh (> min×2), Vàng (> min), Đỏ (≤ min), Đỏ bold (= 0, Hết hàng)
  • [ ] Tìm kiếm theo tên/SKU — debounce 300ms, min 2 ký tự
  • [ ] Phân trang: 20 items/page mặc định, hỗ trợ 50/100
  • [ ] Sort: theo tên (A-Z), giá, tồn kho, trạng thái
  • [ ] Filter: trạng thái tồn kho (Tất cả / Dưới ngưỡng / Hết hàng)
  • [ ] Buttons: [+ Thêm vật tư], [Nhập kho], [Kiểm kê], [Xuất Excel]
  • [ ] "Xuất Excel" xuất danh sách hiện tại (max 5,000 dòng, đồng bộ)
  • [ ] Expandable row (QTable expand): click ▶ mở rộng dòng, hiện danh sách ĐVT sử dụng + giá per ĐVT (tính từ lô FIFO cũ nhất active)
  • [ ] Expandable row hiện danh sách lô active: Tên lô, tồn lô, giá/stock_unit, HSD. Lô FIFO cũ nhất đánh dấu "► đang xuất"
  • [ ] Giá ĐVT trên expandable = batch.unit_price × to_stock_factor từ lô FIFO cũ nhất (batch active, remaining_qty > 0, ORDER BY created_at ASC)

Edge cases:

  • Kho chưa có vật tư → empty state: "Chưa có vật tư. Bấm [+ Thêm vật tư] để bắt đầu."
  • Product chưa có tồn kho (chưa nhập) → cột tồn kho hiện "—" (dash)
  • Product bị disable → badge "Ngưng KD" cạnh tên, vẫn hiện trong danh sách
  • 500 vật tư → phân trang hoạt động bình thường, test performance < 2s load

  • Admin chọn chi nhánh khác (dropdown) → data reload, 0 kết quả nếu branch chưa có kho

FR-003: Quản lý vật tư — thêm/ẩn (Ref: DEC-D08)

Mô tả: Admin thêm product vào kho vật tư (tạo material_price_config) hoặc ẩn vật tư (soft delete config).

AC:

  • [ ] Button [+ Thêm vật tư] → mở dialog search product (từ bảng product ecommerce DB)
  • [ ] Chỉ hiện product chưa có active config trong kho → tránh trùng
  • [ ] Chọn product → mở form SCR-02 (cấu hình giá)
  • [ ] Ẩn vật tư → soft delete material_price_config (deleted_at = now()). Tồn kho giữ nguyên.
  • [ ] Khi ẩn: confirm dialog "Ẩn vật tư [tên]? Tồn kho sẽ giữ nguyên. KTV không thêm được vật tư này vào subtask mới."

Edge cases:

  • Product đã có config bị soft delete → cho phép tạo config mới (new version)
  • Product có tồn kho > 0 mà ẩn → cảnh báo "Vật tư còn [X] [unit] trong kho. Bạn chắc chắn ẩn?"
  • Product search trả về > 100 kết quả → phân trang trong dialog search
  • Admin cố thêm product đã có active config → lỗi "Vật tư đã tồn tại trong kho"
  • Product từ branch khác → không hiện (isolate per branch)

Nhóm 2: Cấu hình giá + đơn vị quy đổi (FR-004 → FR-006)

FR-004: Cấu hình giá vật tư per product (Ref: DEC-D01, DEC-D02, DEC-D03, DEC-D09, SCR-02)

Mô tả: Admin cấu hình giá nhập, đơn vị kho, hao hụt hệ thống, và ngưỡng cảnh báo tồn kho cho mỗi vật tư.

AC:

  • [ ] Form gồm: Sản phẩm (readonly), Đơn vị kho (dropdown: ml, g, miếng, custom), Giá nhập (VND), SL/đơn vị mua, Hao hụt hệ thống (%), Ngưỡng cảnh báo
  • [ ] stock_unit_price tự tính = source_price / (source_quantity × (1 - wastage_rate)) — hiển thị realtime khi nhập (FORMULA-001)
  • [ ] Giá nhập source_price: NUMERIC(15,2), min 0đ, max 999,999,999,999.99đ
  • [ ] SL/đơn vị mua source_quantity: NUMERIC(12,4), must > 0 (DB CHECK constraint)
  • [ ] Hao hụt wastage_rate: 0.0000 → 0.9999 (0% → 99.99%), mặc định 0
  • [ ] Ngưỡng cảnh báo min_stock: >= 0, NULL = không cảnh báo
  • [ ] Giá chung toàn hệ thống (branch_id = NULL). Field branch_id sẵn cho override (Ref: DEC-D09)
  • [ ] Khi giá là global (branch_id = NULL): hiện confirm dialog "Giá này áp dụng cho TẤT CẢ chi nhánh. Bạn chắc chắn?" trước khi lưu
  • [ ] Khi lưu: validate + insert material_price_config + cascade tạo/update material_usage_unit

Validation chi tiết:

  • source_quantity <= 0 → lỗi "SL/đơn vị mua phải > 0"
  • wastage_rate >= 1 → lỗi "Hao hụt không được ≥ 100%"
  • wastage_rate < 0 → lỗi "Hao hụt không được âm"
  • source_price < 0 → lỗi "Giá nhập không được âm"
  • source_price = 0 → cho phép (vật tư miễn phí / nội bộ)

Edge cases:

  • Hao hụt = 0% → stock_unit_price = source_price / source_quantity (không chia thêm)
  • Giá nhập = 0 → hợp lệ (stock_unit_price = 0, vật tư miễn phí)
  • source_quantity rất lớn (VD: 1,000,000) → stock_unit_price rất nhỏ → NUMERIC(15,4) đủ chứa
  • wastage_rate = 0.9999 (99.99%) → giá tăng 10,000× → cảnh báo "Hao hụt > 50%, xác nhận?"
  • Admin sửa đơn vị kho → lock nếu đã có usage unit reference (Ref: DEC-D06)

FR-005: Cập nhật giá (versioning) (Ref: DEC-D02, DEC-D05, SCR-03)

Mô tả: Admin cập nhật giá mới cho vật tư. Hệ thống tạo version mới, giữ lịch sử giá cũ.

AC:

  • [ ] Button [Cập nhật giá] trên SCR-03 → mở form pre-filled với giá hiện tại
  • [ ] Khi lưu: set effective_to = now() trên config cũ → insert config mới effective_from = now(), effective_to = NULL
  • [ ] Clone material_usage_unit từ config cũ sang config mới (giữ usage units, recalc usage_unit_price)
  • [ ] Subtask đã ghi nhận trước đó → giữ snapshot giá cũ (price_snapshot_at, config_version_id)
  • [ ] Subtask mới sau khi cập nhật → lấy giá mới
  • [ ] Lịch sử giá hiển thị trên SCR-03: bảng [Từ ngày | Đến ngày | Giá/ĐV kho | Người sửa]

Edge cases:

  • Admin cập nhật giá 2 lần trong 1 phút → 2 version liên tiếp, hợp lệ
  • Cập nhật giá trong khi KTV đang thêm vật tư → KTV lấy giá tại thời điểm save (snapshot)
  • Rollback giá (quay về giá cũ) → tạo version mới với giá = giá cũ (không delete version giữa)
  • Xem lịch sử giá: paginate 20 records/page, sort desc by effective_from
  • Config cũ đã soft delete → không hiện trong lịch sử giá

FR-006: Cấu hình đơn vị quy đổi (Ref: DEC-D06, DEC-D07, SCR-02)

Mô tả: Admin thêm/sửa đơn vị sử dụng (usage unit) cho mỗi vật tư, với hệ số quy đổi về đơn vị kho.

AC:

  • [ ] Bảng ĐVT trong SCR-02: Tên ĐVT, Hệ số (→ ĐV kho), Giá/ĐVT (auto-calc), Rời? (is_discrete), Mặc định?
  • [ ] to_stock_factor > 0 (CHECK constraint). VD: 1 giọt = 0.05ml → factor = 0.05
  • [ ] usage_unit_price = stock_unit_price × to_stock_factor (FORMULA-002), auto-calc realtime
  • [ ] is_discrete = true → KTV chỉ được nhập SL nguyên (VD: Mask = miếng → 1, 2, 3... không 0.5)
  • [ ] is_default = true → ĐVT mặc định khi KTV thêm vật tư vào subtask. Chỉ 1 default per config.
  • [ ] UNIQUE(config_id, usage_unit_name) → không cho trùng tên ĐVT
  • [ ] Ít nhất 1 ĐVT per config (bắt buộc)
  • [ ] Max 10 ĐVT per config (đủ cho mọi use case spa)

Edge cases:

  • to_stock_factor = 0 → DB CHECK block, FE validate "Hệ số phải > 0"
  • to_stock_factor rất nhỏ (0.001) → usage_unit_price rất nhỏ → hiển thị 4 decimal
  • Admin xóa ĐVT đã được reference bởi project_task_material → block, hiển thị "ĐVT đang được sử dụng bởi [N] subtask"
  • Admin sửa tên ĐVT → cho phép (soft rename, subtask cũ lưu snapshot tên cũ trong usage_unit field)
  • Admin set is_default cho ĐVT B khi ĐVT A đang default → A tự động bỏ default, B thành default

Nhóm 3a: Chuyển kho chi nhánh → Kho vật tư (FR-022) — NEW v3.0

FR-022: Chuyển kho chi nhánh → Kho vật tư (★ NEW v3.0, Ref: DEC-D29, DEC-D31, DEC-D32, DEC-D36, SCR-08)

Mô tả: Manager chọn SP từ kho chi nhánh, nhập SL theo đơn vị mua (chai/hộp), hệ thống tự quy đổi sang đơn vị kho (ml/g) + trừ tồn kho chi nhánh + tạo lô FIFO trong kho vật tư. Không cần duyệt.

AC:

  • [ ] Button [★ Chuyển kho] (primary) trên SCR-01 → mở form SCR-08
  • [ ] Form: Từ kho (kho chi nhánh, readonly) + Đến kho (kho vật tư, readonly)
  • [ ] Search SP từ kho chi nhánh: chỉ hiện SP có tồn > 0. Reuse ProductLotNumberSelect component (DEC-D36)
  • [ ] Chọn lô nguồn bắt buộc: Manager chọn cụ thể lô nào từ kho chi nhánh. Dropdown hiện: [Mã lô | NCC | HSD | Giá nhập | Tồn lô]. Validation: "Chọn mã lô hàng cho vật tư!" nếu chưa chọn.
  • [ ] Bảng editable: [Sản phẩm | SKU | Lô nguồn ▼ | Tồn lô | SL chuyển | ĐV mua]
  • [ ] Giá nhập + HSD: auto-fill từ lô nguồn đã chọn (product_supplying.price, expiry_at), read-only trên UI, không phải cost_price chung
  • [ ] Mỗi lô chuyển = 1 material_batch riêng (giữ nguyên giá + HSD từ lô nguồn → audit 1:1)
  • [ ] SL chuyển: > 0, ≤ tồn lô nguồn (quantity_remain). Vi phạm → "Tồn lô không đủ: cần X, lô chỉ còn Y"
  • [ ] Yêu cầu SP đã có config quy đổi (material_price_config.source_quantity). Chưa có → "Vui lòng cấu hình quy đổi đơn vị" + link SCR-02
  • [ ] Hiển thị SL quy đổi realtime: "2 chai → 1,000ml" (= SL × source_quantity)
  • [ ] Giá nhập: luôn lấy từ lô nguồn đã chọn (product_supplying.price), không cho sửa thủ công. Nếu price = 0/NULL thì hiển thị warning (EC-26) và vẫn cho phép tiếp tục theo giá nguồn; FE/BE/QA không implement manual override cho transfer
  • [ ] Canonical cost basis (DEC-D37b): product_supplying.price = giá per đơn vị mua (VD: 500,000đ/chai, KHÔNG phải tổng tiền). material_batch.purchase_price = giá từ product_supplying.price của lô nguồn (chuẩn hóa NULL -> 0, còn 0 giữ nguyên). material_batch.unit_price = purchase_price / (source_quantity × (1 - wastage_rate)) = giá per stock unit đã tính hao hụt. VD: 500,000đ / (500ml × 0.98) = 1,020.41đ/ml. Tất cả downstream pricing tính từ unit_price này.
  • [ ] Khi xác nhận:
    1. Tạo inventory_document (behavior: export_material_warehouse, status: released)
    2. Tạo product_supplying records (type: export) → trừ tồn kho chi nhánh
    3. Tạo material_batch per dòng (purchase_price, purchase_quantity = SL quy đổi stock unit, status='active')
    4. Tạo material_stock_movement (source_type='transfer', source_reference_id=inventory_document.id)
  • [ ] Không cần bước duyệt — Manager xác nhận = thực hiện luôn (DEC-D31)
  • [ ] Serialize per (warehouse, product) bằng pg_advisory_xact_lock
  • [ ] Tồn kho chi nhánh + tồn kho vật tư cập nhật realtime

Validation:

  • SL chuyển > tồn kho chi nhánh → lỗi "Tồn kho chi nhánh không đủ: cần X, chỉ còn Y"
  • SP chưa config quy đổi → block dòng đó, hiện link cấu hình
  • cost_price = 0/NULL → warning "Giá nhập = 0, vui lòng kiểm tra" (cho phép tiếp tục)
  • 0 dòng → disable button Xác nhận

Edge cases:

  • 2 Manager chuyển cùng SP cùng lúc → advisory lock serialize, first-come-first-served (EC-25)
  • SP không tồn tại trong kho chi nhánh → ẩn khỏi dropdown (EC-24)
  • Hủy phiếu chuyển sau released → nếu batch chưa dùng: reverse cả 2 bên. Nếu batch đã auto-deduct → block hủy (EC-27)

Nhóm 3b: Nhập kho Fallback (FR-007 → FR-008)

FR-007: Nhập tồn kho manual — Fallback (Ref: DEC-D13, DEC-D30, SCR-06)

Mô tả: Admin/Manager nhập tồn kho vật tư bằng form nhập tay (fallback cho chi nhánh data kho chính chưa chuẩn), tạo material_batch + material_stock_movement với source_type = 'stock_in'.

AC:

  • [ ] Button [Nhập tay] (secondary) trên SCR-01 → mở form SCR-06
  • [ ] Form: Kho (readonly), bảng editable [Vật tư | ĐV kho | SL nhập | Giá nhập | HSD | Mã lô | Ghi chú]
  • [ ] Giá nhập: > 0 (bắt buộc), NUMERIC(15,2) — giá per đơn vị mua (chai/hộp)
  • [ ] HSD: date picker, optional (vật tư không có HSD thì bỏ trống)
  • [ ] Mã lô: text, optional (mã lô nhà cung cấp)
  • [ ] [+ Thêm dòng] → search vật tư từ danh mục kho
  • [ ] SL nhập: > 0 (nhập kho luôn dương), NUMERIC(12,4)
  • [ ] Khi lưu: cho mỗi dòng:
    1. INSERT material_batch (status='active', initial_qty=SL, remaining_qty=SL, purchase_price, unit_price=purchase_price/SL_stock_units, expiry_date, batch_code)
    2. INSERT material_stock_movement (source_type='stock_in', batch_id=batch vừa tạo, quantity_change=+SL, note=ghi chú)
  • [ ] Serialize per (warehouse, product) bằng pg_advisory_xact_lock (Ref: DEC-D15)
  • [ ] Tồn kho cập nhật realtime trên SCR-01 sau khi nhập
  • [ ] Manager: chỉ nhập kho branch mình

Validation:

  • SL nhập ≤ 0 → lỗi "SL nhập phải > 0"
  • SL nhập > 1,000,000 → cảnh báo "SL lớn bất thường, xác nhận?"
  • Vật tư trùng trong 1 phiếu nhập → gộp hoặc lỗi "Vật tư đã có trong phiếu"

Edge cases:

  • Nhập kho cho vật tư chưa có tồn kho → tạo lô mới, remaining_qty = SL_nhập. Tồn kho tổng = SUM(remaining_qty) các lô active.
  • Nhập kho khi tồn kho = 0 → tạo lô mới, remaining_qty = SL_nhập. Tồn kho tổng = SUM(remaining_qty) các lô active.
  • 2 Admin nhập cùng lúc cùng product → advisory lock serialize, balance đúng
  • Nhập kho 0 dòng → disable button Xác nhận
  • Network error khi lưu → retry, idempotency qua transaction

FR-008: Nhập tồn kho bằng Excel — Fallback (Ref: DEC-D13, DEC-D30, SCR-06)

Mô tả: Admin/Manager import file Excel để nhập tồn kho hàng loạt.

AC:

  • [ ] Button [Import Excel] trên SCR-06 → upload file .xlsx/.csv
  • [ ] Template Excel: [Mã SP (SKU) | SL nhập | Giá nhập | HSD (dd/mm/yyyy) | Mã lô | Ghi chú] — có link download template mẫu
    • Giá nhập: bắt buộc, > 0 — giá per đơn vị mua (chai/hộp)
    • HSD: optional — để trống nếu vật tư không có HSD
    • Mã lô: optional — mã lô nhà cung cấp
  • [ ] Validate per dòng:
    • SKU không tồn tại trong danh mục kho → lỗi dòng đó
    • SL ≤ 0 → lỗi dòng đó
    • SL > 1,000,000 → warning dòng đó
  • [ ] Hiển thị preview: dòng hợp lệ (xanh) + dòng lỗi (đỏ + lý do)
  • [ ] Admin confirm → import dòng hợp lệ, bỏ qua dòng lỗi
  • [ ] Mỗi dòng tạo 1 material_batch (status='active') + 1 material_stock_movement (source_type='stock_in', batch_id gắn lô vừa tạo)
  • [ ] Mỗi lần import tạo 1 batch_import_id (UUID) gom tất cả batches + movements
  • [ ] Lịch sử nhập kho hiển thị theo batch, button "Hủy phiếu nhập" tạo reverse movements cho toàn batch
  • [ ] Max 500 dòng/file. > 500 → "File quá lớn, vui lòng chia nhỏ."
  • [ ] Xử lý đồng bộ (< 500 dòng → < 10 giây)

Edge cases:

  • File rỗng → "File không có dữ liệu"
  • File sai format (không phải xlsx/csv) → "Chỉ hỗ trợ file .xlsx hoặc .csv"
  • SKU trùng trong 1 file → gộp SL (cộng dồn) hoặc lỗi per dòng (chọn 1 — recommend gộp)
  • File có header sai → "Cột [X] không đúng format. Tải template mẫu."
  • Import thành công 450/500 dòng → hiển thị "Đã nhập 450 dòng thành công. 50 dòng lỗi." + link download lỗi

Nhóm 4: Ghi nhận vật tư trên subtask — giá + ĐVT (FR-009 → FR-011)

FR-009: Chọn đơn vị sử dụng khi thêm vật tư (Ref: DEC-D05, DEC-D07, SCR-04)

Mô tả: KTV chọn ĐVT (giọt, muỗng, ml...) khi thêm vật tư vào subtask. Hệ thống tính giá tự động.

AC:

  • [ ] Cột ĐVT trên MaterialForm → dropdown danh sách material_usage_unit của product đó
  • [ ] Mặc định chọn ĐVT có is_default = true
  • [ ] Khi chọn ĐVT → cập nhật:
    • unit_price = usage_unit_price (snapshot tại thời điểm thêm, FORMULA-002)
    • to_stock_factor = snapshot hệ số
    • amount = ROUND(unit_price × quantity) (FORMULA-003)
    • stock_equivalent = quantity × to_stock_factor (FORMULA-004)
  • [ ] Nếu product chưa có config → ĐVT dropdown trống, hiện text "(chưa có giá)" (Ref: DEC-D11)
  • [ ] Cột Đơn giá + Thành tiền: ẩn với Staff (KTV) (Ref: DEC-D10)

Validation:

  • quantity ≤ 0 → FE block "SL phải > 0"
  • is_discrete = true AND quantity có phần thập phân → FE block "Đơn vị rời, SL phải nguyên"
  • quantity > 10,000 → cảnh báo "SL lớn bất thường, xác nhận?"

Edge cases:

  • Product có 3 ĐVT: giọt, muỗng, ml → dropdown 3 options
  • KTV đổi ĐVT sau khi nhập SL → recalc unit_price, amount, stock_equivalent
  • Product chỉ có 1 ĐVT → auto-select, không hiện dropdown
  • ĐVT bị xóa sau khi đã lưu subtask → subtask cũ giữ snapshot tên ĐVT, không crash

FR-010: Ước tính giá khi lưu subtask + Finalize giá khi done (Ref: DEC-D05, DEC-D02, DEC-D22, DEC-D28)

Mô tả: Giá vật tư trải qua 2 phase:

  • Phase 1 (save subtask): Hệ thống ước tính giá từ lô FIFO cũ nhất hiện tại → ghi vào project_task_material với tag "ước tính". KTV thấy giá ngay nhưng đây chưa phải giá cuối.
  • Phase 2 (done subtask): Hệ thống deduct FIFO thực tế → tính giá bình quân gia quyền từ các lô đã xuất → UPDATE project_task_material với giá cuối cùng. Tag "ước tính" biến mất.
  • Sau done: Giá là FINAL, không thay đổi dù admin đổi config hoặc nhập lô mới.

AC:

  • [ ] Phase 1 — Khi lưu subtask (save/update):
    • Lấy lô FIFO cũ nhất có status='active' AND remaining_qty > 0 cho product
    • unit_price = batch.unit_price × to_stock_factor (giá ước tính)
    • amount = ROUND(unit_price × quantity) (ước tính)
    • stock_equivalent = quantity × to_stock_factor
    • total_stock_equivalent = (quantity + wastage_quantity) × to_stock_factor
    • price_snapshot_at = now()
    • UI hiển thị giá với badge "(ước tính)" nếu subtask chưa done
  • [ ] Phase 2 — Khi subtask done (auto-deduct FR-020):
    • Deduct FIFO thực tế → nếu split 2+ lô → tính bình quân gia quyền
    • UPDATE project_task_material SET unit_price = giá FIFO thực tế, amount = chi phí thực tế, price_snapshot_at = now()
    • Badge "(ước tính)" biến mất
  • [ ] Sau done: unit_priceamount không thay đổi nữa, kể cả admin đổi giá config
  • [ ] Nếu product chưa có lô nào (tồn kho = 0) → unit_price = NULL, amount = NULL, hiện "(chưa có giá)"
  • [ ] Subtask sửa lại SL → recalc amount, stock_equivalent, total_stock_equivalent (re-estimate từ lô FIFO hiện tại)
  • [ ] Subtask sửa ĐVT → recalc toàn bộ (lấy giá ước tính mới theo ĐVT mới)
  • [ ] Sau subtask done: mỗi dòng vật tư có nút [chi tiết] expandable, hiện danh sách lô đã xuất FIFO: Tên lô, SL xuất từ lô, giá/stock_unit của lô, trạng thái lô sau xuất (active/depleted)
  • [ ] FIFO split: nếu xuất vượt 2+ lô, hiện 2+ dòng chi tiết. Giá bình quân gia quyền = tổng cost / tổng SL
  • [ ] Chỉ hiện [chi tiết] khi subtask đã done (Phase 2 DEC-D28). Chưa done → hiện "Giá ước tính từ lô FIFO"
  • [ ] Data từ material_stock_movement WHERE source_type='auto_deduct' AND source_reference_id = ptm.id

Edge cases:

  • Admin đổi giá 1 giây trước KTV save → KTV lấy giá ước tính từ lô FIFO hiện tại (race condition accepted)
  • KTV mở form → đợi 30 phút → save → lấy giá ước tính tại thời điểm save (không phải thời điểm mở)
  • Config bị delete (soft) giữa lúc KTV đang edit → save fail, hiển thị "Giá vật tư đã thay đổi, vui lòng thử lại"
  • quantity = 0 → FE block. Nếu bypass → amount = 0 (hợp lệ toán học nhưng vô nghĩa)
  • wastage_quantity > quantity → cho phép (KTV đổ bỏ nhiều hơn dùng), cảnh báo "Hao hụt > SL sử dụng"
  • Giá ước tính (Phase 1) khác giá thực tế (Phase 2) khi lô FIFO đã hết giữa save và done → giá Phase 2 là đúng, Phase 1 chỉ mang tính tham khảo

FR-011: Ghi nhận hao hụt kỹ thuật (Ref: DEC-D03, FORMULA-004, FORMULA-006)

Mô tả: KTV ghi nhận SL hao hụt kỹ thuật (đổ bỏ, dùng thừa) per vật tư per subtask.

AC:

  • [ ] Cột "Hao hụt" (wastage_quantity) trên MaterialForm — optional, mặc định 0
  • [ ] wastage_quantity mặc định ẩn trên form. Admin bật qua Settings > Kho vật tư > Hiển thị hao hụt (Ref: DEC-D20)
  • [ ] wastage_quantity >= 0, cùng ĐVT với quantity
  • [ ] wastage_reason: text tự do, max 200 ký tự (VD: "Đổ bỏ do hết HSD", "Dùng thừa")
  • [ ] total_stock_equivalent = (quantity + wastage_quantity) × to_stock_factor — dùng khi trừ kho
  • [ ] wastage_cost = ROUND(unit_price × wastage_quantity) — FORMULA-006, tracking riêng cho báo cáo
  • [ ] Khi Settings tắt: cột hao hụt ẩn với tất cả roles trên form subtask. wastage_quantity = 0 mặc định.

Edge cases:

  • wastage_quantity = 0total_stock_equivalent = quantity × factor (không hao hụt)
  • wastage_quantity > quantity → cho phép + cảnh báo "Hao hụt > SL sử dụng"
  • is_discrete = truewastage_quantity cũng phải nguyên
  • unit_price = NULL (chưa config) → wastage_cost = NULL
  • Hao hụt trên product chưa config → ghi nhận được, không tính cost

Nhóm 5: Auto-deduct + reverse (FR-012 → FR-013)

FR-012: Auto-deduct khi subtask done (Ref: DEC-D14, DEC-D15, DEC-D22, DEC-D23, DEC-D18)

Mô tả: Khi subtask chuyển sang status done_*, hệ thống tự động trừ tồn kho vật tư theo total_stock_equivalent.

AC:

  • [ ] Event trigger on project_task.status_id changed
  • [ ] Condition: new status contains done_ (VD: done_branch, done_all)
  • [ ] Handler: materialAutoDeduct(taskId)
  • [ ] Cho mỗi project_task_material của subtask:
    • Nếu total_stock_equivalent IS NULL → SKIP (product chưa config)
    • Lấy warehouse từ task.branch_idmaterial_warehouse.branch_id
    • Nếu warehouse không tồn tại → LOG warning, SKIP
    • pg_advisory_xact_lock(hashtext(warehouse_id || product_id)) — serialize per product
    • currentBalance = getLatestBalance(warehouse, product)
    • newBalance = currentBalance - total_stock_equivalent
    • Insert material_stock_movement:
      • quantity_change = -total_stock_equivalent
      • running_balance = newBalance
      • source_type = 'auto_deduct'
      • source_reference_id = material.id
      • source_reference_type = 'project_task_material'
    • Nếu newBalance <= min_stock → trigger low stock notification (FR-015)
  • [ ] Không cho phép tồn kho âm (Ref: DEC-D23). Nếu remaining_qty của lô FIFO không đủ cho BẤT KỲ vật tư nào → block toàn bộ transaction, rollback tất cả movements, subtask KHÔNG chuyển done. Hiển thị lỗi "Không đủ tồn kho [tên vật tư] (cần X, còn Y)". Không partial deduct (không trừ vật tư A rồi skip vật tư B).
  • [ ] Subtask có 5 vật tư → 5 movements trong 1 transaction. Nếu 1 vật tư fail → rollback cả 5.

Edge cases:

  • Subtask done nhưng 0 vật tư → không tạo movement (hợp lệ)
  • Subtask done nhưng warehouse chưa tạo → log warning, không crash, không trừ kho
  • 2 subtask done cùng lúc trừ cùng product → advisory lock serialize, balance chính xác
  • Subtask done → không đủ tồn kho cho 1 trong 5 vật tư → block toàn bộ transaction, rollback tất cả, subtask không chuyển done, hiển thị lỗi "Không đủ tồn kho [tên vật tư] (cần X, còn Y). Vui lòng chuyển kho hoặc nhập thêm." (Ref: DEC-D23)
  • Product chưa config → skip (total_stock_equivalent IS NULL)
  • Network error trong auto-deduct → retry mechanism (max 3 retries)

FR-013: Auto-reverse khi undo done (Ref: DEC-D18)

Mô tả: Khi subtask chuyển từ done_* sang trạng thái khác (undo done, canceled sau done), hệ thống tự động reverse (cộng lại) tồn kho.

AC:

  • [ ] Event trigger on project_task.status_id changed
  • [ ] Condition: old status contains done_ AND new status NOT contains done_
  • [ ] Handler: materialAutoReverse(taskId)
  • [ ] Tìm movements source_type = 'auto_deduct' liên quan đến subtask
  • [ ] Cho mỗi movement:
    • pg_advisory_xact_lock per (warehouse, product)
    • currentBalance = getLatestBalance(warehouse, product)
    • Insert material_stock_movement:
      • quantity_change = +ABS(original_quantity_change)
      • running_balance = currentBalance + ABS(original_quantity_change)
      • source_type = 'auto_reverse'
      • source_reference_id = original_material_id
      • note = 'Reverse: subtask undo/canceled'
  • [ ] Immutable log: KHÔNG delete movement cũ. Tạo movement mới +quantity.
  • [ ] Reverse chỉ xảy ra 1 lần per auto-deduct movement (idempotent check)

Edge cases:

  • Subtask done → undo → done lại → 3 movements: deduct(-X), reverse(+X), deduct(-X) — balance đúng
  • Subtask done → undo → sửa SL → done lại → deduct cũ + reverse + deduct mới (SL mới)
  • Reverse movement không tìm thấy (data inconsistent) → log error, không crash
  • Subtask canceled từ in_progress (chưa done) → không reverse (chưa có auto-deduct)
  • Double-trigger (event fire 2 lần) → idempotent: check "đã có auto_reverse cho movement này chưa"

Nhóm 6: Kiểm kê + cảnh báo (FR-014 → FR-015)

FR-014: Kiểm kê + điều chỉnh (Ref: SCR-07)

Mô tả: Admin/Manager kiểm kê tồn kho thực tế, so sánh với hệ thống, và điều chỉnh chênh lệch.

AC:

  • [ ] Button [Kiểm kê] trên SCR-01 → mở form SCR-07
  • [ ] Form: Kho (readonly), Ngày kiểm kê (readonly = today), bảng [Vật tư | ĐV kho | Hệ thống | Thực tế (input) | Chênh lệch (auto) | Lý do (input)]
  • [ ] "Hệ thống" = running_balance từ material_stock_balance
  • [ ] "Chênh lệch" = Thực tế - Hệ thống, auto-calc
  • [ ] Khi confirm:
    • Dòng có chênh lệch ≠ 0 → tạo material_stock_movement:
      • quantity_change = chênh lệch
      • running_balance = Thực tế
      • source_type = 'adjustment'
      • note = lý do
    • Dòng chênh lệch = 0 → skip
  • [ ] Manager: chỉ kiểm kê branch mình
  • [ ] Sau kiểm kê: running_balance = Thực tế (exactly)
  • [ ] Batch-level adjustment (v3.0): Chênh lệch PHẢI phân bổ về batch cụ thể, không chỉ cập nhật aggregate:
    • Chênh lệch < 0 (thực tế < hệ thống): trừ FIFO từ lô cũ nhất active (ORDER BY created_at ASC). Nếu trừ hết 1 lô → SET status='depleted', tiếp tục lô kế.
    • Chênh lệch > 0 (thực tế > hệ thống): cộng vào lô mới nhất active (ORDER BY created_at DESC). Nếu không có lô active → tạo lô mới (batch_code='ADJ-{timestamp}', purchase_price=0, note='Điều chỉnh kiểm kê').
    • Mỗi batch bị điều chỉnh → 1 material_stock_movement riêng (source_type='adjustment', batch_id, batch_remaining_after)
  • [ ] Audit trail: Kiểm kê tạo 1 nhóm movements cùng source_reference_id = kiểm kê ID. Có thể truy vết "lần kiểm kê này điều chỉnh những lô nào".

Validation:

  • "Thực tế" >= 0 (sau kiểm kê, tồn kho phải >= 0)
  • "Lý do" bắt buộc khi chênh lệch > 10% → "Chênh lệch > 10%, vui lòng nhập lý do"
  • "Lý do" tùy chọn khi chênh lệch ≤ 10%

Edge cases:

  • Tất cả dòng chênh lệch = 0 → "Không có chênh lệch. Không cần điều chỉnh."
  • Kiểm kê khi KTV đang trừ kho → advisory lock đảm bảo không race condition
  • Kiểm kê > 500 vật tư → phân trang trong form kiểm kê (ít xảy ra)
  • Admin kiểm kê 2 lần/ngày → hợp lệ, tạo 2 bộ adjustment movements
  • Chênh lệch lớn (> 50%) → cảnh báo "Chênh lệch bất thường, xác nhận?"

FR-015: Cảnh báo tồn kho thấp (Ref: DEC-D23, DEC-D24)

Mô tả: Khi tồn kho vật tư giảm xuống ≤ min_stock, hệ thống gửi notification cho Manager branch + Admin.

AC:

  • [ ] Trigger: sau auto_deduct hoặc adjustment khi running_balance <= min_stock
  • [ ] Template: "[{branch_name}] {product_name} còn {balance} {stock_unit}, dưới ngưỡng {min_stock}"
  • [ ] Kênh: In-app notification (notification-api)
  • [ ] Người nhận: Manager của branch + Admin toàn hệ thống
  • [ ] Dedupe: 1 notification / product / warehouse / ngày (check before send)
  • [ ] Ví dụ: "[Quận 1] Mask collagen còn 49 miếng, dưới ngưỡng 50"
  • [ ] 4 mức hiển thị trên SCR-01:
    • > min_stock × 2: Xanh (Đủ)
    • > min_stock: Vàng (Sắp hết)
    • ≤ min_stock: Đỏ (Dưới ngưỡng)
    • = 0: Đỏ bold (Hết hàng) — không cho phép âm (Ref: DEC-D23)
  • [ ] min_stock = NULL → không cảnh báo, hiển thị "—" ở cột trạng thái

Edge cases:

  • Tồn kho dao động quanh min_stock (49 → 51 → 48) → dedupe chỉ gửi 1 notification/ngày
  • 10 products đồng thời dưới ngưỡng → 10 notifications riêng biệt
  • Admin chưa set min_stock → không trigger notification cho product đó
  • Balance = 0 → gửi notification "Hết hàng", block deduct cho đến khi nhập thêm (Ref: DEC-D23)
  • Notification-api unavailable → log error, retry 3 lần, queue cho lần sau

Nhóm 7: Kết nối tổng hợp tài chính (FR-016)

FR-016: Chi phí vật tư trong sidebar tài chính đơn hàng (Ref: FORMULA-005)

Mô tả: Hiển thị tổng chi phí vật tư trong sidebar tổng hợp tài chính đơn hàng dịch vụ.

AC:

  • [ ] Sidebar tài chính đơn hàng thêm dòng "Chi phí vật tư" (material_cost)
  • [ ] Công thức:
    material_cost = SUM(ptm.amount)
      WHERE ptm.task_id IN (
        SELECT id FROM project_task
        WHERE parent_id IN (
          SELECT id FROM project_task WHERE order_id = $order_id
        )
      )
      AND ptm.amount IS NOT NULL
      AND subtask.status_id NOT LIKE 'canceled%'
  • [ ] Subtask canceled → exclude khỏi SUM (Ref: DEC-D19)
  • [ ] Tổng chi phí đơn hàng cập nhật: total_cost = tour_money + material_cost + commission + fixed_cost
  • [ ] Backward compatibility:
    • Đơn mới (có project_task_material.amount): dùng FORMULA-005
    • Đơn cũ (có inventory_document.capture): dùng SUM(capture.Price × capture.Quantity)
    • Không có data: material_cost = 0
  • [ ] Quyền xem: Admin (all) + Manager (branch mình) (Ref: DEC-D21)
  • [ ] Realtime: cập nhật khi subtask save (vật tư mới/sửa SL)

Edge cases:

  • Đơn hàng có cả subtask cũ (không có giá) + subtask mới (có giá) → cộng dồn cả 2 nguồn
  • Tất cả vật tư chưa config giá → material_cost = 0 (tất cả amount = NULL)
  • Đơn hàng 0 subtask → material_cost = 0
  • Đơn hàng chỉ có subtask canceled → material_cost = 0 (tất cả excluded, Ref: DEC-D19)
  • Undo subtask done → amount giữ nguyên (amount = chi phí ghi nhận, không liên quan trừ kho)

Nhóm 8: Import cấu hình (FR-017)

FR-017: Import Excel cấu hình giá + đơn vị quy đổi

Mô tả: Admin import file Excel để cấu hình giá và đơn vị quy đổi hàng loạt cho nhiều vật tư cùng lúc.

AC:

  • [ ] Button [Import cấu hình] trên SCR-01 → upload file .xlsx/.csv
  • [ ] Template Excel: [SKU | Đơn vị kho | Giá nhập | SL/đơn vị mua | Hao hụt % | Ngưỡng cảnh báo | ĐVT1 | Factor1 | ĐVT2 | Factor2] — có link download template mẫu
  • [ ] Validate per dòng:
    • SKU không tồn tại trong product → lỗi dòng đó
    • Giá nhập < 0 → lỗi dòng đó
    • SL/đơn vị mua ≤ 0 → lỗi dòng đó
    • Hao hụt % < 0 hoặc ≥ 100 → lỗi dòng đó
    • Factor ≤ 0 → lỗi dòng đó
  • [ ] Hiển thị preview: dòng hợp lệ (xanh) + dòng lỗi (đỏ + lý do)
  • [ ] Admin confirm → import dòng hợp lệ: tạo material_price_config + material_usage_unit per dòng
  • [ ] SKU đã có config active → tạo version mới (close config cũ, Ref: DEC-D02)
  • [ ] Max 500 dòng/file. > 500 → "File quá lớn, vui lòng chia nhỏ."
  • [ ] Xử lý đồng bộ (< 500 dòng → < 10 giây)

Edge cases:

  • Cột ĐVT2/Factor2 trống → chỉ tạo 1 usage unit (ĐVT1)
  • SKU trùng trong 1 file → lỗi "SKU [X] xuất hiện nhiều lần, chỉ giữ dòng đầu"
  • File rỗng → "File không có dữ liệu"
  • File sai format → "Chỉ hỗ trợ file .xlsx hoặc .csv"
  • Import thành công 480/500 dòng → hiển thị "Đã import 480 dòng thành công. 20 dòng lỗi." + link download lỗi

Nhóm 9: Quản lý lô hàng + FIFO (FR-018 → FR-021) — NEW v2.0

FR-018: Quản lý lô hàng (batch) (Ref: DEC-D22, DEC-D25)

Mô tả: Mỗi lần nhập kho tạo 1 lô (batch) mới với giá nhập riêng, hạn sử dụng (HSD), mã lô nhà cung cấp. Trạng thái lô theo lifecycle.

AC:

  • [ ] Mỗi material_stock_movement với source_type IN ('stock_in', 'import', 'transfer') tạo 1 record material_batch
  • [ ] Thông tin lô: batch_code (mã lô NCC, optional), purchase_price (giá nhập per stock unit), expiry_date (HSD, optional), initial_qty, remaining_qty
  • [ ] Trạng thái lô (status):
    • active: còn hàng, đang sử dụng (remaining_qty > 0 AND chưa hết hạn AND không bị hủy)
    • depleted: đã dùng hết (remaining_qty = 0)
    • locked: hết hạn sử dụng, auto-lock bởi scheduler (Ref: DEC-D24). Không cho xuất.
    • disposed: đã hủy (lỗi, thu hồi). Ref: DEC-D26
  • [ ] Nhập cùng giá cùng ngày vẫn tách lô riêng (Ref: DEC-D25) — vì có thể khác mã lô NCC, khác HSD
  • [ ] remaining_qty >= 0 (CHECK constraint, Ref: DEC-D23)
  • [ ] SCR-03 (Chi tiết vật tư): thêm tab "Danh sách lô" hiển thị [Mã lô | Giá nhập | HSD | SL ban đầu | SL còn | Trạng thái]
  • [ ] Sort lô: created_at ASC (lô cũ nhất trên cùng — FIFO order)

Validation:

  • initial_qty > 0 (nhập kho luôn dương)
  • remaining_qty >= 0 (CHECK constraint)
  • purchase_price >= 0 (cho phép 0 = vật tư miễn phí)
  • expiry_date nếu có phải > ngày nhập (created_at)

Edge cases:

  • Nhập 2 lần cùng ngày cùng giá → 2 lô riêng biệt, FIFO theo thứ tự nhập (Ref: DEC-D25)
  • Lô không có HSD (expiry_date = NULL) → không bao giờ auto-lock, trạng thái chỉ chuyển active → depleted
  • Lô depleted → không cho xuất thêm. Nếu reverse (undo subtask) → remaining_qty tăng lại, status chuyển active
  • Import Excel: mỗi dòng = 1 lô mới (có thể thêm cột batch_code, expiry_date vào template)
  • Lô có remaining_qty = 0.0001 (gần hết) → vẫn active, FIFO sẽ dùng trước

FR-019: Hạn sử dụng (HSD) — cảnh báo + auto-lock (Ref: DEC-D24)

Mô tả: Cảnh báo Manager 3 tháng trước HSD. Scheduler auto-lock lô hết hạn, không cho xuất.

AC:

  • [ ] Scheduler chạy hàng ngày (00:00 UTC+7): quét tất cả lô status = 'active'expiry_date
  • [ ] Lô hết hạn (expiry_date < today) → auto update status = 'locked', ghi locked_reason = 'expired'
  • [ ] Lô sắp hết hạn (≤ 90 ngày) → gửi notification cho Manager branch + Admin:
    • Template: "[{branch_name}] Lô {batch_code} — {product_name} hết hạn ngày {expiry_date} (còn {days_left} ngày)"
    • Dedupe: 1 notification / lô / tuần (không spam hàng ngày)
  • [ ] Lô locked → không tham gia FIFO xuất kho. Auto-deduct bỏ qua lô locked.
  • [ ] SCR-01: badge "Sắp hết HSD" (vàng) nếu có lô sắp hết hạn ≤ 90 ngày. Badge "Hết HSD" (đỏ) nếu có lô locked.
  • [ ] SCR-03 tab "Danh sách lô": cột HSD hiển thị đỏ nếu ≤ 90 ngày hoặc đã hết hạn

Edge cases:

  • Lô không có HSD → scheduler bỏ qua
  • Lô locked (hết hạn) nhưng còn hàng (remaining_qty > 0) → Manager có thể hủy lô (FR-021) hoặc Admin unlock thủ công nếu HSD gia hạn
  • Lô sắp hết hạn nhưng đã depleted → không cảnh báo (không còn hàng)
  • 10 lô sắp hết hạn cùng lúc → 10 notifications riêng biệt (mỗi lô 1 notification)
  • Admin sửa expiry_date thành ngày xa hơn → nếu lô đang locked (expired) → chuyển lại active (nếu remaining_qty > 0)

FR-020: FIFO xuất kho (Ref: DEC-D22, DEC-D27, DEC-D23)

Mô tả: Auto-deduct khi subtask done: lấy từ lô cũ nhất (FIFO). Nếu 1 lần xuất vượt 2+ lô → tách nhiều movements, mỗi movement gắn 1 lô. Giá snapshot = bình quân gia quyền.

AC:

  • [ ] Khi auto-deduct (FR-012), thay vì trừ trực tiếp running_balance, áp dụng FIFO:
    1. Lấy danh sách lô status = 'active' AND remaining_qty > 0 cho (warehouse, product), ORDER BY created_at ASC (lô cũ nhất trước)
    2. Duyệt từng lô theo thứ tự FIFO:
      • Nếu remaining_qty >= qty_needed → trừ hết từ lô này. Tạo 1 movement gắn batch_id. Done.
      • Nếu remaining_qty < qty_needed → trừ hết lô này (remaining_qty → 0, status → depleted). qty_needed -= remaining_qty. Tạo movement gắn lô này. Tiếp lô sau. (FIFO split, Ref: DEC-D27)
    3. Nếu hết lô mà qty_needed > 0block deduct, rollback transaction, hiển thị lỗi "Không đủ tồn kho" (Ref: DEC-D23)
  • [ ] Mỗi movement lưu batch_id → truy vết giá theo lô
  • [ ] Giá snapshot cho subtask: bình quân gia quyền (weighted average) khi FIFO split:
    snapshot_price = SUM(qty_from_batch_i × batch_i.purchase_price) / total_qty
  • [ ] amount = ROUND(snapshot_price × total_qty) — BIGINT
  • [ ] Subtask project_task_material lưu: unit_price = snapshot_price (bình quân gia quyền nếu split)

Validation:

  • Tổng remaining_qty tất cả lô active < total_stock_equivalent → block deduct
  • Mỗi movement: quantity_change <= batch.remaining_qty (không được trừ quá lô)

Edge cases:

  • Xuất 10 đơn vị, lô A còn 3, lô B còn 20 → 2 movements: M1 (lô A, -3), M2 (lô B, -7). Lô A → depleted.
  • Xuất vượt 3 lô liên tiếp → 3 movements. Hiếm nhưng hợp lệ.
  • Lô cũ nhất bị locked (hết HSD) → bỏ qua, lấy lô active tiếp theo
  • Tất cả lô còn hàng bị locked → block deduct "Tất cả lô đã hết hạn hoặc bị khóa"
  • Concurrent deduct: pg_advisory_xact_lock per (warehouse, product) serialize — FIFO order nhất quán
  • Reverse (FR-013): cộng lại remaining_qty cho từng lô đã trừ (theo batch_id trong movement). Lô depleted → chuyển lại active.

FR-021: Hủy lô — disposal (Ref: DEC-D26)

Mô tả: Manager/Admin hủy lô lỗi, thu hồi, hoặc hết hạn. Tạo disposal movement + ghi chú lý do. Giữ audit trail.

AC:

  • [ ] Button [Hủy lô] trên SCR-03 tab "Danh sách lô" → mở dialog confirm
  • [ ] Dialog: Lô [mã lô] — [tên vật tư] — SL còn: [remaining_qty] [stock_unit]
  • [ ] Field bắt buộc: disposal_reason (dropdown + text tự do):
    • Dropdown: "Hết hạn sử dụng" | "Lỗi sản phẩm" | "Thu hồi NCC" | "Hư hỏng" | "Khác"
    • Text tự do: max 500 ký tự (bắt buộc khi chọn "Khác")
  • [ ] Khi confirm:
    • Update batch status = 'disposed', disposed_at = now(), disposed_by = current_user
    • Tạo material_stock_movement:
      • quantity_change = -remaining_qty (trừ hết SL còn lại)
      • source_type = 'disposal'
      • batch_id = batch.id
      • note = disposal_reason
    • Update batch remaining_qty = 0
  • [ ] Quyền: Manager (branch mình) + Admin (tất cả)
  • [ ] Sau hủy: lô hiển thị với badge "Đã hủy" (xám), không cho xuất, không tham gia FIFO

Edge cases:

  • Hủy lô đã depleted (remaining_qty = 0) → cho phép (chỉ đổi status, không tạo movement vì qty = 0)
  • Hủy lô đang được FIFO deduct (concurrent) → advisory lock serialize, hủy chờ deduct xong
  • Manager cố hủy lô branch khác → block (permission check)
  • Admin hủy lô → ghi disposed_by = admin_user_id cho audit
  • Undo hủy lô → không hỗ trợ (disposal là irreversible). Nếu cần → nhập lô mới.
  • Hủy lô có HSD xa (chưa hết hạn) → cho phép + confirm "Lô chưa hết hạn, bạn chắc chắn hủy?"

A6) Assumptions

IDAssumptionImpact nếu saiOwner xác nhận
ASM-01Trung bình 3-5 vật tư/subtask, 5K-30K subtask/ngày toàn hệ thốngScale material_stock_movement: 15K-150K movements/ngày. Cần partition sớm hơn nếu > 50K.PO + Tech Lead
ASM-02Supplier đổi giá 1-2 lần/năm → Batch-based FIFO theo giá từng lô nhậpNếu giá đổi hàng tuần → FIFO split nhiều lô, chi phí tính toán phức tạp hơn.PO
ASM-031 chi nhánh trung bình 30-100 loại vật tư> 500 vật tư → cần tối ưu search/filter trên SCR-01.PO
ASM-04Kho chính chưa chuẩn hóa, cần 3-6 tháng trước khi kết nốiv3.0: Kết nối ngay với dual-input (DEC-D29, DEC-D30). Data kho chính chưa chuẩn ở một số chi nhánh → dùng nhập tay fallback. Song song dọn data (DEC-D35).PO + Admin
ASM-05Manager tin tưởng số liệu nhập tay ban đầu (kiểm kê thực tế trước nhập)Nếu nhập sai → chênh lệch từ ngày 1 → giảm tin tưởng. Mitigation: kiểm kê sau 1 tuần.Manager
ASM-06Product hiếm khi đổi tên/SKU → denormalize product_name/sku chấp nhận đượcNếu đổi nhiều → stale data hiển thị → cần sync job.PO + Tech Lead
ASM-07Concurrent editing kiểm kê (last-write-wins) chấp nhận cho Phase 1Nếu 2 Manager kiểm kê cùng lúc → data sai. Mitigation: lock form kiểm kê per warehouse.PO

A7) Risks

IDRiskLikelihoodImpactSeverityMitigation
RSK-01material_stock_movement grow nhanh (15K-150K records/ngày)HighMediumHighIndexes đã plan. Partition by created_at monthly khi > 5M records (Phase 2).
RSK-02Running balance sai nếu advisory lock failLowHighMediumRetry mechanism + audit: SUM(quantity_change) = running_balance check hàng đêm.
RSK-03Admin setup sai giá/hệ số → chi phí sai hàng loạtMediumHighHighConfirm dialog "Giá mới ≠ giá cũ > 20%, xác nhận?". Versioning cho rollback.
RSK-04Import Excel sai data → tồn kho sai từ ngày 1MediumHighHighPreview trước import. Kiểm kê sau 1 tuần sử dụng.
RSK-05Event trigger auto-deduct fail → tồn kho không trừLowHighMediumDead letter queue + retry 3 lần. Alert Admin nếu fail 3 lần.
RSK-06KTV ghi nhận sai SL → chi phí saiMediumMediumMediumSupervisor review (Manager xem aggregate). Phase 2: so sánh dự kiến vs thực tế.
RSK-07Performance: SCR-01 chậm khi > 500 vật tưLowMediumLowPhân trang + indexes. Test load 500 vật tư < 2 giây.
RSK-08Không đủ tồn kho block subtask done → gián đoạn KTVMediumMediumMediumCảnh báo tồn kho thấp sớm (FR-015). Manager nhập kho kịp thời. UI hiển thị rõ lý do block. (Ref: DEC-D23)
RSK-09FIFO split nhiều lô → performance chậm nếu nhiều lô nhỏLowLowLowTối ưu query: index (warehouse_id, product_id, status, created_at). Thực tế spa ít khi > 10 lô/product.
RSK-10HSD auto-lock lô đang dùng dở → KTV bất ngờ bị blockMediumMediumMediumCảnh báo 3 tháng trước HSD (FR-019). Manager chủ động hủy/dùng hết lô sắp hết hạn.

A8) Success Metrics

MetricBaseline (hiện tại)Target (sau 1 tháng)Target (sau 3 tháng)Cách đo
% subtask có giá vật tư (unit_price NOT NULL)0%> 80% (sau config)> 95%SQL: COUNT(unit_price IS NOT NULL) / COUNT(*)
Chênh lệch tồn kho (hệ thống vs kiểm kê)N/A< 5%< 2%ABS(system - actual) / actual × 100 trung bình per product. Đo được khi Manager kiểm kê. Backlog Phase 2: nhắc nhở kiểm kê định kỳ (PH2-10).
% đơn hàng có chi phí vật tư trong sidebar0%> 70%> 90%SQL: COUNT(material_cost > 0) / COUNT(orders_with_materials)
Thời gian từ tồn thấp → notificationN/A< 5 phút< 1 phútNotification log: created_at - movement.created_at
Số incident "tồn kho sai"N/A< 3/tháng< 1/thángJira/feedback tracking
Thời gian Admin setup 1 branch (50 products)N/A< 2 giờ< 1 giờManual tracking

A9) Glossary — Thuật ngữ

Thuật ngữ (VI)Thuật ngữ (EN)Định nghĩaPhân biệt với
Kho vật tưMaterial Warehouse / BackbarKho chứa vật tư dùng cho dịch vụ, tồn kho theo đơn vị sử dụng (ml, g, miếng). Mỗi chi nhánh 1 kho. Nhận hàng từ kho chi nhánh qua chuyển kho (v3.0, DEC-D29).≠ Kho chi nhánh (kho chính, đơn vị mua: chai/hộp). Kết nối qua phiếu chuyển kho.
Đơn vị kho (vật tư)Stock UnitĐơn vị tính tồn kho trong kho vật tư (ml, g, miếng)≠ Đơn vị mua (chai, hộp, tube)
Đơn vị sử dụngUsage UnitĐơn vị KTV dùng khi làm dịch vụ (giọt, muỗng, miếng)≠ Đơn vị kho (có thể khác, VD: giọt vs ml)
Hệ số quy đổiConversion Factor (to_stock_factor)1 usage unit = X stock units. VD: 1 giọt = 0.05mlChiều: usage → stock
Giá đơn vị khoStock Unit PriceGiá per đơn vị kho, đã tính hao hụt hệ thống. = source_price / (source_quantity × (1 - wastage_rate))≠ Giá nhập (source_price)
Giá đơn vị sử dụngUsage Unit PriceGiá per ĐVT sử dụng. = stock_unit_price × to_stock_factor≠ Giá đơn vị kho
Hao hụt hệ thốngSystem Wastage Rate% vật tư mất do bay hơi, đáy chai, etc. Tính vào đơn giá. Config per product.≠ Hao hụt kỹ thuật (KTV đổ bỏ, ghi per subtask)
Hao hụt kỹ thuậtTechnical WastageSL vật tư hao hụt do KTV (đổ bỏ, dùng thừa). Ghi trên subtask, tính vào trừ kho.≠ Hao hụt hệ thống (% config)
Snapshot giáPrice SnapshotGiá + hệ số quy đổi copy vào subtask tại thời điểm thêm vật tư. Đơn cũ giữ giá cũ dù admin đổi giá.≠ Giá hiện tại (latest price)
Running balanceTồn kho lũy kếSố dư tồn kho sau mỗi giao dịch (movement). Lưu trực tiếp trên movement → query O(1).≠ Computed balance (SUM movements, O(n))
Auto-deductTự động trừ khoHệ thống trừ tồn kho vật tư khi subtask chuyển status → done_*.≠ Manual deduct (phase sau)
Auto-reverseTự động hoàn khoHệ thống cộng lại tồn kho khi subtask undo done hoặc canceled sau done.≠ Auto-deduct
Latest PriceGiá nhập gần nhấtPhương pháp tính giá vốn: luôn dùng config active hiện tại. Superseded by DEC-D22 — chuyển sang Batch-based FIFO.≠ FIFO (đã áp dụng), ≠ Weighted Average
Kiểm kêStock Check / Inventory CountAdmin/Manager đếm thực tế, so sánh với hệ thống, điều chỉnh chênh lệch.≠ Nhập kho (thêm hàng mới)
MovementGiao dịch kho1 record nhập/xuất/điều chỉnh trong material_stock_movement. Immutable (không sửa, không xóa).≠ Stock balance (tồn kho hiện tại)
Config activeCấu hình đang hoạt độngmaterial_price_configeffective_to IS NULL AND deleted_at IS NULL≠ Config cũ (đã có effective_to)
Sidebar tài chínhFinancial Summary SidebarPanel bên phải trong chi tiết đơn hàng, hiển thị tổng hợp tài chính: tour, vật tư, commission, etc.
Lô (Batch)BatchMỗi lần nhập kho = 1 lô riêng, có giá nhập + HSD riêng. Record trong material_batch.≠ Stock movement (log giao dịch)
FIFOFIFO (First In First Out)Xuất lô cũ nhất (nhập trước) trước. Áp dụng cho auto-deduct khi subtask done.≠ LIFO, ≠ WAC
HSDExpiry DateHạn sử dụng của lô hàng. Cảnh báo 3 tháng trước, auto-lock khi hết hạn.≠ Ngày nhập kho
Hủy lôDisposalHủy lô lỗi/thu hồi/hết hạn, chuyển status disposed. Tạo disposal movement. Irreversible.≠ Xuất kho (deduct)
FIFO SplitFIFO Split1 lần dùng vượt qua 2+ lô, tách thành nhiều giao dịch (movements). Mỗi giao dịch gắn 1 lô riêng.Giá snapshot = bình quân gia quyền

Non-goals (Explicit)

#Non-goalLý do loạiPhase dự kiến
NG-01Kết nối kho chính (phiếu chuyển kho)Moved to Phase 1 scope (v3.0, DEC-D29). Chuyển kho chi nhánh → kho vật tư đã nằm trong FR.Phase 2 Done
NG-02FIFO / Weighted Average costingĐã implement Batch-based FIFO (v2.0, DEC-D22). Weighted Average vẫn là non-goal.FIFO đã vào scope. WAC nếu cần sau.Phase 2 Done
NG-03Override giá per branch (UI form)Chưa có nhu cầu thực tế. Field branch_id sẵn. Ref: DEC-D09Phase 2
NG-04Material recipe / BOM (pha trộn)Spa chưa có nghiệp vụ pha trộn phức tạpPhase 3
NG-05Chuyển kho vật tư giữa chi nhánhMulti-branch operation chưa cầnPhase 2
NG-06Báo cáo chi phí vật tư (report module)Cần data 1-2 tháng trước khi có ý nghĩaPhase 2
NG-07Export báo cáo vật tư (Excel)Phụ thuộc NG-06Phase 2
NG-08Mobile supportƯu tiên desktop trướcPhase 3
NG-09Audit log cho material config changesPhase 1 không yêu cầuPhase 2
NG-10Optimistic locking cho concurrent editingLast-write-wins chấp nhận Phase 1Phase 2

Changelog

VersionNgàyThay đổiTác giả
1.02026-03-26Initial PRD — 18 DECs, 16 FRs, 6 Formulas, 13 Business Rules, 15+ Edge Cases. Dựa trên design doc v1.1.PO/BA + AI
1.12026-03-27Fix review: +DEC-D19~D21, +FR-017 (import config Excel), sửa FR-004 (confirm global price), FR-008 (batch_import_id), FR-011 (wastage mặc định ẩn), FR-016 (exclude canceled + Manager quyền xem), sync milestones T+20d, +PH2-10/PH2-11.PO/BA
2.02026-03-27Chuyển sang Batch-based FIFO (DEC-D22~D27). +FR-018~FR-021 (batch, HSD, FIFO xuất kho, hủy lô). Supersede DEC-D04 (Latest Price → FIFO), DEC-D17 (tồn kho âm → block). Sửa FR liên quan. Cập nhật Glossary, Risks, Non-goals.PO/BA
2.12026-03-27Post-review fixes: +DEC-D28 (Two-Phase Pricing: save=ước tính, done=final FIFO). Sửa FR-010 (Two-Phase Pricing rewrite). FR-007 thêm batch fields (Giá nhập, HSD, Mã lô) + source_type='stock_in'. FR-008 Excel template thêm cột batch. FR-001 chọn auto-create. Bỏ trạng thái tồn kho < 0 (FR-002).PO/BA
3.02026-03-27Major: Kết nối kho chính. Supersede DEC-D12 → DEC-D29 (kết nối kho chi nhánh). +DEC-D29→D36 (8 decisions: transfer flow, unit conversion, no approval, dual-input, data cleanup, lot selection). Kiến trúc 4 cấp: NCC→Kho tổng→Kho chi nhánh→Kho vật tư. +FR-022 (chuyển kho, Nhóm 3a). PH2-01/NG-01→scope. ASM-04 superseded. Milestones +M5a/M6a. Quy trình 5→6 bước. Executive Summary rewrite. Fix DEC-D05 (Two-Phase Pricing), effort 23d→33d, RACI/AI-04 số bảng, Phase 2 roadmap, FR-018 source_type.PO/BA

Hướng dẫn đọc (RACI-based)

Bạn là...Đọc gìBỏ qua
PO / Manager / SếpExecutive Summary → A2 Context (10 mục) → A5 FRs (overview) → Milestones → Backlog Phase saudev-spec.md (C sections), qa-test-plan.md
Frontend DevZ Decision Log → A5 FRs (chi tiết AC + edge cases) → ui-spec.md (SCR-01→SCR-07, wireframes, permission, state matrix) → dev-spec.md (C4 Data Model, C5 API, C11 Tasks)qa-test-plan.md
Backend DevZ Decision Log (Z3 Technical) → A5 FRs (FR-012, FR-013 auto-deduct/reverse) → dev-spec.md (C4 Data Model, C5 API, C6 Event Triggers, C7 Migration)ui-spec.md (B sections)
QAZ Decision Log (Z4) → A5 FRs (tất cả AC + edge cases) → A2 Ví dụ thực tế → qa-test-plan.mddev-spec.md (C4-C7)
UI/UX DesignerA2 Context (giao diện mô phỏng) → ui-spec.md (B1-B9, ASCII wireframes, permission matrix, state matrix, copy text)dev-spec.md, qa-test-plan.md
Sếp / StakeholderExecutive Summary only (3 bullet points, đủ hiểu feature)Tất cả sections khác