---
date: 2026-05-26
type: prd
epic_id: EPIC-006
title: Product Variant — Cấu hình thuộc tính sản phẩm theo đơn hàng
status: draft
revision: 1
---

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

## Problem & Goal

### Problem

Khi sale/kỹ thuật tiếp nhận yêu cầu tùy chỉnh sản phẩm từ khách hàng (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ó cấu trúc nào trong hệ thống** để ghi nhận các lựa chọn này:

- Thông tin trao đổi qua Zalo, email, giấy in → dễ thất lạc, không traceable khi sản xuất
- Khi thay đổi yêu cầu → không biết phiên bản nào là mới nhất
- Cùng cấu hình xuất hiện ở nhiều đơn → nhân viên phải nhập lại thủ công mỗi lần
- Khi mở thêm nhà máy sản xuất nệm → cần bộ thuộc tính khác → hardcode không scale

**Người dùng bị ảnh hưởng:**
- **Sale + Kỹ thuật** — không có form chuẩn để ghi nhận yêu cầu tùy chỉnh của khách
- **Sản xuất / Tổ trưởng** — không có spec rõ ràng trong hệ thống để sản xuất theo
- **Admin** — không thể cấu hình thêm loại component mới khi mở rộng dây chuyền sản xuất

### Data Model

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

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

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

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

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

**Seed ban đầu — ComponentType "Khung sắt":**

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

### Goal

| Mục tiêu | Đo lường | Target |
|---|---|---|
| Tập trung spec sản phẩm vào ERP | % OrderLine có Variant trước khi approve | ≥ 80% sau 1 tháng |
| Tái sử dụng cấu hình | % Variant được dùng cho ≥ 2 OrderLine | ≥ 20% sau 2 tháng |
| Loại bỏ spec ngoài hệ thống | Số incident "sai spec vì thất lạc thông tin" | = 0 |
| Thao tác thành công | Tỷ lệ save Variant thành công | > 99% |
| Thời gian phản hồi | Response time các API Variant | < 500ms p95 |

### Why Now

Bước mở rộng sang sản xuất nệm đang đến gần. Nếu không có cơ chế cấu hình linh hoạt ngay bây giờ, khi thêm dây chuyền mới sẽ phải hardcode fields → technical debt lớn. Đây là thời điểm tốt nhất để chuẩn hóa cách thu thập spec khách hàng trước khi có thêm loại component.

---

## User Flow

### Happy Path 1 — Admin tạo ComponentType và cấu hình AttributeType

```
1. Admin mở trang "Cấu hình loại component"
2. Nhấn "Tạo loại mới"
3. Nhập tên ComponentType (VD: "Khung sắt X")
4. Gắn từng AttributeType từ danh sách seeded:
   - Chọn AttributeType + đánh dấu required / optional
5. Nhấn Lưu
6. ComponentType xuất hiện trong danh sách
7. Component nào được assign type này sẽ render đúng form trong Variant builder
```

### Happy Path 2 — Sale/Kỹ thuật tạo Variant mới

```
1. Sale mở trang "Variant Management" → nhấn "Tạo Variant mới"
2. Nhập tên Variant (VD: "Khung đen cào xước RAL9016")
3. Chọn Product từ dropdown
4. Chọn ProductStructure (lọc theo Product đã chọn)
5. Nhấn "Tiếp tục"
6. Hệ thống load danh sách level-1 components của ProductStructure
7. Mỗi component hiển thị section riêng:
   - Nếu có componentType → render fields theo AttributeType (SELECT / TEXT)
   - Nếu chưa có componentType → badge "Chưa cấu hình loại"
8. Sale điền các attributes; required fields được đánh dấu (*)
9. Nhấn Lưu
10. Redirect đến Variant detail
```

### Happy Path 3 — Gắn Variant vào OrderLine

```
1. Sale đang tạo/sửa OrderLine (product có ProductStructure)
2. Trường "Variant" xuất hiện trong form
3. Sale tìm/chọn Variant (gợi ý lọc theo ProductStructure tương ứng)
4. Variant đã chọn hiển thị tên + preview tóm tắt attributes
5. Lưu OrderLine
```

### Happy Path 4 — Sản xuất/Tổ trưởng xem spec Variant của OrderLine

```
1. Tổ trưởng/công nhân mở OrderLine detail
2. Scroll đến section "Thông số sản phẩm"
3. Đọc bảng: tên component → danh sách attribute (read-only)
4. Biết ngay cần sản xuất theo spec nào
```

### Error / Edge Paths

| Tình huống | Hành vi hệ thống |
|---|---|
| Tên ComponentType trùng | API 409: "Tên loại đã tồn tại"; form không đóng |
| Tạo ComponentType không có AttributeType | API 400 + inline error: "Loại component phải có ít nhất 1 thuộc tính" |
| Tạo Variant không nhập tên | Inline error: "Vui lòng nhập tên Variant" |
| Không chọn Product | Inline error: "Vui lòng chọn sản phẩm" |
| Không chọn ProductStructure | Inline error: "Vui lòng chọn kết cấu" |
| Attribute bắt buộc chưa điền khi save | Inline error dưới field: "Trường này là bắt buộc"; scroll đến field đầu tiên |
| SELECT field không có option nào | Field hiển thị "Chưa có lựa chọn. Liên hệ admin."; field disabled |
| ProductStructure không có level-1 component | "Kết cấu này không có component. Vui lòng chọn kết cấu khác." |
| Toàn bộ component chưa có componentType | Warning: "Các component chưa được cấu hình loại. Liên hệ admin để cấu hình." |
| API load ProductStructure lỗi | Toast error: "Không thể tải kết cấu sản phẩm. Thử lại."; nút Tiếp tục disabled |
| Network timeout khi save Variant | Toast error: "Không thể lưu, vui lòng thử lại"; form giữ nguyên giá trị |
| Không có Variant phù hợp khi tạo OrderLine | Link "Tạo Variant mới" hiển thị ngay trong dropdown |

### Recovery Paths

- Sau lỗi validation → sửa inline, không cần bắt đầu lại từ đầu
- Component "Chưa cấu hình loại" → bỏ qua được; Variant vẫn lưu được với các component có type
- Chọn sai ProductStructure ở bước 1 → nhấn "Quay lại" để chọn lại (attributes đã điền bị reset — báo trước bằng confirm dialog)
- Gắn nhầm Variant vào OrderLine → edit OrderLine, chọn Variant khác

---

## Acceptance Criteria

### EPIC-006-AC01 — Admin tạo ComponentType + gắn AttributeType (Must)

**Given** admin đang xem trang Cấu hình loại component  
**When** nhập tên ComponentType, gắn ≥ 1 AttributeType (với flag required/optional), nhấn Lưu  
**Then**
- API `POST /component-types` trả HTTP 201 với entity mới
- ComponentType xuất hiện trong danh sách
- Variant tạo sau đó với component có type này sẽ render đúng fields

**Error cases:**
- Tên trùng → API 409 + inline error: `"Tên loại đã tồn tại"`
- Không gắn AttributeType → API 400 + inline error: `"Loại component phải có ít nhất 1 thuộc tính"`

---

### EPIC-006-AC02 — Admin assign ComponentType cho Component (Must)

**Given** admin xem / chỉnh sửa một Component  
**When** chọn ComponentType từ dropdown, nhấn Lưu  
**Then**
- API `PATCH /components/:id` cập nhật `componentTypeId`
- Component đó khi xuất hiện trong Variant builder sẽ render đúng fields theo type đã chọn

---

### EPIC-006-AC03 — Tạo Variant — bước 1: chọn Product + ProductStructure (Must)

**Given** sale/kỹ thuật 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 bước 2 hiển thị danh sách level-1 components của ProductStructure đã chọn
- Component có componentType → render đúng AttributeType fields (SELECT hoặc TEXT)
- Field bắt buộc được đánh dấu (*)
- Component chưa có componentType → badge "Chưa cấu hình loại"; không có field để điền

**Error cases:**
- Tên trống → inline error: `"Vui lòng nhập tên Variant"`
- Không chọn Product → inline error: `"Vui lòng chọn sản phẩm"`
- Không chọn ProductStructure → inline error: `"Vui lòng chọn kết cấu"`

---

### EPIC-006-AC04 — Tạo Variant — bước 2: điền và lưu attributes (Must)

**Given** form bước 2 đang hiển thị các component + fields  
**When** điền đầy đủ các attribute bắt buộc, nhấn Lưu  
**Then**
- API `POST /variants` trả HTTP 201
- Redirect đến Variant detail
- Variant detail hiển thị: tên, product, productStructure, và attributes nhóm theo component

**Error cases:**
- Attribute bắt buộc chưa điền → inline error dưới field: `"Trường này là bắt buộc"`; không submit
- TEXT field vượt 500 ký tự → inline error: `"Không được vượt quá 500 ký tự"`

---

### EPIC-006-AC05 — Chỉnh sửa Variant (Must)

**Given** user đang xem Variant detail  
**When** nhấn Edit, sửa một hoặc nhiều attribute, nhấn Lưu  
**Then**
- API `PUT /variants/:id` trả HTTP 200
- Variant detail cập nhật hiển thị giá trị mới ngay lập tức
- Validation AC04 vẫn áp dụng (attribute bắt buộc không được để trống)

---

### EPIC-006-AC06 — Xem danh sách Variant (Must)

**Given** user mở trang Variant Management  
**When** trang load  
**Then**
- API `GET /variants` trả HTTP 200 trong < 500ms p95
- Danh sách hiển thị columns: Tên Variant | Sản phẩm | Kết cấu | 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 theo tên Variant: debounce 300ms, min 1 ký tự, filter realtime hoặc submit

---

### EPIC-006-AC07 — Gắn Variant vào OrderLine (Must)

**Given** user đang tạo/sửa một OrderLine, product đã chọn có ProductStructure  
**When** trường Variant xuất hiện trong form  
**Then**
- User có thể search và chọn Variant; danh sách gợi ý lọc theo ProductStructure tương ứng
- Variant đã chọn hiển thị tên + preview tóm tắt attributes
- Nếu không có Variant phù hợp → link "Tạo Variant mới" xuất hiện trong dropdown, mở trang tạo Variant trong tab mới

---

### EPIC-006-AC08 — Xem Variant trong OrderLine detail (read-only) (Must)

**Given** user xem OrderLine detail (bất kỳ role nào có quyền xem)  
**When** scroll đến section "Thông số sản phẩm"  
**Then**
- Attributes hiển thị nhóm theo component name
- Mỗi attribute: tên attribute | giá trị (read-only)
- Không có nút Edit trong section này
- Nếu OrderLine chưa có Variant → "Chưa có thông số sản phẩm"

---

## UI / Design

_Design chưa có. Mô tả requirements đủ để implement:_

### 1. Trang Cấu hình loại component (Admin)

- Table: Tên ComponentType | Số AttributeType | Actions (Edit, Delete)
- Form tạo/sửa: text field "Tên" + multi-select danh sách AttributeType (toggle Required per item)
- Delete chỉ khả dụng nếu không có Component nào đang dùng type đó; nếu có → disable nút + tooltip "Đang được sử dụng bởi X component"

### 2. Trang Variant Management (Sale / Kỹ thuật)

- List view với columns: Tên Variant | Sản phẩm | Kết cấu | Ngày tạo | Người tạo | Actions (View, Edit)
- Search bar (text, debounce 300ms)
- Create flow — stepper 2 bước:
  - **Bước 1**: Nhập tên + chọn Product + chọn ProductStructure; nút "Tiếp tục" disabled cho đến khi cả 3 field hợp lệ
  - **Bước 2**: Form attributes nhóm theo component — mỗi component = 1 card/section
    - SELECT field → dropdown với options seeded
    - TEXT field → input text (max 500 ký tự)
    - Required field → dấu (*) + inline error khi bỏ trống
    - Component không có type → card gray, badge "Chưa cấu hình loại", không có field
- Nút "Quay lại" ở bước 2 → confirm dialog nếu đã điền dữ liệu

### 3. OrderLine Form — extension

- Trường "Variant" (optional) xuất hiện khi Product có ProductStructure
- Combobox + search; lọc theo ProductStructure của product đã chọn
- Preview mini khi đã chọn: tên Variant + tóm tắt 2–3 attribute đầu tiên
- Link "Tạo Variant mới" khi search không có kết quả, mở tab mới

### 4. OrderLine Detail — section "Thông số sản phẩm"

- Card cho mỗi component: header = tên component + danh sách attribute (key: value)
- Read-only; không có nút Edit, không có action

---

## Non-Functional Requirements

### Performance

| API | SLA p95 |
|---|---|
| `GET /variants` (list) | < 500ms |
| `POST /variants` (create) | < 1000ms |
| `GET /variants/:id` (detail) | < 300ms |
| `GET /component-types` | < 300ms |
| `GET /attribute-types` | < 300ms |

EAV join pattern có thể nặng nếu nhiều attribute — Tech Lead cần index đúng trên `(variant_id, component_id, attribute_type_id)`.

### Security

- **Authentication**: tất cả endpoints Variant yêu cầu JWT (cùng pattern với module hiện có)
- **Authorization**: dùng permission framework hiện có
  - `variant:read` — Sale, Kỹ thuật, Sản xuất, Quản lý sản xuất
  - `variant:write` — Sale, Kỹ thuật
  - `component-type:write` — Admin
- **PII**: không có PII trong Variant data
- **Input validation**: TEXT field max 500 ký tự; tên Variant max 255 ký tự

### Accessibility

- Form fields có label rõ ràng; SELECT field keyboard-accessible
- Error messages không chỉ dùng màu (kèm text)

### Compatibility

- Browser: Chrome 100+, Safari 15+, Edge 100+
- Desktop web only (app-kingston out of scope)

---

## Dependencies

| Dependency | Trạng thái | Owner | Ghi chú |
|---|---|---|---|
| Component entity (bảng `component`) | ✅ Done | Backend | Cần thêm `componentTypeId` FK nullable; cần confirm level-1 component queryable chưa |
| ProductStructure entity | ✅ Done | Backend | Cần API trả level-1 components cho một ProductStructure — xác nhận endpoint tồn tại |
| OrderLine entity | ✅ Done | Backend | Cần thêm `variantId` FK nullable — không break existing flow |
| Permission framework | ✅ Done | Backend | Dùng lại pattern `variant:read`, `variant:write`, `component-type:write` |
| AttributeType seed data | ⚠️ Cần tạo | Backend | Migration seed 4 AttributeTypes + options cho ComponentType "Khung sắt" |
| UI component library (web-kingston) | ✅ Done | Frontend | Select, text input, stepper, combobox đã có sẵn |

**Rủi ro phụ thuộc cần xác nhận trước khi implement:**

1. **Level-1 component API**: Tech Lead confirm endpoint `GET /product-structures/:id/components?level=1` đã có hay cần build mới
2. **Migration existing components**: `componentTypeId` nullable → Component hiện tại không bị break. Admin assign type qua UI (AC02) sau khi rollout
3. **Delete ComponentType**: cần guard — không cho xóa nếu đang có Component sử dụng type đó

---

## Rollout

### Strategy

**Direct rollout** — không cần feature flag.

- `variantId` trên OrderLine là nullable → OrderLine không có Variant vẫn hoạt động bình thường
- `componentTypeId` trên Component là nullable → Component không có type vẫn hoạt động bình thường
- Tính năng mới hoàn toàn, không thay đổi hành vi flow hiện có

### Phase đề xuất (deadline 3 ngày)

| Phase | Scope | Timeline |
|---|---|---|
| Phase 1 | ComponentType config + CRUD Variant (AC01–AC06) | Ngày 1–2 |
| Phase 2 | OrderLine integration (AC07–AC08) | Ngày 2–3 |

### Kill-switch

Nếu có regression sau rollout:
- Ẩn trang Variant Management trên web (route guard)
- Ẩn Variant selector trong OrderLine form
- DB không bị ảnh hưởng (nullable FK, additive migrations)
