---
date: 2026-05-26
type: epic
tags: [epic, product, variant, ban-hang, mes, product-structure]
epic_id: EPIC-006
title: Product Variant — Cấu hình thuộc tính sản phẩm theo đơn hàng
status: planning
---

# EPIC-006 — Product Variant (Cấu hình thuộc tính sản phẩm theo đơn hàng)

## Problem Statement

Khi sale tiếp nhận yêu cầu từ khách hàng về tùy chỉnh sản phẩm (hiệu ứng bề mặt, tĩnh điện, dầu màu, vật liệu đan...), hiện tại **không có nơi nào trong hệ thống** để ghi nhận các lựa chọn này theo cấu trúc. Thông tin trao đổi qua chat, email, giấy tờ → dễ thất lạc, sai sót, không trace được khi sản xuất.

Ngoài ra, mỗi **loại component** (khung sắt, nệm, phụ kiện...) có bộ thuộc tính khác nhau và thay đổi khi công ty mở rộng sang dây chuyền mới (VD: thêm nhà máy nệm). Hệ thống cần cơ chế **cấu hình linh hoạt** thay vì hardcode fields.

**Người dùng bị ảnh hưởng:** Sale + Kỹ thuật (người thu thập yêu cầu khách), Sản xuất (người thực hiện theo spec), Quản lý sản xuất (người duyệt và theo dõi).

## Business Value

| Lợi ích | Đo lường |
|---------|----------|
| Tập trung spec sản phẩm vào ERP | 100% OrderLine có Variant trước khi xác nhận đơn |
| Giảm sai sót do truyền thông tin ngoài hệ thống | Số lần sản xuất phải làm lại do sai spec giảm về 0 |
| Tái sử dụng cấu hình: Variant tạo 1 lần, dùng nhiều đơn | Tiết kiệm thời gian tạo đơn cho sản phẩm cùng cấu hình |
| Nền tảng để mở rộng thuộc tính trong tương lai | AttributeType có thể thêm mới mà không cần thay đổi schema |

## Target User

| Người dùng | Vai trò | Nhu cầu |
|-----------|---------|---------|
| Nhân viên Sale | Thu thập yêu cầu khách, tạo Variant | Tạo Variant từ ProductStructure, điền thuộc tính theo từng cụm |
| Kỹ thuật viên | Xác nhận thông số kỹ thuật | Xem/edit Variant, validate thông số trước khi đưa vào đơn |
| Sản xuất / Tổ trưởng | Đọc spec để sản xuất | Xem Variant của từng OrderLine |
| Quản lý sản xuất | Duyệt đơn, theo dõi | Tra cứu Variant, tái sử dụng khi đặt lại |

## Scope

### In Scope

- Entity **ComponentType** (mới) — loại component (VD: "Khung sắt X", "Nệm"), mỗi type định nghĩa bộ AttributeType áp dụng và field nào bắt buộc
- Extend bảng **Component** hiện có — thêm FK `componentTypeId`
- Entity **AttributeType** — 4 loại ban đầu (seeded cho ComponentType "Khung sắt"), extensible
- Config screen: **ComponentType → AttributeType** — admin định nghĩa "loại X cần những fields nào"
- Entity **Variant** — standalone, tạo trước khi có đơn hàng, tái sử dụng được qua nhiều OrderLine
- Entity **VariantComponentAttribute** — giá trị attribute cho từng level-1 component trong Variant
- **Tạo Variant flow**: chọn Product → chọn ProductStructure → render level-1 components → mỗi component hiển thị fields theo `componentType` → điền giá trị → Save
- CRUD Variant: tạo mới, xem, chỉnh sửa, danh sách + search theo tên
- Input types: SELECT (single), TEXT
- Liên kết **OrderLine → Variant** (chọn Variant khi tạo/sửa OrderLine)
- Validate bắt buộc: không save được Variant nếu thiếu attribute bắt buộc của bất kỳ component nào
- API: CRUD Variant, CRUD ComponentType, config ComponentType → AttributeType

### Out of Scope

- **Phụ kiện (Hardware)** — xi đen/xi trắng/mạ kẽm/inox (epic riêng)
- **Cách đóng gói** — 1A/3A/6A (epic riêng)
- **Tem nhãn** — thương hiệu / tiêu chuẩn thị trường (epic riêng)
- **Logo** — file upload layout khách hàng (epic riêng)
- **Chứng chỉ** — FSC/CARB/Prop 65/REACH (epic riêng)
- **Nệm (Cushion)** — sẽ theo pattern EPIC-005 (packaging material), epic riêng
- Sub-fields của Vật liệu đan (màu dây, kích thước, thời tiết resistance tách riêng) — MVP dùng TEXT
- AttributeOption management UI (seeded qua migration, thêm qua admin hoặc migration)
- Variant versioning / lịch sử thay đổi
- Import Variant từ Excel
- Báo cáo / thống kê theo thuộc tính (VD: bao nhiêu đơn chọn RAL 9016)
- Mobile app (app-kingston)
- Variant approval workflow

## Data Model

```
ComponentType (mới)
  ├── id, name (VD: "Khung sắt X")
  └── ComponentTypeAttribute[]
        ├── attributeTypeId
        └── isRequired

Component (extend)
  └── + componentTypeId (FK → ComponentType)

AttributeType (mới, seeded)
  ├── id, name, inputType (SELECT | TEXT)
  └── AttributeTypeOption[] (cho SELECT)

Variant (mới)
  ├── id, name, productId, productStructureId
  └── VariantComponentAttribute[]
        ├── componentId (level 1)
        ├── attributeTypeId
        └── value

OrderLine (extend)
  └── + variantId (FK → Variant, nullable)
```

## Attribute Types seed — ComponentType "Khung sắt"

| # | AttributeType | Input Type | Ghi chú |
|---|--------------|-----------|---------|
| 1 | Hiệu ứng bề mặt | SELECT | Options: Cào xước, Ép cước, Giả cổ... |
| 2 | Tĩnh điện | TEXT | Mã màu (VD: RAL 9016) |
| 3 | Dầu màu | TEXT | Loại hóa chất / tên màu |
| 4 | Vật liệu đan | TEXT | Màu + kích thước + chịu thời tiết (VD: "Đen, 3mm, 1000hr") |

> Khi thêm ComponentType mới (VD: "Nệm") → admin tự config AttributeType tương ứng qua config screen, không cần code.

## User Stories

| ID | Story | Acceptance Criteria (summary) | Priority |
|----|-------|-------------------------------|----------|
| US-01 | Là admin, tôi muốn tạo ComponentType và config các AttributeType đi kèm để hệ thống tự động render đúng form khi build Variant | Tạo ComponentType, gắn AttributeType (có đánh dấu bắt buộc/không); khi thêm loại mới không cần code | MUST |
| US-02 | Là sale/kỹ thuật, tôi muốn tạo Variant bằng cách chọn Product + ProductStructure → điền thuộc tính từng component | Form render đúng fields theo componentType của từng level-1 component; attribute bắt buộc được validate | MUST |
| US-03 | Là sale/kỹ thuật, tôi muốn chỉnh sửa Variant trước khi gắn vào đơn | Edit được giá trị các attribute; lưu thành công | MUST |
| US-04 | Là sale, tôi muốn tái sử dụng Variant khi lập OrderLine để tránh nhập lại | Khi tạo/sửa OrderLine có thể search và chọn Variant đã có | MUST |
| US-05 | Là sale/kỹ thuật, tôi muốn xem danh sách Variant để tra cứu và tái sử dụng | Danh sách Variant hiển thị tên, Product, ProductStructure, ngày tạo; có search theo tên | MUST |
| US-06 | Là sản xuất, tôi muốn xem Variant của một OrderLine để biết cần sản xuất theo spec nào | Trong màn hình OrderLine detail, hiển thị Variant attributes nhóm theo component (read-only) | MUST |

## Acceptance Criteria (chi tiết)

### EPIC-006-AC01 — Config ComponentType → AttributeType (admin)
**Given** admin mở trang cấu hình ComponentType  
**When** tạo ComponentType mới (VD: "Khung sắt X"), gắn AttributeType + đánh dấu required/optional, nhấn Lưu  
**Then** ComponentType được tạo; các Variant tạo sau đó với component thuộc loại này sẽ tự render đúng fields

**Error cases:**
- Tên ComponentType trùng → lỗi "Tên loại đã tồn tại"
- Không gắn bất kỳ AttributeType nào → lỗi "Loại component phải có ít nhất 1 thuộc tính"

### EPIC-006-AC02 — Tạo Variant
**Given** sale/kỹ thuật mở trang Variant Management, nhấn "Tạo Variant mới"  
**When** nhập tên Variant → chọn Product → chọn ProductStructure → nhấn Tiếp tục  
**Then** form hiện danh sách level-1 components của ProductStructure; mỗi component render đúng fields theo `componentType`; attribute bắt buộc được đánh dấu (*); component chưa có `componentType` hiển thị thông báo "Chưa cấu hình loại"

**Error cases:**
- Không nhập tên → lỗi "Vui lòng nhập tên Variant"
- Không chọn Product hoặc ProductStructure → lỗi tương ứng

### EPIC-006-AC03 — Điền và lưu thuộc tính
**Given** form Variant đang hiển thị các component và AttributeType tương ứng  
**When** điền đầy đủ các attribute bắt buộc, nhấn Lưu  
**Then** Variant được tạo thành công; redirect về Variant detail hoặc list

**Error cases:**
- Attribute bắt buộc chưa điền → lỗi inline "Trường này là bắt buộc"

### EPIC-006-AC04 — Chỉnh sửa Variant
**Given** user đang xem Variant detail  
**When** nhấn Edit, sửa một hoặc nhiều attribute, nhấn Lưu  
**Then** dữ liệu được cập nhật, hiển thị giá trị mới

### EPIC-006-AC05 — Xem danh sách Variant
**Given** user mở trang Variant Management  
**When** trang load  
**Then** hiển thị danh sách với columns: Tên Variant, Sản phẩm, Kết cấu (ProductStructure), Ngày tạo, Người tạo  
**Empty state:** "Chưa có Variant nào. Nhấn 'Tạo Variant mới' để bắt đầu."  
**Search:** filter theo tên Variant (realtime hoặc submit)

### EPIC-006-AC06 — Gắn Variant vào OrderLine
**Given** user đang tạo/sửa một OrderLine  
**When** chọn sản phẩm có ProductStructure, trường "Variant" xuất hiện  
**Then** user có thể search và chọn Variant (lọc theo ProductStructure tương ứng); Variant đã chọn hiển thị tên + preview attributes

**Edge case:** Nếu chưa có Variant phù hợp → hiển thị link "Tạo Variant mới"

### EPIC-006-AC07 — Xem Variant trong OrderLine (read-only)
**Given** user xem màn hình OrderLine detail (bất kỳ role nào có quyền xem order)  
**When** scroll đến section "Thông số sản phẩm"  
**Then** hiển thị Variant attributes nhóm theo component; read-only; không có nút Edit

## Affected Areas

| Surface | Thay đổi |
|---------|----------|
| **api-kingston** | Entity mới: `ComponentType`, `AttributeType`, `ComponentTypeAttribute`, `Variant`, `VariantComponentAttribute`; extend `Component` (+`componentTypeId`), `OrderLine` (+`variantId`); endpoints: `/component-types`, `/attribute-types`, `/variants` |
| **web-kingston** | Trang ComponentType config (admin); Trang Variant Management (list + create + edit + detail); section "Thông số sản phẩm" trong OrderLine detail; Variant selector trong OrderLine form |
| **Database** | Bảng mới: `component_type`, `attribute_type`, `attribute_type_option`, `component_type_attribute`, `variant`, `variant_component_attribute`; alter: `component` (+`component_type_id`), `order_line` (+`variant_id`) |
| **app-kingston** | Không thuộc scope |

## Dependencies

| Phụ thuộc | Trạng thái | Owner | Ghi chú |
|-----------|-----------|-------|---------|
| Component entity (bảng hiện có) | ✅ Done | — | Cần extend thêm `componentTypeId`; Tech Lead confirm level-1 component đã queryable chưa |
| ProductStructure entity | ✅ Done | — | Cần lấy level-1 components của một ProductStructure |
| OrderLine entity | ✅ Done | — | Cần thêm `variantId` FK (nullable) |
| Permission framework | ✅ Done | — | Dùng lại pattern hiện có |

## Epic Phases

| Phase | Artifact | Status |
|-------|----------|--------|
| Planning (PO) | `EPIC-006.md` | ✅ Done |
| PRD | `PRD.md` | ✅ Done |
| Tech Design | `TECH-DESIGN.md` | ✅ Done |
| Implementation API | `impl-report-api.md` | ⬜ Chưa bắt đầu |
| Implementation Web | `impl-report-web.md` | ⬜ Chưa bắt đầu |

## Risks & Mitigations

| Rủi ro | Mức độ | Mitigation |
|--------|--------|-----------|
| EAV schema phức tạp → query chậm nếu join nhiều | Trung bình | Tech Lead index đúng trên `(variantId, componentId, attributeTypeId)`; báo cáo out of scope |
| Component chưa có `componentTypeId` → phải migrate + assign type cho toàn bộ component hiện có | Cao | Tech Lead cần migration + có thể cần UX để assign type cho component cũ; confirm trước khi implement |
| Level-1 component chưa queryable rõ ràng trong ProductStructure | Trung bình | Tech Lead confirm query pattern; nếu chưa có cần build API trước |
| Deadline 3 ngày — scope bao gồm cả config screen + Variant CRUD + OrderLine integration | Cao | Đề xuất tách: Phase 1 (ComponentType config + CRUD Variant), Phase 2 (OrderLine integration); confirm với Tech Lead |
| ComponentType config chưa có → cần seed dữ liệu ban đầu cho "Khung sắt" | Thấp | Seed qua migration; admin có thể tự thêm type mới sau qua config screen |

## Ubiquitous Language

| Tiếng Anh | Tiếng Việt | Mô tả |
|-----------|-----------|-------|
| Variant | Biến thể sản phẩm | Tập hợp các thuộc tính tùy chỉnh theo yêu cầu khách hàng, gắn với một Product + ProductStructure cụ thể |
| ComponentType | Loại component | Phân loại component (VD: "Khung sắt X", "Nệm"); drives bộ AttributeType hiển thị khi build Variant |
| AttributeType | Loại thuộc tính | Định nghĩa một thông số tùy chỉnh (VD: Hiệu ứng bề mặt, Vật liệu đan); có inputType (SELECT/TEXT) |
| VariantComponentAttribute | Giá trị thuộc tính | Giá trị cụ thể của một AttributeType cho một component trong một Variant |

## Links

- [[docs/epics/EPIC-005/EPIC-005|EPIC-005 — Add Packaging Material (pattern tham khảo)]]
- [[topics/ban-hang/_index|Ban hàng — Order, OrderLine]]
- [[topics/phan-mem/_index|Phần mềm — ProductStructure]]
