improve BA skill

This commit is contained in:
Leonard-ThindPad-P50
2026-09-09 06:34:57 +07:00
parent 119792967c
commit 2c7bcde741
42 changed files with 5429 additions and 54 deletions

View File

@@ -0,0 +1,215 @@
# AC — Acceptance Criteria — US-002 Xem giỏ hàng nhóm theo seller
| | |
|---|---|
| **Version** | 1.0 |
| **Date** | 2026-09-08 |
| **Author** | BA (qua skill ba-3-specification) |
| **Status** | 🟡 Draft |
| **Approved by** | QA: — *(chưa có QA thật tham gia lần chạy này)* |
| **Source** | `SRS_US002-003_v1.0.md` · `RBAC_CartCheckout_v1.0.md` §2, §6 |
| **Scope** | US-002 |
## Change Log
| Version | Date | Người sửa | Thay đổi | CR |
|---|---|---|---|---|
| 1.0 | 2026-09-08 | BA (qua skill ba-3-specification) | Bản đầu — 6 AC, 4 nhóm | — |
---
## 1. Bốn nhóm bắt buộc
| Nhóm | Nội dung | Tối thiểu | Số AC đã viết |
|---|---|---|---|
| 1. Luồng thành công | Xem giỏ hàng nhóm theo seller | 1/hành động | 2 |
| 2. Validation | Không áp dụng — US-002 không có input do người dùng nhập, chỉ hiển thị (xem lý do dưới) | — | 0 (N/A có lý do) |
| 3. Lỗi hệ thống | Timeout, 5xx, phân biệt "rỗng thật" vs "không tải được" | ≥1 | 2 |
| 4. Phân quyền | Không đủ quyền, hết phiên | ≥1/vai trò bị chặn | 2 |
**Vì sao Nhóm 2 (Validation) ghi N/A:** US-002 là hành động **xem** thuần tuý (`SCR-04` khi mở màn hình) — không có trường nào do người dùng nhập trong phạm vi US-002 (nhập số lượng thuộc US-003, xem `AC_US-003_v1.0.md`). Đây không phải "chỉ có nhóm 1" (vi phạm nguyên tắc), vì Nhóm 3 và Nhóm 4 đã viết đủ.
---
## 2. Danh sách AC
### Nhóm 1 — Luồng thành công
#### AC-US002-01 — Xem giỏ hàng có nhiều seller
| | |
|---|---|
| **Liên quan** | SCR-04 · C01, C02, C03–C06, C08, C09, C11 |
| **Vai trò** | ROLE-02 Customer (hành vi tương tự với ROLE-01 Guest, khác ở định danh `X-Guest-Session-Id` thay JWT) |
```
Given tôi đăng nhập với ROLE-02 (Customer) và giỏ hàng của tôi (Cart.status=active) có 5 CartItem:
3 CartItem thuộc seller "Shop A", 2 CartItem thuộc seller "Shop B"
When tôi mở SCR-04 (Giỏ hàng)
Then hệ thống hiển thị đúng 2 nhóm theo seller ("Shop A", "Shop B"), mỗi nhóm hiển thị đúng
số CartItem thuộc nhóm đó
And tạm tính mỗi nhóm (C08) bằng Σ (quantity × đơn giá hiển thị) của các CartItem trong
nhóm đó
And tổng cộng giỏ hàng (C09) bằng Σ tất cả C08
And nút "Tiến hành Checkout" (C11) hiển thị và không bị disable
```
**Dữ liệu mẫu để test:**
| Field | Giá trị |
|---|---|
| Shop A — CartItem 1 | quantity=2, đơn giá=100.000 ₫ |
| Shop A — CartItem 2 | quantity=1, đơn giá=50.000 ₫ |
| Shop A — CartItem 3 | quantity=1, đơn giá=75.000 ₫ |
| Shop A — tạm tính (C08) kỳ vọng | 2×100.000 + 1×50.000 + 1×75.000 = 325.000 ₫ |
| Shop B — CartItem 1 | quantity=1, đơn giá=200.000 ₫ |
| Shop B — CartItem 2 | quantity=2, đơn giá=30.000 ₫ |
| Shop B — tạm tính (C08) kỳ vọng | 1×200.000 + 2×30.000 = 260.000 ₫ |
| Tổng cộng (C09) kỳ vọng | 325.000 + 260.000 = 585.000 ₫ |
#### AC-US002-02 — Xem giỏ hàng rỗng
| | |
|---|---|
| **Liên quan** | SCR-04 · C10, C11 |
| **Vai trò** | ROLE-01, ROLE-02 |
```
Given tôi đăng nhập với ROLE-02 và giỏ hàng của tôi không có CartItem nào (Cart.status=active,
0 dòng)
When tôi mở SCR-04
Then hệ thống hiển thị trạng thái "Giỏ hàng trống" (khoá cart.empty.title) và nút
"Tiếp tục mua sắm" (C10)
And nút "Tiến hành Checkout" (C11) không hiển thị/bị disable — theo §2.A.3.6 của SRS
```
---
### Nhóm 2 — Validation
*N/A — xem lý do ở mục 1.*
---
### Nhóm 3 — Lỗi hệ thống
#### AC-US002-03 — Lỗi hệ thống khi tải giỏ hàng
| | |
|---|---|
| **Liên quan** | SCR-04 · C12, C13 |
| **Vai trò** | ROLE-01, ROLE-02 |
```
Given tôi đăng nhập với ROLE-02
When tôi mở SCR-04 và GET /v1/cart trả về 500/503 hoặc timeout (ngưỡng — xem NFR §1, OQ-033)
Then hệ thống hiển thị banner lỗi "Không tải được dữ liệu, thử lại." (E-CART-0004) tại vùng
danh sách
And nút "Thử lại" (C13) hiển thị; bấm vào gọi lại GET /v1/cart
```
Ba tình huống tối thiểu của nhóm này:
| # | Tình huống | AC |
|---|---|---|
| 1 | Server trả 5xx | AC-US002-03 |
| 2 | Timeout / mất mạng | AC-US002-03 |
| 3 | Dữ liệu bị người khác sửa/xoá trong lúc mình đang mở | Không áp dụng ở US-002 (chỉ xem, không sửa) — xem `AC_US-003_v1.0.md` §3 Nhóm 3 (`AC-US003-11`) cho trường hợp này khi có thao tác sửa/xoá |
#### AC-US002-04 — Phân biệt "giỏ hàng chưa từng có dữ liệu" và "không tải được"
| | |
|---|---|
| **Liên quan** | SCR-04 · C10, C12 |
| **Vai trò** | ROLE-02 |
```
Given tôi đăng nhập với ROLE-02 lần đầu, chưa từng thêm sản phẩm nào vào giỏ (Cart mới tạo,
0 CartItem); GET /v1/cart trả 200 với danh sách rỗng
When tôi mở SCR-04
Then hệ thống hiển thị trạng thái "Giỏ hàng trống" (không phải banner lỗi C12)
And hai trạng thái "giỏ hàng trống thật" (AC-US002-02/04) và "không tải được" (AC-US002-03)
dùng hai khối hiển thị khác nhau (data-state khác nhau trong WF), không dùng chung một
thông điệp — kiểm chứng bằng cách tắt mạng khi mở màn hình lần đầu (kỳ vọng thấy banner
lỗi, KHÔNG phải "Giỏ hàng trống")
```
---
### Nhóm 4 — Phân quyền
#### AC-US002-05 — Guest chỉ thấy giỏ hàng của session mình
| | |
|---|---|
| **Liên quan** | SCR-04 |
| **Vai trò** | ROLE-01 |
```
Given tôi là Guest với X-Guest-Session-Id = S1; một Guest khác có giỏ hàng thuộc session S2
(không phải của tôi)
When tôi gọi GET /v1/cart với header X-Guest-Session-Id: S1
Then hệ thống chỉ trả về giỏ hàng gắn với S1 (dữ liệu của chính tôi), không có cách nào lộ
dữ liệu của S2 qua endpoint này (endpoint không nhận cartId của người khác qua tham số)
```
#### AC-US002-06 — Customer hết phiên đăng nhập cố xem giỏ hàng
| | |
|---|---|
| **Liên quan** | SCR-04, SCR-08 (đăng nhập) |
| **Vai trò** | ROLE-02 |
```
Given tôi trước đó đã đăng nhập và có giỏ hàng theo customer_id; nay access token đã hết hạn
(401 ERR_AUTH_INVALID_TOKEN)
When tôi mở SCR-04
Then hệ thống chuyển hướng về màn đăng nhập (SCR-08)
And giữ đường dẫn quay lại SCR-04 sau khi đăng nhập thành công (theo UICONV §2)
```
---
## 3. Bảng dữ liệu biên
*N/A — US-002 không có field nhập liệu (chỉ xem). Xem `AC_US-003_v1.0.md` §4 cho F01 (số lượng).*
---
## 4. Kịch bản kết hợp
| # | Kịch bản | Các bước | AC liên quan |
|---|---|---|---|
| S1 | Mở giỏ hàng nhiều seller rồi tiến hành checkout | SCR-04 (mở, kiểm tra tổng đúng) → bấm "Tiến hành Checkout" (C11) → điều hướng SCR-05 (chỉ kiểm tra điều hướng, ngoài phạm vi hành vi SCR-05) | AC-US002-01 |
---
## 5. Truy vết
| AC | US | BR | Field/Thành phần | Mã lỗi | Test case (QA điền) | Kết quả (QA điền) |
|---|---|---|---|---|---|---|
| AC-US002-01 | US-002 | — | C01, C02, C03–C06, C08, C09, C11 | — | | |
| AC-US002-02 | US-002 | BR §3 CART | C10, C11 | — | | |
| AC-US002-03 | US-002 | — | C12, C13 | E-CART-0004 | | |
| AC-US002-04 | US-002 | — | C10, C12 | E-CART-0004 | | |
| AC-US002-05 | US-002 | RBAC §2 | — | — | | |
| AC-US002-06 | US-002 | — | — | — | | |
---
## 6. Xác nhận của QA
| | |
|---|---|
| **Người xác nhận** | *(chưa có — chưa có QA thật tham gia lần chạy này)* |
| **Ngày** | — |
| ☐ Mọi AC đều test được (không có AC mô tả cảm tính) | |
| ☐ Đủ 4 nhóm (Nhóm 2 N/A có lý do) | |
| ☐ Bảng dữ liệu biên đủ dùng để viết test case (N/A — không có field ở US-002) | |
| ☐ Không có AC nào mâu thuẫn với AC khác | |
**AC bị QA từ chối:**
| AC | Lý do từ chối | BA sửa thế nào |
|---|---|---|
| | | |

View File

@@ -0,0 +1,267 @@
# AC — Acceptance Criteria — US-003 Sửa số lượng / xoá sản phẩm trong giỏ hàng
| | |
|---|---|
| **Version** | 1.0 |
| **Date** | 2026-09-08 |
| **Author** | BA (qua skill ba-3-specification) |
| **Status** | 🟡 Draft |
| **Approved by** | QA: — *(chưa có QA thật tham gia lần chạy này)* |
| **Source** | `SRS_US002-003_v1.0.md` · `RBAC_CartCheckout_v1.0.md` §6 · `BR_CartCheckout_v1.0.md` §3 |
| **Scope** | US-003 |
## Change Log
| Version | Date | Người sửa | Thay đổi | CR |
|---|---|---|---|---|
| 1.0 | 2026-09-08 | BA (qua skill ba-3-specification) | Bản đầu — 13 AC, 4 nhóm (2 AC còn mở chờ `OQ-028`/`OQ-032`, ghi rõ thay vì bịa) | — |
---
## 1. Bốn nhóm bắt buộc
| Nhóm | Nội dung | Tối thiểu | Số AC đã viết |
|---|---|---|---|
| 1. Luồng thành công | Tăng/giảm số lượng, xoá dòng, xoá nhóm, xoá giỏ, huỷ modal | 1/hành động | 6 |
| 2. Validation | Số lượng không hợp lệ; số lượng về 0 | 1/rule | 2 (1 còn mở — `OQ-028`) |
| 3. Lỗi hệ thống | 5xx cập nhật, timeout xoá, item bị xoá đồng thời | ≥1 | 3 |
| 4. Phân quyền | Sửa/xoá CartItem không thuộc quyền sở hữu (Customer, Guest) | ≥1/vai trò | 2 (1 còn mở — `OQ-032`) |
---
## 2. Danh sách AC
### Nhóm 1 — Luồng thành công
#### AC-US003-01 — Tăng số lượng CartItem thành công
| | |
|---|---|
| **Liên quan** | SCR-04 · F01, C08, C09 |
| **Vai trò** | ROLE-02 (tương tự ROLE-01) |
```
Given CartItem X có quantity=2, Cart.status=active
When tôi bấm nút "+" (F01) để tăng số lượng lên 3
Then hệ thống gọi PATCH /v1/cart/items/{X} với quantity=3, nhận 200 OK
And F01 hiển thị 3; tạm tính nhóm (C08) và tổng cộng (C09) cập nhật lại ngay theo giá trị mới
```
#### AC-US003-02 — Giảm số lượng CartItem thành công (còn ≥1)
```
Given CartItem X có quantity=3
When tôi bấm nút "-" (F01) để giảm còn 2
Then PATCH thành công, F01=2, C08/C09 cập nhật
```
#### AC-US003-03 — Xoá CartItem thành công (đã xác nhận modal)
| | |
|---|---|
| **Liên quan** | SCR-04 · C07, C08, C09 |
| **Vai trò** | ROLE-02 |
```
Given CartItem X tồn tại trong nhóm seller "Shop A" (nhóm đang có 2 CartItem)
When tôi bấm "Xoá" (C07) trên CartItem X, modal xác nhận hiện ra (theo UICONV §5), tôi bấm
nút xác nhận trong modal
Then hệ thống gọi DELETE /v1/cart/items/{X}, nhận 200/204
And dòng CartItem X biến mất khỏi danh sách; tạm tính nhóm "Shop A" (C08) cập nhật (còn
1 CartItem); tổng cộng (C09) cập nhật
```
#### AC-US003-04 — Xoá CartItem cuối cùng của một nhóm seller
```
Given nhóm seller "Shop B" chỉ có 1 CartItem
When tôi xoá CartItem đó (đã xác nhận modal)
Then toàn bộ khối nhóm "Shop B" (C02 + dòng CartItem) biến mất khỏi màn hình — không hiển
thị một nhóm rỗng không có dòng nào
```
#### AC-US003-05 — Xoá CartItem cuối cùng của toàn giỏ hàng
```
Given giỏ hàng chỉ còn duy nhất 1 CartItem (1 nhóm, 1 dòng)
When tôi xoá CartItem đó (đã xác nhận modal)
Then màn hình chuyển sang trạng thái "Giỏ hàng trống" (theo SRS §2.A.3.6); nút
"Tiến hành Checkout" (C11) ẩn/disable
And Cart.status vẫn là active (KHÔNG tự chuyển abandoned — theo BR §3, chỉ hết hạn phiên
mới chuyển abandoned)
```
#### AC-US003-06 — Huỷ modal xác nhận xoá
```
Given modal xác nhận xoá CartItem X đang mở
When tôi bấm nút huỷ hoặc đóng modal (không xác nhận)
Then CartItem X vẫn còn nguyên trong danh sách; không có request DELETE nào được gửi
```
---
### Nhóm 2 — Validation
#### AC-US003-07 — Nhập số lượng không phải số nguyên dương hợp lệ
| | |
|---|---|
| **Liên quan** | SCR-04 · F01 |
| **Vai trò** | ROLE-02 |
```
Given CartItem X có quantity=2
When tôi nhập trực tiếp vào F01 giá trị không hợp lệ (VD "-1", "1.5", "abc", để trống)
Then hệ thống hiện lỗi inline dưới F01: "Số lượng phải là số nguyên từ 1 trở lên."
(E-CART-0002)
And không gửi request PATCH; F01 giữ nguyên giá trị hợp lệ trước đó (không lưu giá trị lỗi)
```
#### AC-US003-08 — Giảm số lượng về 0 *(🔴 chưa viết được đầy đủ — chờ `OQ-028`)*
```
Given CartItem X có quantity=1 (đã ở mức tối thiểu)
When tôi bấm nút "-" hoặc nhập trực tiếp "0" vào F01
Then 🔴 CHƯA XÁC ĐỊNH — phụ thuộc OQ-028 (PO chưa chốt: (a) chặn nút "-"/nhập 0, hiện
thông báo giữ tối thiểu 1, hay (b) tự động mở modal xác nhận xoá dòng)
```
*QA không viết test case cho AC này cho tới khi `OQ-028` được trả lời — đây là chỗ SRS chưa
viết xong, không phải QA bỏ sót.*
---
### Nhóm 3 — Lỗi hệ thống
#### AC-US003-09 — Server lỗi 5xx khi cập nhật số lượng
| | |
|---|---|
| **Liên quan** | SCR-04 · F01 |
| **Vai trò** | ROLE-02 |
```
Given CartItem X có quantity=2
When tôi đổi thành 5 (bấm "+" 3 lần hoặc nhập tay) và PATCH /v1/cart/items/{X} trả 500
Then hệ thống hiện banner lỗi tại dòng CartItem: "Không thể cập nhật giỏ hàng, thử lại."
(E-CART-0006)
And 🔴 F01 trở về giá trị trước đó (2), KHÔNG giữ giá trị đã nhập lỗi (5); nút +/- mở lại
được bấm ngay
```
#### AC-US003-10 — Timeout khi xoá CartItem
```
Given tôi bấm "Xoá" trên CartItem X (đã xác nhận modal)
When request DELETE bị timeout theo ngưỡng NFR §1 (xem OQ-033)
Then hệ thống hiện banner lỗi tại dòng: "Không thể cập nhật giỏ hàng, thử lại." (E-CART-0006)
And CartItem X vẫn hiển thị trong danh sách (không xoá khỏi giao diện khi chưa có xác nhận
từ server); nút "Xoá" bấm lại được
```
#### AC-US003-11 — CartItem đã bị xoá bởi thao tác khác (đồng thời, VD 2 tab)
```
Given CartItem X đang hiển thị trên màn hình của tôi, nhưng đã bị xoá khỏi giỏ hàng ở một
tab/thiết bị khác trước đó
When tôi bấm sửa số lượng (F01) hoặc bấm "Xoá" (C07) trên CartItem X
Then PATCH/DELETE trả 404 ERR_NOT_FOUND hoặc 409 ERR_CONFLICT (xem API §4)
And hệ thống hiện banner "Sản phẩm này đã được xoá khỏi giỏ hàng trước đó." (E-CART-0003),
sau đó ẩn dòng CartItem X khỏi danh sách và cập nhật lại C08/C09
```
Ba tình huống tối thiểu của nhóm này:
| # | Tình huống | AC |
|---|---|---|
| 1 | Server trả 5xx | AC-US003-09 |
| 2 | Timeout / mất mạng | AC-US003-10 |
| 3 | Dữ liệu bị người khác sửa/xoá trong lúc mình đang mở | AC-US003-11 |
---
### Nhóm 4 — Phân quyền
#### AC-US003-12 — Gọi thẳng API sửa/xoá CartItem không thuộc quyền sở hữu (Customer)
| | |
|---|---|
| **Liên quan** | SCR-04 |
| **Vai trò** | ROLE-02 |
```
Given tôi đăng nhập ROLE-02 với customerId = A; CartItem Y thuộc Cart của customerId = B
When tôi gọi thẳng PATCH /v1/cart/items/{Y} với token của A
Then hệ thống trả 403 ERR_FORBIDDEN_OWNERSHIP (E-CART-0005), không cập nhật CartItem Y
And không có thông tin nào của CartItem Y bị lộ ra trong response lỗi
```
#### AC-US003-13 — Guest cố sửa/xoá CartItem không thuộc session của mình *(🔴 chưa viết được đầy đủ — chờ `OQ-032`)*
```
Given tương tự AC-US003-12 nhưng với ROLE-01 (Guest): X-Guest-Session-Id = S1 của tôi,
CartItem Z thuộc Cart của session S2
When tôi gọi thẳng PATCH /v1/cart/items/{Z} với X-Guest-Session-Id: S1
Then 🔴 CHƯA XÁC ĐỊNH — đề xuất BA: trả 403 ERR_FORBIDDEN_OWNERSHIP (E-CART-0005) giống hệt
AC-US003-12, cần Tech Lead xác nhận cơ chế đối chiếu session_id (OQ-032) trước khi coi
AC này là chốt
```
🔴 Chặn ở giao diện là trải nghiệm, **chặn ở backend mới là bảo mật**. Cả hai AC nhóm này đều
kiểm tra gọi thẳng API, không chỉ kiểm tra ẩn nút trên giao diện.
---
## 3. Bảng dữ liệu biên
| Field | Dưới ngưỡng | Ngưỡng dưới | Trong khoảng | Ngưỡng trên | Trên ngưỡng | Rỗng | Ký tự đặc biệt | Khoảng trắng đầu/cuối |
|---|---|---|---|---|---|---|---|---|
| F01 Số lượng (≥1, ngưỡng trên **chưa chốt** — `OQ-030`) | `0` → lỗi `E-CART-0002` (hành vi cụ thể — `OQ-028`) | `1` → OK | `5` → OK | 🔴 chưa có ngưỡng trên xác nhận — `OQ-030` | 🔴 chưa có ngưỡng trên — `OQ-030` | Xoá trắng ô nhập → lỗi bắt buộc (`E-CART-0002`) | `"abc"`, `"1.5"`, `"-1"` → lỗi `E-CART-0002` | N/A — F01 là input dạng số (`type=number`), không nhận khoảng trắng |
---
## 4. Kịch bản kết hợp
| # | Kịch bản | Các bước | AC liên quan |
|---|---|---|---|
| S1 | Sửa số lượng rồi xoá hết một nhóm seller | SCR-04 (mở) → sửa số lượng CartItem của "Shop B" (tổng cập nhật) → xoá cả 2 CartItem còn lại của "Shop B" → nhóm "Shop B" biến mất, tổng cộng chỉ còn của "Shop A" | AC-US003-01, 03, 04 |
---
## 5. Truy vết
| AC | US | BR | Field/Thành phần | Mã lỗi | Test case (QA điền) | Kết quả (QA điền) |
|---|---|---|---|---|---|---|
| AC-US003-01 | US-003 | BR §3 CART | F01, C08, C09 | — | | |
| AC-US003-02 | US-003 | BR §3 CART | F01, C08, C09 | — | | |
| AC-US003-03 | US-003 | BR §3 CART | C07, C08, C09 | — | | |
| AC-US003-04 | US-003 | — | C02, C07 | — | | |
| AC-US003-05 | US-003 | BR §3 CART | C07, C10, C11 | — | | |
| AC-US003-06 | US-003 | — | C07 | — | | |
| AC-US003-07 | US-003 | — | F01 | E-CART-0002 | | |
| AC-US003-08 | US-003 | — | F01 | E-CART-0002 (dự kiến) | *(chờ OQ-028)* | |
| AC-US003-09 | US-003 | — | F01 | E-CART-0006 | | |
| AC-US003-10 | US-003 | — | C07 | E-CART-0006 | | |
| AC-US003-11 | US-003 | — | F01, C07 | E-CART-0003 | | |
| AC-US003-12 | US-003 | RBAC §6 | — | E-CART-0005 | | |
| AC-US003-13 | US-003 | RBAC §6 | — | E-CART-0005 (dự kiến) | *(chờ OQ-032)* | |
---
## 6. Xác nhận của QA
| | |
|---|---|
| **Người xác nhận** | *(chưa có — chưa có QA thật tham gia lần chạy này)* |
| **Ngày** | — |
| ☐ Mọi AC đều test được (2 AC còn mở, chờ OQ trước khi viết test case) | |
| ☐ Đủ 4 nhóm | |
| ☐ Bảng dữ liệu biên đủ dùng để viết test case | |
| ☐ Không có AC nào mâu thuẫn với AC khác | |
**AC bị QA từ chối:**
| AC | Lý do từ chối | BA sửa thế nào |
|---|---|---|
| | | |

View File

@@ -0,0 +1,235 @@
# API — Contract — US-002, US-003 (SCR-04 Giỏ hàng)
| | |
|---|---|
| **Version** | 1.0 |
| **Date** | 2026-09-08 |
| **Author** | BA (qua skill ba-3-specification) |
| **Status** | 🟡 Draft |
| **Nguồn contract** | 🔶 **Hỗn hợp** — method/path/auth: ✅ **BE cung cấp** (`e-commerce/docs/sections/04-api-design.md` §4.1.5 "Cart & Order Service"); request/response body schema: ⚠️ **BA đề xuất — chờ BE xác nhận** (SAD liệt kê method/path/mô tả ngắn nhưng không có ví dụ JSON cho ba endpoint dưới đây, khác với `POST /v1/checkout` là endpoint duy nhất SAD có ví dụ đầy đủ) |
| **Approved by** | BE Lead: — |
| **Source** | `SRS_US002-003_v1.0.md` · `e-commerce/docs/sections/04-api-design.md` §4.1.1, §4.1.5, §4.1.13, §4.2 |
| **Scope** | US-002, US-003 |
> 🔴 **Đọc dòng `Nguồn contract` trước.** Method/path là ràng buộc thật (đã có trong SAD đã
> duyệt). Cấu trúc request/response bên dưới là **giả định của BA**, dev phải đối chiếu với
> tài liệu BE thật (hoặc OpenAPI spec nếu có) trước khi code.
---
## 1. Ba điểm phải chốt trước tiên
| # | Vấn đề | Quyết định | Lý do |
|---|---|---|---|
| 1 | **Số lớn** (`cartItemId`, `productVariantId`, `sellerId`) truyền dạng gì | `string` | Khớp ví dụ `POST /v1/checkout` của SAD (`"orderId": "order_001"` dạng string) — áp dụng nhất quán cho mọi id trong hệ thống |
| 2 | **Thời gian** — SCR-04 không hiển thị trường thời gian nào | N/A | Không có trường `*_at` nào cần hiển thị ở US-002/US-003 |
| 3 | **Phân trang** | Không áp dụng — `GET /v1/cart` trả toàn bộ giỏ hàng, không phân trang | Giỏ hàng không có khái niệm "trang", số dòng thực tế nhỏ (không giống danh sách sản phẩm) |
## 2. Quy ước chung
| | |
|---|---|
| **Base URL** | `https://api.<domain>/v1` (theo SAD §4.1.1) |
| **Xác thực** | Bearer JWT (Customer) hoặc header `X-Guest-Session-Id` (Guest) — theo SAD §4.1.1, §4.1.5 |
| **Định dạng phản hồi thành công** | `{ "data": {...} }` (theo SAD §4.1.1) |
| **Định dạng phản hồi lỗi** | `{ "error": { "code", "message", "details" }, "traceId" }` (theo SAD §4.1.13 — **khác** với format mặc định của template BA, ưu tiên đúng theo SAD) |
| **Mã HTTP dùng** | 200 · 204 · 400 · 401 · 403 · 404 · 409 · 500 · 503 |
| **Ngôn ngữ thông điệp** | FE tự dịch theo mã lỗi (`code`) + bảng text §4.2 của `SRS`, không dựa vào `message` trả về từ BE để hiển thị cho người dùng cuối (theo `UICONV` §9 — BA sở hữu text hiển thị, W5) |
🔴 **Lỗi nghiệp vụ trả mã HTTP 4xx** (không phải 200 kèm cờ lỗi) — đúng theo thiết kế đã có ở
SAD §4.1.13.
---
## 3. Endpoint
### 3.1 `GET /v1/cart` — Lấy giỏ hàng hiện tại (đa seller)
| | |
|---|---|
| **Mục đích** | Tải dữ liệu hiển thị SCR-04 (US-002) |
| **Màn hình** | SCR-04 |
| **Quyền** | ROLE-01 Guest (`X-Guest-Session-Id`) hoặc ROLE-02 Customer (Bearer JWT) — không cần scope riêng, chỉ cần định danh hợp lệ |
**Query parameters:** không có (không phân trang).
**Response 200** *(⚠️ đề xuất của BA — SAD không có ví dụ JSON cho endpoint này)*
```json
{
"data": {
"cartId": "cart_123",
"status": "active",
"sellers": [
{
"sellerId": "seller_11",
"sellerName": "Shop A",
"subtotalVnd": 325000,
"items": [
{
"cartItemId": "citem_001",
"productVariantId": "variant_789",
"productName": "Áo thun basic",
"variantLabel": "Size M, Đỏ",
"unitPriceVnd": 100000,
"quantity": 2,
"imageUrl": "https://cdn.example.com/p/789.jpg"
}
]
}
],
"totalVnd": 585000,
"totalItemCount": 5
}
}
```
**Từng trường**
| Trường | Kiểu | Có thể null | Nguồn | Ghi chú |
|---|---|---|---|---|
| `cartId` | string | ❌ | `cart.id` | — |
| `status` | enum | ❌ | `cart.status` | `active`/`converted`/`abandoned` — SCR-04 chỉ hiển thị khi `active` |
| `sellers[].sellerId` | string | ❌ | `cart_item.seller_id` | — |
| `sellers[].sellerName` | string | ❌ | Module Seller Management (tham chiếu) | 🔴 **BA đề xuất BE trả kèm tên đã join sẵn** — cần BE xác nhận, xem §5 mục 1 |
| `sellers[].subtotalVnd` | number | ❌ | Tính từ `items[]` | 🔴 **BA đề xuất BE tính sẵn** (không để FE tự cộng) — tránh sai lệch làm tròn giữa FE/BE, xem §5 mục 2 |
| `items[].unitPriceVnd` | number | ❌ | `cart_item.unit_price_snapshot` hoặc giá real-time | 🔴 Phụ thuộc `OQ-029` (chưa chốt ở `SRS`) |
| `items[].quantity` | number | ❌ | `cart_item.quantity` | Số nguyên |
| `totalVnd` | number | ❌ | Σ `sellers[].subtotalVnd` | — |
| `totalItemCount` | number | ❌ | Σ `items[].quantity` toàn giỏ | Dùng cho C01 |
**Response lỗi**
| HTTP | `code` | Khi nào | Mã lỗi SRS |
|---|---|---|---|
| 401 | `ERR_AUTH_INVALID_TOKEN` | Customer token hết hạn/không hợp lệ | Điều hướng đăng nhập (`AC-US002-06`), không có mã `E-CART` riêng |
| 500 / 503 | `ERR_INTERNAL` / `ERR_SERVICE_UNAVAILABLE` | Lỗi hệ thống/timeout | `E-CART-0004` |
---
### 3.2 `PATCH /v1/cart/items/{cartItemId}` — Cập nhật số lượng
| | |
|---|---|
| **Mục đích** | Cập nhật `quantity` của một `CartItem` (US-003, field F01) |
| **Màn hình** | SCR-04 |
| **Quyền** | Chủ sở hữu `CartItem` (ownership theo `customerId`/`sellerId` trong JWT, hoặc `session_id` cho Guest — cơ chế Guest chưa xác nhận, `OQ-032`) |
**Request body** *(⚠️ đề xuất)*
```json
{ "quantity": 3 }
```
| Trường | Kiểu | Bắt buộc | Ràng buộc | Field SRS |
|---|---|---|---|---|
| `quantity` | number | ✅ | Số nguyên ≥ 1; ngưỡng trên 🔴 chưa chốt (`OQ-030`) | F01 |
🔴 **Ràng buộc ở API phải khớp bảng field trong SRS** — giữ nguyên "≥ 1", không thêm ngưỡng
trên tự ý cho tới khi `OQ-030` được trả lời.
**Response 200** *(⚠️ đề xuất — cần BE xác nhận trả về `CartItem` đã cập nhật hay toàn bộ
`Cart` mới, xem §5 mục 3)*
```json
{
"data": {
"cartItemId": "citem_001",
"quantity": 3,
"sellerSubtotalVnd": 375000,
"cartTotalVnd": 635000
}
}
```
**Response lỗi**
| HTTP | `code` | Khi nào | Mã lỗi SRS |
|---|---|---|---|
| 400 | `ERR_VALIDATION` | `quantity` không phải số nguyên ≥ 1 | `E-CART-0002` |
| 403 | `ERR_FORBIDDEN_OWNERSHIP` | `cartItemId` không thuộc giỏ hàng của caller | `E-CART-0005` |
| 404 | `ERR_NOT_FOUND` | `cartItemId` không tồn tại (đã bị xoá) | `E-CART-0003` |
| 409 | `ERR_CONFLICT` | `cartItemId` đã bị xoá bởi request khác trong lúc xử lý | `E-CART-0003` |
| 422 | `ERR_BUSINESS_RULE` | 🔴 Dự kiến — nếu `OQ-030` chốt có kiểm tra tồn kho tại đây | *(chưa có mã — chỉ tạo khi `OQ-030` xác nhận thuộc scope)* |
| 500 / 503 | `ERR_INTERNAL` / `ERR_SERVICE_UNAVAILABLE` | Lỗi hệ thống/timeout | `E-CART-0006` |
---
### 3.3 `DELETE /v1/cart/items/{cartItemId}` — Xoá sản phẩm khỏi giỏ
| | |
|---|---|
| **Mục đích** | Xoá một `CartItem` (US-003, nút C07, sau khi xác nhận modal) |
| **Màn hình** | SCR-04 |
| **Quyền** | Chủ sở hữu `CartItem` — cùng quy tắc §3.2 |
**Response 200/204** *(⚠️ đề xuất — 204 không có body; BA đề xuất 200 kèm `sellerSubtotalVnd`/
`cartTotalVnd` mới để FE khỏi tự tính lại, cần BE xác nhận, xem §5 mục 4)*
```json
{
"data": {
"deletedCartItemId": "citem_001",
"sellerSubtotalVnd": 260000,
"cartTotalVnd": 260000,
"sellerRemoved": false
}
}
```
**Response lỗi**
| HTTP | `code` | Khi nào | Mã lỗi SRS |
|---|---|---|---|
| 403 | `ERR_FORBIDDEN_OWNERSHIP` | Không thuộc quyền sở hữu | `E-CART-0005` |
| 404 | `ERR_NOT_FOUND` | Đã bị xoá trước đó | `E-CART-0003` |
| 409 | `ERR_CONFLICT` | Xung đột đồng thời | `E-CART-0003` |
| 500 / 503 | `ERR_INTERNAL` / `ERR_SERVICE_UNAVAILABLE` | Lỗi hệ thống/timeout | `E-CART-0006` |
---
## 4. Bảng đối chiếu mã lỗi
| Mã lỗi SRS | Endpoint | HTTP | `code` của API | ☐ Khớp |
|---|---|---|---|---|
| `E-CART-0002` | `PATCH /v1/cart/items/{id}` | 400 | `ERR_VALIDATION` | ☐ |
| `E-CART-0003` | `PATCH`, `DELETE /v1/cart/items/{id}` | 404/409 | `ERR_NOT_FOUND` / `ERR_CONFLICT` | ☐ |
| `E-CART-0004` | `GET /v1/cart` | 500/503 | `ERR_INTERNAL` / `ERR_SERVICE_UNAVAILABLE` | ☐ |
| `E-CART-0005` | `PATCH`, `DELETE /v1/cart/items/{id}` | 403 | `ERR_FORBIDDEN_OWNERSHIP` | ☐ |
| `E-CART-0006` | `PATCH`, `DELETE /v1/cart/items/{id}` | 500/503 | `ERR_INTERNAL` / `ERR_SERVICE_UNAVAILABLE` | ☐ |
*Cột ☐ do BE đánh dấu khi xác nhận khớp implementation thật.*
## 5. Điểm cần BE xác nhận
| # | Điểm cần chốt | Đề xuất của BA | BE trả lời | Ngày |
|---|---|---|---|---|
| 1 | `GET /v1/cart` trả `sellerName` đã join sẵn, hay chỉ `sellerId` để FE tự gọi API khác lấy tên? | Trả sẵn `sellerName` — tránh FE gọi thêm N request cho N seller | | |
| 2 | `sellers[].subtotalVnd`/`totalVnd` do BE tính sẵn, hay FE tự cộng từ `items[]`? | BE tính sẵn — tránh sai lệch làm tròn giữa FE/BE | | |
| 3 | `PATCH /v1/cart/items/{id}` trả về `CartItem` đã cập nhật + tổng nhóm/tổng giỏ, hay toàn bộ `Cart` mới? | Trả `CartItem` + tổng nhóm/tổng giỏ liên quan (nhẹ hơn trả toàn bộ `Cart`) | | |
| 4 | `DELETE /v1/cart/items/{id}` trả `200` kèm body hay `204` rỗng? | `200` kèm tổng nhóm/tổng giỏ mới + cờ `sellerRemoved` (để FE biết có cần ẩn cả nhóm không) | | |
| 5 | `id` (`cartItemId`, `productVariantId`, `sellerId`) dạng `string` hay `number`? | `string` | | |
| 6 | Với Guest, ownership của `cartItemId` có được đối chiếu theo `session_id` giống cơ chế `403 ERR_FORBIDDEN_OWNERSHIP` của JWT không? (`OQ-032`, đã ghi ở `SRS`) | Áp dụng cùng cơ chế | | |
| 7 | `PATCH /v1/cart/items/{id}` có kiểm tra tồn kho (trả `422 ERR_BUSINESS_RULE`) không? Phụ thuộc `OQ-030` ở `SRS` | Không kiểm tra ở endpoint này (giữ đúng khoanh vùng `BACKLOG`) — chờ PO xác nhận | | |
## 6. Hành vi khi API lỗi *(giao diện phải làm gì)*
| Tình huống | Giao diện làm gì | AC |
|---|---|---|
| 401 hết phiên (Customer) | Chuyển về đăng nhập, giữ đường dẫn quay lại `/cart` | `AC-US002-06` |
| 403 (ownership) | Chuyển trang "Không có quyền", không hiển thị dữ liệu | `AC-US003-12`, `13` |
| 404/409 (item đã bị xoá) | Banner tại dòng, sau đó ẩn dòng, cập nhật lại tổng | `AC-US003-11` |
| 5xx / timeout khi tải (`GET`) | Banner C12 thay chỗ danh sách, nút "Thử lại" | `AC-US002-03` |
| 5xx / timeout khi sửa/xoá (`PATCH`/`DELETE`) | Banner tại dòng, **giữ nguyên dữ liệu trước đó**, mở lại nút | `AC-US003-09`, `10` |
| Đang gửi `PATCH`/`DELETE` | Khoá F01/C07 của đúng dòng đang xử lý (không khoá toàn trang) để tránh gửi trùng | — |
🔴 **Khoá đúng nút/dòng đang xử lý, không khoá toàn màn hình** — SCR-04 thường có nhiều dòng
`CartItem` cùng lúc; khoá toàn trang khi chỉ một dòng đang cập nhật sẽ chặn nhầm thao tác trên
các dòng khác.
## 7. Open Questions
| ID | Câu hỏi | Hỏi ai | Từ ngày | Chặn gì |
|---|---|---|---|---|
| OQ-032 | Cơ chế ownership check của Guest cho `PATCH`/`DELETE /v1/cart/items/{id}` (xem `SRS` §8) | Tech Lead | 2026-09-08 | §5 mục 6, `E-CART-0005` cho Guest |
| OQ-030 | `PATCH /v1/cart/items/{id}` có kiểm tra tồn kho (`422 ERR_BUSINESS_RULE`) không (xem `SRS` §8) | PO, Tech Lead | 2026-09-08 | §3.2, §5 mục 7 |

View File

@@ -0,0 +1,124 @@
# NFR — Yêu cầu phi chức năng — US-002, US-003 (SCR-04 Giỏ hàng)
| | |
|---|---|
| **Version** | 1.0 |
| **Date** | 2026-09-08 |
| **Author** | BA (qua skill ba-3-specification) |
| **Status** | 🟡 Draft |
| **Approved by** | Tech Lead: — |
| **Source** | `SRS_US002-003_v1.0.md` · `e-commerce/docs/sections/02-phan-tich-yeu-cau.md` (NFR-01…08) · `e-commerce/docs/sections/04-api-design.md` §4.1.1, §4.2 (ownership, rate limit, Guest session) · `RBAC_CartCheckout_v1.0.md` §5 |
| **Scope** | US-002, US-003 — màn hình `SCR-04` Giỏ hàng |
---
## Quy tắc
Mỗi NFR bắt buộc ba thứ: **con số đo được** · **điều kiện đo** · **cách verify**. Con số chưa
có nguồn ⇒ ghi 🔴 + `OQ-nnn`, không tự đặt.
---
## 1. Hiệu năng — `NFR-PERF-nn`
| ID | Yêu cầu | Ngưỡng | Điều kiện đo | Cách verify | Ai chốt |
|---|---|---|---|---|---|
| NFR-PERF-01 | Thời gian tải giỏ hàng (`GET /v1/cart`) | 🔴 chưa chốt riêng cho US-002/003 — SAD NFR-01 chỉ có ngưỡng cho danh mục/tìm kiếm (<2s) và checkout (<3s); BA đề xuất áp cùng ngưỡng danh mục (≤2s p95) vì cùng là thao tác đọc — xem `OQ-033` | 🔴 chưa xác nhận số `CartItem` tối đa/giỏ để đo (đề xuất ≤50 dòng, chưa có số liệu vận hành — `greenfield`) | k6 kịch bản tải `GET /v1/cart` | Tech Lech (chờ xác nhận) |
| NFR-PERF-02 | Thời gian phản hồi sửa số lượng / xoá (`PATCH`/`DELETE /v1/cart/items`) | 🔴 chưa chốt — BA đề xuất ≤1s p95 (thao tác ghi 1 dòng, đơn giản hơn checkout) — xem `OQ-033` | 1 người dùng, 1 request | k6 kịch bản `PATCH`/`DELETE` đơn lẻ | Tech Lead (chờ xác nhận) |
🔴 **Không tự đặt số khi PO/Tech Lead chưa xác nhận** — hai dòng trên chỉ là **đề xuất** của BA
dựa trên phép loại suy từ NFR-01 của SAD, không phải giá trị đã chốt. Xem `OQ-033`.
## 2. Dung lượng & tăng trưởng — `NFR-CAP-nn`
| Đại lượng | Hiện tại | Sau 1 năm | Sau 3 năm | Nguồn số liệu |
|---|---|---|---|---|
| Số `CartItem` trung bình/tối đa mỗi giỏ hàng | N/A — `greenfield`, chưa vận hành | 🔴 chưa có số liệu | 🔴 chưa có số liệu | Chưa có — cần đo sau go-live (không phải việc của GĐ3) |
| Số giỏ hàng đồng thời (giờ cao điểm) | N/A | 🔴 chưa có | 🔴 chưa có | Tham chiếu `BRIEF_CartCheckout_v1.0.md` §2 quy mô "large" — chưa có con số cụ thể cho riêng module Giỏ hàng |
**N/A theo `LIFECYCLE = greenfield`** — không có dữ liệu vận hành cũ để ước lượng tăng trưởng
(nhất quán với `IMPACT_CartCheckout_v1.0.md` §1.6). Ghi nhận, không bỏ trống im lặng.
| Giờ cao điểm | Khi nào | Vì sao |
|---|---|---|
| 🔴 chưa xác nhận | *(đề xuất tham khảo: giờ tối, cuối tuần, mùa sale — theo mô hình marketplace bán lẻ thông thường, chưa có số liệu thật)* | Chưa vận hành để có dữ liệu thật |
## 3. Bảo mật & quyền riêng tư — `NFR-SEC-nn`
| ID | Yêu cầu | Chi tiết | Căn cứ | Cách verify |
|---|---|---|---|---|
| NFR-SEC-01 | Mọi `PATCH`/`DELETE` cart item kiểm tra quyền sở hữu (chống IDOR) | Đối chiếu `customerId`/`sellerId` trong JWT (Customer) hoặc `session_id` (Guest — cơ chế cụ thể chờ `OQ-032`) với chủ sở hữu `CartItem`; không khớp → `403 ERR_FORBIDDEN_OWNERSHIP` | `e-commerce/docs/sections/04-api-design.md` §4.1.1 | Test gọi thẳng API với token/session không phải chủ sở hữu — `AC-US003-12`, `13` |
| NFR-SEC-02 | Định danh Guest (`X-Guest-Session-Id`) không lộ qua kênh không an toàn | Cookie `HttpOnly; Secure; SameSite=Lax` (theo SAD §4.1.1) — SRS này chỉ tham chiếu, không định nghĩa lại thiết kế kỹ thuật | `e-commerce/docs/sections/04-api-design.md` §4.1.1 | Kiểm tra thuộc tính cookie trên môi trường staging |
| NFR-SEC-03 | Giới hạn tần suất gọi (rate limit) cho `PATCH`/`DELETE /v1/cart/items` của Guest | Theo IP nguồn, ngưỡng cụ thể do SAD định nghĩa ở tầng kỹ thuật (không phải quyết định của SRS này) | `e-commerce/docs/sections/04-api-design.md` §4.2 | Tham chiếu, không kiểm thử ở phạm vi BA |
**Rà bốn câu:**
| Câu hỏi | Trả lời |
|---|---|
| Có thông tin cá nhân không? Loại nào? | Không — `CartItem` chỉ chứa `product_variant_id`, `seller_id`, `quantity`; không có PII của khách trong phạm vi US-002/US-003 |
| Lưu bao lâu? Xoá thế nào khi hết hạn? | Theo vòng đời `Cart` — `BR_CartCheckout_v1.0.md` §3 (Guest 7 ngày, Customer 30 ngày theo `05-thiet-ke-du-lieu.md` §5.3, **chưa được Tech Lead xác nhận áp dụng đúng số này cho module**, xem `BACKLOG` US-001) |
| Ai được xuất ra ngoài hệ thống? | Không áp dụng — không có chức năng xuất dữ liệu giỏ hàng trong US-002/US-003 |
| Có quy định pháp luật nào áp dụng không? | Không trực tiếp — dữ liệu giỏ hàng chưa phải giao dịch tài chính (khác `Order`/`Payment`, thuộc NFR-05) |
## 4. Lưu vết — `NFR-AUD-nn`
| Nhóm | N/A vì |
|---|---|
| Lưu vết (audit log) cho sửa/xoá `CartItem` | Đây là thao tác tự phục vụ trên dữ liệu **tạm thời** trước khi đặt hàng, không phải giao dịch tài chính — theo `RBAC_CartCheckout_v1.0.md` §5, yêu cầu ghi vết chỉ bắt buộc từ bước tạo `Order`/`Payment` trở đi (US-004 trở lên). Sửa/xoá `CartItem` không cần audit log riêng ở US-002/US-003 |
## 5. Khả dụng & xử lý sự cố — `NFR-AVL-nn`
| ID | Yêu cầu | Ngưỡng | Ghi chú |
|---|---|---|---|
| NFR-AVL-01 | Hành vi khi Cart & Order Service lỗi/chậm khi tải giỏ hàng | Banner lỗi + nút "Thử lại" (không tự động retry lặp lại) | Đã có AC — `AC-US002-03` |
| NFR-AVL-02 | Hành vi khi Cart & Order Service lỗi/chậm khi sửa/xoá | Banner lỗi tại dòng, giữ nguyên dữ liệu trước đó, mở lại nút | Đã có AC — `AC-US003-09`, `10` |
**Hệ thống ngoài lỗi thì nghiệp vụ này làm gì?**
| Hệ thống ngoài | Nếu lỗi/chậm | Người dùng thấy gì | Dữ liệu xử lý sao |
|---|---|---|---|
| Catalog & Inventory (nội bộ — cung cấp tên/ảnh/biến thể sản phẩm hiển thị ở C03–C06) | Chậm/không phản hồi khi tải giỏ hàng | 🔴 chưa xác nhận: chặn hoàn toàn tải giỏ hàng, hay hiển thị giỏ hàng với các trường thiếu (VD ảnh trống, tên "—")? | Chưa xác định — cần Tech Lead xác nhận SLA nội bộ giữa Cart & Order Service và Catalog Service (tương tự khoảng trống đã ghi ở `IMPACT_CartCheckout_v1.0.md` §1.5). Ghi nhận là khoảng trống, không tạo `OQ` mới trùng lặp — thuộc phạm vi `IMPACT` đã có |
## 6. Đa ngữ & định dạng — `NFR-I18N-nn`
| Khía cạnh | Yêu cầu | Ghi chú |
|---|---|---|
| Ngôn ngữ hỗ trợ | VI (mặc định), EN đầy đủ ở SRS §4.2; ZH/KO/JA để trống — `OQ-023` (đã mở ở `UICONV`) | Tham chiếu `UICONV` §9 |
| Múi giờ hiển thị | N/A — SCR-04 không hiển thị trường thời gian nào | — |
| Định dạng ngày | N/A — không áp dụng cho SCR-04 | — |
| Dấu phân cách số | Theo `UICONV` §8 (`#,##0 ₫`) | — |
| Đơn vị tiền | VND, không đổi tiền tệ giao dịch; giá quy đổi tham khảo (`CurrencyToggle`) — chưa xác nhận có hiển thị ở SCR-04 hay không, mức độ thấp, không chặn G3, ghi nhận cho Dev FE tham khảo `UICONV` §0 | — |
| Sắp xếp chuỗi có dấu | N/A — không có sắp xếp theo tên trên SCR-04 (chỉ nhóm theo seller, thứ tự nhóm — xem `SRS` §2.A.3.2) | — |
| Độ dài text sau khi dịch | Tên sản phẩm/seller dài hơn ở ZH/KO/JA so với VI — layout co giãn theo `UICONV` §0, không fix-width | Tham chiếu `UICONV` §0 |
## 7. Khả năng truy cập & thiết bị — `NFR-ACC-nn`
| Khía cạnh | Yêu cầu |
|---|---|
| Trình duyệt hỗ trợ | Tham chiếu `UICONV` — chưa có bảng ma trận trình duyệt riêng, N/A ở mức SRS này |
| Kích thước màn hình nhỏ nhất | Tham chiếu `UICONV` §1 — breakpoint mobile < 768px (giá trị cụ thể `OQ-018`, đã mở) |
| Dùng trên điện thoại không | Có — web responsive (theo SAD §7.0) |
| Thao tác bằng bàn phím | 🔴 chưa xác nhận thứ tự focus cho F01/C07 — mức độ thấp ở `RIGOR = standard` (chỉ bắt buộc đủ ở `strict`), không chặn G3, ghi nhận cho Dev FE |
| Tương phản màu | N/A ở mức BA — thuộc thiết kế visual (`OQ-022`, design system chưa chọn) |
---
## 8. Nhóm không áp dụng
| Nhóm | N/A vì |
|---|---|
| Lưu vết (`NFR-AUD`) | Xem §4 — thao tác tự phục vụ trên dữ liệu tạm thời, không phải giao dịch tài chính |
## 9. Open Questions
| ID | Câu hỏi | Hỏi ai | Từ ngày | Chặn gì |
|---|---|---|---|---|
| OQ-033 | Ngưỡng thời gian phản hồi (p95) cho `GET /v1/cart` và cho `PATCH`/`DELETE /v1/cart/items` là bao nhiêu giây? | PO, Tech Lead | 2026-09-08 | NFR-PERF-01, NFR-PERF-02 |
## 10. Xác nhận
| Vai trò | Người | Nội dung xác nhận | Ngày |
|---|---|---|---|
| Tech Lead | *(chưa có)* | ☐ Ngưỡng khả thi với kiến trúc hiện tại (chờ chốt số ở `OQ-033`) | — |
| QA | *(chưa có)* | ☐ Mọi NFR đều có cách verify chạy được | — |
| PO | *(chưa có)* | ☐ Ngưỡng khớp kỳ vọng người dùng đã trao đổi | — |

View File

@@ -0,0 +1,379 @@
# SRS — US-002, US-003 — Xem giỏ hàng nhóm theo seller · Sửa số lượng / xoá sản phẩm trong giỏ
| | |
|---|---|
| **Version** | 1.0 |
| **Date** | 2026-09-08 |
| **Author** | BA (qua skill ba-3-specification) |
| **Status** | 🟡 Draft |
| **Approved by** | PO: — · Tech Lead: — · QA: — · Designer: — *(dự án không có Designer riêng — PO ký thay, `DEC-01`)* |
| **Source** | `02-analysis/BACKLOG_CartCheckout_v1.0.md` (US-002, US-003) · `02-analysis/BR_CartCheckout_v1.0.md` (§3 vòng đời `CART`, §4 ERD) · `02-analysis/RBAC_CartCheckout_v1.0.md` (§1, §2, §6) · `02-analysis/IMPACT_CartCheckout_v1.0.md` · `00-index/UICONV_e-commerce_v1.0.md` v1.1 · `e-commerce/docs/sections/07-giao-dien.md` §7.1.1 SCR-04 (prototype tham chiếu dạng văn bản, chế độ 🎨, `DEC-01`) · `e-commerce/docs/sections/04-api-design.md` §4.1.1, §4.1.5, §4.1.13, §4.2 · `e-commerce/docs/sections/05-thiet-ke-du-lieu.md` §5.2.3 (tham chiếu entity `cart`/`cart_item`, không chép schema vật lý) |
| **Scope** | US-002 (Xem giỏ hàng nhóm theo seller), US-003 (Sửa số lượng / xoá sản phẩm trong giỏ) — cùng màn hình `SCR-04` Giỏ hàng |
| **Profile** | `screen · greenfield · standard` (`00-index/PROFILE_e-commerce.md`) |
| **Biến thể PART 2** | `srs-part2/screen.md` (2.A Màn hình) + 2.B API (US-002/US-003 gọi trực tiếp API giỏ hàng để hiển thị/cập nhật) |
| **Ngôn ngữ hiển thị** | VI + EN (đầy đủ ở §4.2) · ZH/KO/JA để trống — chờ `OQ-023` (cần biên dịch chuyên nghiệp, không dịch máy — ghi chú người duyệt) |
## Change Log
| Version | Date | Người sửa | Thay đổi | CR |
|---|---|---|---|---|
| 1.0 | 2026-09-08 | BA (qua skill ba-3-specification) | Bản đầu — SRS chung US-002 + US-003, PART 2.A (màn hình SCR-04) + 2.B (API), 6 mã lỗi mới (`E-CART-0002`…`0006`), 6 `OQ` mới (`OQ-028`…`OQ-033`) | — |
> ⚠️ **Ngoại lệ gate** — tài liệu này được viết trong khi Gate G2 (Solution sign-off) của module Giỏ hàng & Checkout **chưa được PO + Tech Lech ký chính thức** (xem `DEC-02`, `DEC-05`, `00-index/DECISION_e-commerce.md`). Ngoại lệ đã được người điều phối xác nhận rõ ràng — xem `DEC-06`. Nếu `BACKLOG_CartCheckout`/`BR_CartCheckout` đổi khi G2 ký thật, phần bị ảnh hưởng nhiều nhất ở đây là §1.3 (BR áp dụng — hiện chưa có `BR-CART-nn` nào áp dụng trực tiếp) và §1.4 (vòng đời `CART`).
> **Quy ước đọc tài liệu này** *(quy tắc W11)*. Bảng thành phần ở §2.A.3.1 quyết định **một phần tử có tồn tại hay không** và **nó hành xử thế nào**. `WF_US002-003_v1.0.md` + `.html` (và prototype SAD §7.1.1 nó tham chiếu) quyết định **nó nằm ở đâu, to bằng nào, trông ra sao ở từng trạng thái**. `UICONV_e-commerce_v1.0.md` quyết định **quy ước chung** — tài liệu này chỉ ghi phần khác. Khi mâu thuẫn: bảng thắng về sự tồn tại và hành vi, WF thắng về bố cục — mâu thuẫn đó có dòng ở `WF` §5.
---
## PHẦN 1 — NGHIỆP VỤ
### 1.1 Tóm tắt cho người quyết định
| | |
|---|---|
| **US này giải quyết** | RQ-001: Giỏ hàng đa seller, một lần checkout. US-002 cho khách nhìn thấy rõ mình đang mua sản phẩm gì của những seller nào và tổng tiền từng seller/toàn giỏ trước khi quyết định đặt hàng; US-003 cho khách tự sửa số lượng hoặc bỏ bớt sản phẩm ngay tại màn hình này mà không phải rời trang |
| **Người dùng được gì** | Nhìn một màn hình duy nhất biết chính xác đang mua của ai, số tiền mỗi seller và tổng cộng; tự điều chỉnh giỏ hàng (số lượng, xoá dòng) và thấy tổng tiền cập nhật ngay, không cần tải lại trang |
| **Khác hiện tại chỗ nào** | N/A — dự án `greenfield` (xem `PROFILE_e-commerce.md` §Hệ quả đã áp dụng), không có màn hình giỏ hàng cũ để so sánh; đây là màn hình mới hoàn toàn của module Giỏ hàng & Checkout |
### 1.2 Vai trò và quyền
| Vai trò | Được làm gì trong US này | Không được làm gì | RBAC |
|---|---|---|---|
| ROLE-01 Guest | Xem giỏ hàng của chính mình (theo `X-Guest-Session-Id`); sửa số lượng / xoá `CartItem` của chính mình | Xem/sửa giỏ hàng của người khác; không có sổ địa chỉ (không liên quan ở US này) | `RBAC_CartCheckout_v1.0.md` §2 |
| ROLE-02 Customer | Xem giỏ hàng của chính mình (theo `customer_id`); sửa số lượng / xoá `CartItem` của chính mình | Xem/sửa giỏ hàng của người khác | `RBAC_CartCheckout_v1.0.md` §2 |
### 1.3 Business rule áp dụng
| BR | Phát biểu ngắn | Áp dụng ở màn hình/field nào | Khi vi phạm |
|---|---|---|---|
| *(không có)* | Không có `BR-CART-nn` nào áp dụng trực tiếp cho US-002/US-003 — đúng theo `BACKLOG_CartCheckout_v1.0.md` (US-002: "BR áp dụng: chưa có — mục hiển thị thuần tuý"; US-003: "BR áp dụng: chưa có — ràng buộc số lượng tối thiểu/tối đa mỗi dòng chưa được chốt") | — | — |
| BR §3 `CART` | Chỉ được sửa số lượng / xoá `CartItem` khi `Cart.status = active` (nguồn: `BR_CartCheckout_v1.0.md` §3, bảng "Chuyển trạng thái KHÔNG được phép") | Toàn màn `SCR-04` | Chặn thao tác — xem §1.4 |
🔴 **Không có BR nào định nghĩa giới hạn trên của số lượng (kiểm tra tồn kho) hay hành vi khi số lượng về 0** — đây là khoảng trống thật của `BACKLOG`/`BR` ở GĐ2, không phải BA quên chép. Xem `OQ-028`, `OQ-030`.
### 1.4 Vòng đời trạng thái *(tham chiếu — không định nghĩa lại)*
*Vòng đời đầy đủ của `CART` ở `BR_CartCheckout_v1.0.md` §3. Ở đây chỉ nêu phần US-002/US-003 chạm tới.*
| Nguồn | Sự kiện | Điều kiện | Đích | Ai được làm | BR |
|---|---|---|---|---|---|
| Active | Sửa số lượng `CartItem` | `Cart.status = active` | Active (không đổi) | Guest/Customer chủ giỏ | BR §3 `CART` |
| Active | Xoá `CartItem` (kể cả dòng cuối cùng của giỏ) | `Cart.status = active` | Active (không đổi — 🔴 giỏ hàng **không** tự chuyển `abandoned` khi rỗng, chỉ chuyển khi hết hạn phiên theo BR §3) | Guest/Customer chủ giỏ | BR §3 `CART` |
**Chuyển trạng thái KHÔNG được phép ở US-002/US-003:** không có thao tác nào trong hai US này làm `Cart.status` chuyển sang `converted`/`abandoned` — hai chuyển đổi đó thuộc US-004 (checkout) và cơ chế hết hạn phiên tự động (ngoài phạm vi SRS này).
### 1.5 Ngoài phạm vi *(quy tắc W12)*
| # | Không làm gì | Vì sao | Xử lý ở đâu/khi nào |
|---|---|---|---|
| 1 | Kiểm tra tồn kho thời điểm thực khi xem/sửa giỏ hàng | `BACKLOG_CartCheckout_v1.0.md` US-003 khoanh phạm vi: "cảnh báo hết hàng theo thời gian thực thuộc US-004 tại thời điểm checkout, không phải tại thời điểm xem/sửa giỏ — trừ khi PO quyết khác" | US-004 (checkout); sẽ được cập nhật nếu PO trả lời `OQ-030` theo hướng khác |
| 2 | Áp dụng khuyến mãi/mã giảm giá/điểm thưởng | Điểm tích hợp với module Khuyến mãi & Loyalty (`DEC-03`) | US-004/US-005 (module khác gọi vào) |
| 3 | Chọn/bỏ chọn từng dòng hoặc từng nhóm seller để checkout một phần giỏ hàng (checkbox) | `SAD §7.1.1 SCR-04` mô tả thành phần này nhưng **không có `US` nào trong `BACKLOG` xác nhận** tính năng "checkout một phần giỏ hàng" | Chưa xác định — xem `OQ-031`, `WF_US002-003` §5 dòng #1. **Không tự thêm vào SRS này** |
| 4 | Hiển thị badge "Sản phẩm đã hết hàng" / "Giá đã thay đổi" tại màn Giỏ hàng | `SAD §7.1.1 SCR-04` mô tả nhưng phụ thuộc mục 1, 2 ở trên (nguồn dữ liệu tồn kho/giá real-time chưa xác nhận thuộc scope US-002/US-003) | Xem `OQ-029`, `OQ-030`, `WF_US002-003` §5 dòng #2, #3 |
| 5 | Tính phí vận chuyển | Chỉ có ở bước checkout, cần địa chỉ trước | US-005 |
| 6 | Thanh toán | Ngoài phạm vi màn hình Giỏ hàng | US-007/US-008 |
| 7 | Điều hướng từ dòng `CartItem` (ảnh/tên) về trang chi tiết sản phẩm | Không được `BACKLOG`/`BR` xác nhận là hành vi bắt buộc của US-002/US-003 | Bổ sung qua `CR` sau nếu PO yêu cầu |
---
## PHẦN 2 — ĐẶC TẢ SẢN PHẨM
### 2.A — Màn hình (`srs-part2/screen.md`)
#### 2.A.1 Danh sách màn hình
| ID | Tên màn hình | Loại | Đường dẫn | Vai trò truy cập | Vào từ đâu | Ra đi đâu |
|---|---|---|---|---|---|---|
| SCR-04 | Giỏ hàng | Chi tiết (không phân trang) | `/cart` *(quy ước kỹ thuật, Dev FE xác nhận — không chặn G3)* | ROLE-01 Guest, ROLE-02 Customer | Icon giỏ hàng ở header toàn site (`SCR-01`, theo SAD §7.1.1) | `SCR-05` Checkout (US-004, ngoài phạm vi SRS này) · `SCR-01` (khi giỏ rỗng, nút "Tiếp tục mua sắm") |
#### 2.A.2 Sơ đồ điều hướng
```mermaid
flowchart LR
SCR01(["SCR-01 Trang chủ — icon giỏ hàng"]) -->|"bấm icon giỏ hàng"| SCR04["SCR-04 Giỏ hàng"]
SCR04 -->|"'Tiếp tục mua sắm' (giỏ rỗng, C10)"| SCR01
SCR04 -->|"'Tiến hành Checkout' (C11) — US-004, ngoài phạm vi SRS này"| SCR05[["SCR-05 Checkout — ngoài phạm vi"]]
SCR01 -.->|"vào URL /cart trực tiếp"| SCR04
```
*ID node khớp cột `ID` của bảng §2.A.1.*
| Câu hỏi | SCR-04 |
|---|---|
| Hai đường quay lại (nút back + breadcrumb)? Quay lại có giữ trạng thái danh sách? | Không áp dụng breadcrumb (SCR-04 không lồng trong danh mục); quay lại dùng logo/menu header (theo `UICONV` §2). Không có "trạng thái danh sách" (giỏ hàng không phân trang/lọc) để giữ |
| Vào bằng URL trực tiếp `/cart` khi chưa có `CartItem` nào / chưa có session Guest | Hệ thống tự tạo `Cart` mới rỗng (Guest) hoặc lấy `Cart` rỗng của `customer_id` (Customer) — hiển thị trạng thái rỗng §2.A.3.6, không phải lỗi 404 |
| Rời màn hình khi đang sửa dở | N/A — không có "form đang dở": mỗi lần sửa số lượng/xoá gửi ngay lập tức (không có nút Lưu tổng, xem §2.A.3.5 và ngoại lệ `UICONV` §12) |
#### 2.A.3 SCR-04 — Giỏ hàng
**Bố cục:** `WF_US002-003_v1.0.md` §3.1 · `WF_US002-003_v1.0.html#SCR-04` · prototype: 🎨 `e-commerce/docs/sections/07-giao-dien.md` §7.1.1 SCR-04. Hành vi xem bảng bên dưới, không suy từ hình (W11).
##### 2.A.3.1 Bảng thành phần
| ID | Tên thành phần | Loại | Nhãn (nguyên văn) | Placeholder / Hint | Hành vi & sự kiện | Điều kiện ẩn/khoá | BR |
|---|---|---|---|---|---|---|---|
| C01 | lbl_header_title | Nhãn | "Giỏ hàng của bạn ({n} sản phẩm)" | — | `{n}` = tổng số lượng (Σ quantity mọi `CartItem`), tĩnh, cập nhật khi Σ đổi | — | — |
| C02 | grp_seller_header | Nhãn nhóm | "{Tên gian hàng}" | — | Lặp lại theo từng `seller_id` có ≥1 `CartItem`; nguồn tên: `seller_id` → tên gian hàng (module Seller Management, tham chiếu) | ❌ ẩn nếu nhóm không còn `CartItem` nào (xem AC-US003-04) | — |
| C03 | img_cart_item | Ảnh | — | Ảnh dạng ô xám khi lỗi tải | Ảnh đại diện `product_variant_id` | — | — |
| C04 | lbl_product_name | Nhãn | Tên sản phẩm | — | Hiển thị tĩnh, không điều hướng (xem §1.5 dòng 7) | — | — |
| C05 | lbl_variant | Nhãn | "{Thuộc tính biến thể}" (VD "Size M, Đỏ") | — | Hiển thị tĩnh, nguồn `product_variant_id` (module Catalog, tham chiếu, không sở hữu) | — | — |
| C06 | lbl_unit_price | Nhãn | "{đơn giá} ₫" | — | Hiển thị tĩnh — nguồn giá trị: 🔴 chưa chốt snapshot hay real-time, xem `OQ-029` | — | — |
| F01 | input_quantity | Ô nhập số (bộ đếm +/-) | — | — | Xem bảng field §2.A.3.3 | 🔒 disable trong lúc đang gửi cập nhật (spinner cục bộ, xem §2.A.3.6) | — |
| C07 | btn_delete_item | Nút nguy hiểm | "Xoá" | — | Bấm → mở modal xác nhận (theo `UICONV` §5 — mặc định áp dụng, không có ngoại lệ ghi ở `UICONV` §12 cho dòng này) → xác nhận thì gọi `DELETE` | 🔒 disable trong lúc đang gửi | — |
| C08 | lbl_seller_subtotal | Nhãn | "Tạm tính: {tổng nhóm} ₫" | — | = Σ (`quantity` × giá hiển thị C06) của các `CartItem` cùng `seller_id`; cập nhật ngay khi F01/C07 thành công | — | — |
| C09 | lbl_cart_total | Nhãn | "Tổng cộng: {tổng giỏ} ₫" | — | = Σ tất cả C08 | — | — |
| C10 | btn_continue_shopping | Nút phụ | "Tiếp tục mua sắm" | — | Chỉ hiện ở trạng thái rỗng → điều hướng `SCR-01` | ❌ ẩn khi giỏ có ≥1 `CartItem` | — |
| C11 | btn_checkout | Nút chính | "Tiến hành Checkout" | — | Bấm → điều hướng `SCR-05` (US-004, ngoài phạm vi SRS này) | 🔒 disable khi giỏ có 0 `CartItem`. **Điều kiện khác (yêu cầu chọn dòng) chưa xác nhận thuộc scope — xem `OQ-031`, không áp dụng ở SRS này** | — |
| C12 | banner_error_load | Banner | — | — | Hiện khi `GET /v1/cart` lỗi/timeout, thay chỗ danh sách `CartItem` | ❌ ẩn khi tải thành công | — |
| C13 | btn_retry_load | Nút phụ | "Thử lại" | — | Bấm → gọi lại `GET /v1/cart` | Nằm trong C12 | — |
**Cột `Điều kiện ẩn/khoá` — quy tắc W7 áp dụng đúng ba giá trị `❌ ẩn` / `🔒 disable` / `👁 read-only`, không ghi "tuỳ trường hợp".**
##### 2.A.3.2 Bảng thuộc tính hiển thị mỗi dòng `CartItem`
*Không phải danh sách phân trang/sắp xếp (giỏ hàng hiển thị toàn bộ, không giới hạn số dòng mỗi trang) — bảng dưới đây thay cho §2.3.2 chuẩn để ghi nguồn dữ liệu từng thuộc tính hiển thị.*
| # | Thuộc tính | Nguồn dữ liệu | Định dạng | Xử lý khi rỗng/thiếu | Ghi chú |
|---|---|---|---|---|---|
| 1 | Ảnh | `product_variant_id` → ảnh đại diện (module Catalog) | Ảnh vuông | Ô xám mặc định (theo `UICONV`) | — |
| 2 | Tên sản phẩm | `product_variant_id` → tên (module Catalog) | Text, không giới hạn dòng xác nhận — 🔴 chưa chốt số dòng tối đa (không blocking G3, mức độ thấp, Dev FE tự quyết theo CSS) | — | — |
| 3 | Biến thể | `product_variant_id` → thuộc tính | Text | `—` nếu sản phẩm không có biến thể | — |
| 4 | Đơn giá | `cart_item.unit_price_snapshot` **hoặc** giá hiện tại real-time — 🔴 `OQ-029` | `#,##0 ₫` (`UICONV` §8) | N/A | — |
| 5 | Số lượng | `cart_item.quantity` | Số nguyên | N/A (luôn ≥1 khi dòng còn tồn tại) | Xem F01 |
**Thứ tự hiển thị nhóm seller và `CartItem` trong mỗi nhóm:** 🔴 chưa được `BACKLOG`/SAD xác nhận. BA đề xuất: theo thời điểm thêm vào giỏ tăng dần (`added_at`/tương đương `cart_item` không có cột này trong ERD GĐ2 hiện tại — cần Tech Lech xác nhận có cột thời gian thêm hay dùng thứ tự trả về mặc định của API). Mức độ thấp, không chặn G3 — ghi nhận ở `WF_US002-003` §5 dòng #6.
##### 2.A.3.3 Bảng field
| ID | Tên field | Kiểu | Bắt buộc | Độ dài / Khoảng | Default | Nguồn giá trị | Validation | Message khi sai | BR |
|---|---|---|---|---|---|---|---|---|---|
| F01 | Số lượng (`quantity`) | Số nguyên (bộ đếm +/- và nhập trực tiếp) | ✅ | Tối thiểu 1 · tối đa 🔴 **chưa chốt — xem `OQ-030`** (BA không tự đặt số, vì đây là ràng buộc tồn kho, quyết định nghiệp vụ) | Giá trị `quantity` hiện có của `CartItem` khi mở màn hình | `cart_item.quantity` (giá trị hiện tại) | Số nguyên ≥ 1 | "Số lượng phải là số nguyên từ 1 trở lên." (`E-CART-0002`) | *(không có BR — xem §1.3)* |
🔴 **Hành vi khi giảm số lượng xuống 0 chưa chốt** (bấm nút `-` khi `quantity = 1`, hoặc nhập trực tiếp `0`): tự động mở modal xoá dòng, hay chặn không cho giảm dưới 1? — xem `OQ-028`. AC liên quan viết ở trạng thái mở, xem `AC_US-003_v1.0.md` §3 Nhóm 2.
##### 2.A.3.4 Sơ đồ luồng — Sửa số lượng / Xoá `CartItem`
```mermaid
sequenceDiagram
autonumber
actor U as Customer/Guest
participant FE as Giao diện SCR-04
participant BE as Cart & Order Service
U->>FE: Đổi số lượng (F01) hoặc bấm Xoá (C07, đã xác nhận modal)
FE->>FE: Validate phía giao diện (số nguyên ≥ 1)
FE->>BE: PATCH /v1/cart/items/{cartItemId} hoặc DELETE /v1/cart/items/{cartItemId} (khoá F01/C07 đang xử lý)
alt Thành công
BE-->>FE: 200 OK (dữ liệu giỏ hàng cập nhật)
FE-->>U: Cập nhật C08/C09, mở lại F01/C07
else CartItem không còn tồn tại trong giỏ (404/409 — bị xoá ở nơi khác)
BE--xFE: 404 ERR_NOT_FOUND / 409 ERR_CONFLICT
FE-->>U: Banner "Sản phẩm này đã được xoá khỏi giỏ hàng trước đó." (E-CART-0003), ẩn dòng, cập nhật C08/C09
else Không đủ quyền sở hữu (403)
BE--xFE: 403 ERR_FORBIDDEN_OWNERSHIP
FE-->>U: Chuyển trang "Không có quyền" (E-CART-0005)
else Timeout / lỗi hệ thống (5xx)
BE--xFE: timeout / 5xx
FE-->>U: Banner lỗi tại dòng CartItem "Không thể cập nhật giỏ hàng, thử lại." (E-CART-0006), GIỮ NGUYÊN giá trị trước đó, mở lại F01/C07
end
```
🔴 **Bắt buộc vẽ cả nhánh lỗi** (`alt`/`else`) — đã vẽ đủ 4 nhánh (thành công, xung đột, phân quyền, hệ thống).
**Bảng đi kèm:**
| Bước | Mô tả | Timeout | Thất bại thì sao | Mã lỗi | AC |
|---|---|---|---|---|---|
| 3 | Gửi `PATCH`/`DELETE` cart item | 🔴 chưa chốt — xem `NFR_US002-003_v1.0.md` §1, `OQ-033` | Giữ giá trị/dòng cũ, mở lại nút | `E-CART-0002`/`0003`/`0005`/`0006` (tuỳ nhánh) | `AC-US003-07`…`13` |
##### 2.A.3.5 Hành động trên màn hình
| Hành động | Điều kiện được phép | Xác nhận trước khi làm | Kết quả thành công | Kết quả thất bại | Vai trò | AC |
|---|---|---|---|---|---|---|
| Xem giỏ hàng (tải trang) | — | Không | Hiển thị đủ nhóm/dòng/C08/C09 | Banner lỗi (C12) + Thử lại (C13) | Guest, Customer | `AC-US002-01`…`04` |
| Sửa số lượng (F01) | `Cart.status = active`; giá trị mới là số nguyên ≥ 1 | Không | Cập nhật F01, C08, C09 ngay | Hiện lỗi theo bảng mã lỗi §4.1, **giữ giá trị trước đó** | Guest, Customer | `AC-US003-01`, `02`, `07`…`13` |
| Xoá `CartItem` (C07) | `Cart.status = active` | ✅ Modal xác nhận (`UICONV` §5) | Xoá dòng, cập nhật C08/C09; nhóm rỗng thì ẩn nhóm (C02); giỏ rỗng thì chuyển trạng thái rỗng | Hiện lỗi theo bảng mã lỗi §4.1, **giữ nguyên dòng** | Guest, Customer | `AC-US003-03`…`06`, `10`, `11` |
| Bấm "Tiến hành Checkout" (C11) | Giỏ có ≥1 `CartItem` | Không | Điều hướng `SCR-05` (ngoài phạm vi SRS này) | N/A | Guest, Customer | Ngoài phạm vi |
🔴 **Thất bại phải giữ nguyên dữ liệu người dùng đã nhập** — áp dụng cho F01 (giữ số lượng cũ khi lỗi) và cho C07 (dòng không biến mất khi `DELETE` lỗi).
##### 2.A.3.6 Trạng thái rỗng, đang tải, lỗi
| Trạng thái | Hiển thị gì | Text nguyên văn | Nút hành động |
|---|---|---|---|
| Đang tải lần đầu | Skeleton theo `UICONV` §6 | — | — |
| **Chưa có dữ liệu nào (giỏ trống)** | Minh hoạ + câu dẫn + nút chính. 🔴 **Ngoại lệ so với mẫu chung `UICONV` §6** ("Chưa có \<đối tượng\> nào.") — dùng nguyên văn theo SAD, đã ghi ở `UICONV` §12 | "Giỏ hàng trống" (khoá `cart.empty.title`) | C10 "Tiếp tục mua sắm" |
| Bộ lọc không khớp | N/A — giỏ hàng không có bộ lọc | — | — |
| Lỗi tải dữ liệu (`GET /v1/cart`) | Banner C12 thay chỗ danh sách | "Không tải được dữ liệu, thử lại." (`error.load`, theo `UICONV` §6 chung) | C13 "Thử lại" |
| Đang xử lý sửa/xoá 1 dòng | Spinner cục bộ tại dòng đó (F01/C07 disable), theo `UICONV` §6 "đang tải cục bộ" | — | — |
### 2.B — API tiêu thụ *(vì SCR-04 gọi trực tiếp API giỏ hàng)*
*Chi tiết đầy đủ ở `API_US002-003_v1.0.md`. Ở đây chỉ mô tả **góc nhìn người tiêu thụ (FE)**, không lặp lại hợp đồng.*
| Endpoint | Dùng để | Gọi khi nào | Nguồn contract |
|---|---|---|---|
| `GET /v1/cart` | Tải dữ liệu SCR-04 khi vào màn hình | Mở màn hình; bấm "Thử lại" (C13) | ✅ method/path — SAD §4.1.5 · ⚠️ schema response — BA đề xuất |
| `PATCH /v1/cart/items/{cartItemId}` | Cập nhật F01 | Mỗi lần đổi số lượng (bấm +/-, hoặc rời field sau khi gõ tay) | ✅ method/path — SAD §4.1.5 · ⚠️ schema request/response — BA đề xuất |
| `DELETE /v1/cart/items/{cartItemId}` | Xoá C07 (sau khi xác nhận modal) | Người dùng xác nhận trong modal | ✅ method/path — SAD §4.1.5 · ⚠️ schema response — BA đề xuất |
Guest dùng header `X-Guest-Session-Id` thay JWT cho cả ba endpoint (theo SAD §4.1.1, §4.1.5).
---
## PHẦN 3 — TIÊU CHÍ NGHIỆM THU
*Chi tiết đầy đủ, tách riêng theo US: [`AC_US-002_v1.0.md`](AC_US-002_v1.0.md) và [`AC_US-003_v1.0.md`](AC_US-003_v1.0.md).*
| AC | Nhóm | Tóm tắt | Field/Thành phần | BR | Test case (QA điền) |
|---|---|---|---|---|---|
| AC-US002-01 | Thành công | Xem giỏ hàng nhiều seller | C01–C11 | — | |
| AC-US002-02 | Thành công | Xem giỏ hàng rỗng | C10 | BR §3 | |
| AC-US002-03 | Lỗi hệ thống | Lỗi tải giỏ hàng | C12, C13 | — | |
| AC-US002-04 | Lỗi hệ thống | Phân biệt "chưa có dữ liệu" ↔ "không tải được" | — | — | |
| AC-US002-05 | Phân quyền | Guest chỉ thấy giỏ của session mình | — | RBAC §2 | |
| AC-US002-06 | Phân quyền | Customer hết phiên bị chuyển đăng nhập | — | — | |
| AC-US003-01…06 | Thành công | Tăng/giảm/xoá `CartItem`, xoá nhóm/giỏ, huỷ modal | F01, C07, C08, C09 | BR §3 | |
| AC-US003-07, 08 | Validation | Số lượng không hợp lệ; giảm về 0 (🔴 mở, `OQ-028`) | F01 | — | |
| AC-US003-09…11 | Lỗi hệ thống | 5xx cập nhật, timeout xoá, item bị xoá đồng thời | F01, C07 | — | |
| AC-US003-12, 13 | Phân quyền | Gọi API không đúng chủ sở hữu (Customer/Guest — Guest 🔴 mở, `OQ-032`) | — | RBAC §6 | |
**Mỗi US phải có đủ bốn nhóm** — đã đủ cho cả hai US, trừ hai AC còn mở chờ trả lời `OQ` (ghi rõ thay vì bịa).
---
## PHẦN 4 — MÃ LỖI VÀ TEXT HIỂN THỊ
### 4.1 Mã lỗi
| Mã | Khi nào xảy ra | Thông điệp hiển thị (nguyên văn) | Hiển thị ở đâu | Người dùng làm gì tiếp | BR/AC |
|---|---|---|---|---|---|
| `E-CART-0002` | Nhập số lượng không phải số nguyên ≥ 1 | "Số lượng phải là số nguyên từ 1 trở lên." | Inline dưới F01 | Sửa lại giá trị | `AC-US003-07` |
| `E-CART-0003` | `CartItem` không còn thuộc giỏ hàng (đã bị xoá ở nơi khác) khi cố sửa/xoá | "Sản phẩm này đã được xoá khỏi giỏ hàng trước đó." | Banner tại dòng (trước khi dòng biến mất) | Không cần làm gì — hệ thống tự cập nhật lại danh sách | `AC-US003-11` |
| `E-CART-0004` | `GET /v1/cart` lỗi/timeout | "Không tải được dữ liệu, thử lại." | Banner C12 thay chỗ danh sách | Bấm "Thử lại" (C13) | `AC-US002-03` |
| `E-CART-0005` | Gọi `PATCH`/`DELETE` cart item không thuộc quyền sở hữu (403) | "Bạn không có quyền truy cập nội dung này." | Trang trạng thái | Về trang chủ | `AC-US003-12`, `13` |
| `E-CART-0006` | Lỗi hệ thống (5xx) hoặc timeout khi `PATCH`/`DELETE` | "Không thể cập nhật giỏ hàng, thử lại." | Banner tại dòng | Bấm lại nút +/- hoặc Xoá | `AC-US003-09`, `10` |
Dãy mã theo `UICONV` §10: `E-CART-` tiếp từ `0002` (`0001` đã dùng cho `BR-CART-02` ở US-004). Sau SRS này, số kế tiếp chưa dùng là `0007` — đã cập nhật `UICONV` §10.
### 4.2 Text màn hình
| Khoá | Ngữ cảnh | VI | EN | KO/ZH/JA | Giới hạn ký tự |
|---|---|---|---|---|---|
| `cart.title` | Header C01 | "Giỏ hàng của bạn" | "Your Cart" | 🔴 để trống — `OQ-023` | 🔴 chưa chốt — `OQ-023` |
| `cart.itemCount` | Header C01 phụ | "({n} sản phẩm)" | "({n} items)" | 🔴 `OQ-023` | — |
| `cart.empty.title` | Trạng thái rỗng | "Giỏ hàng trống" | "Your cart is empty" | 🔴 `OQ-023` | — |
| `cart.empty.cta` | Nút C10 | "Tiếp tục mua sắm" | "Continue shopping" | 🔴 `OQ-023` | — |
| `cart.error.load` | Banner C12 | "Không tải được dữ liệu, thử lại." | "Failed to load data. Please try again." | 🔴 `OQ-023` | — |
| `btn.retry` | Nút C13 | "Thử lại" | "Try again" | 🔴 `OQ-023` | — |
| `btn.delete` | Nút C07 | "Xoá" | "Remove" | 🔴 `OQ-023` | — |
| `cart.delete.confirm.title` | Modal xác nhận xoá | "Xoá sản phẩm khỏi giỏ hàng?" | "Remove item from cart?" | 🔴 `OQ-023` | — |
| `cart.delete.confirm.body` | Modal xác nhận xoá | "Sản phẩm sẽ được xoá khỏi giỏ hàng của bạn." | "This item will be removed from your cart." | 🔴 `OQ-023` | — |
| `btn.checkout` | Nút C11 | "Tiến hành Checkout" | "Proceed to Checkout" | 🔴 `OQ-023` | — |
| `E-CART-0002.msg` | Inline F01 | "Số lượng phải là số nguyên từ 1 trở lên." | "Quantity must be a whole number of 1 or more." | 🔴 `OQ-023` | — |
| `E-CART-0003.msg` | Banner dòng | "Sản phẩm này đã được xoá khỏi giỏ hàng trước đó." | "This item has already been removed from your cart." | 🔴 `OQ-023` | — |
| `E-CART-0005.msg` | Trang trạng thái | "Bạn không có quyền truy cập nội dung này." | "You don't have permission to access this content." | 🔴 `OQ-023` | — |
| `E-CART-0006.msg` | Banner dòng | "Không thể cập nhật giỏ hàng, thử lại." | "Couldn't update your cart. Please try again." | 🔴 `OQ-023` | — |
*Bản dịch KO/ZH/JA để trống, ghi `OQ-023` (đã mở từ `UICONV`) — không dịch máy, theo ghi chú người duyệt.*
### 4.3 Định dạng hiển thị
*Tham chiếu `UICONV` §8, không có ngoại lệ cho SCR-04.*
| Loại dữ liệu | Định dạng | Ví dụ | Ghi chú |
|---|---|---|---|
| Số tiền | `#,##0 ₫` | 1.234.567 ₫ | Theo `UICONV` §8, không làm tròn thêm (VND) |
| Số lượng | Số nguyên, không định dạng | 3 | — |
| Rỗng/null | `—` | | Theo `UICONV` §8 |
---
## PHẦN 5 — PHI CHỨC NĂNG
*Chi tiết ở [`NFR_US002-003_v1.0.md`](NFR_US002-003_v1.0.md). Ở đây chỉ nêu áp dụng cho US-002/US-003.*
| ID | Nhóm | Yêu cầu (có số đo) | Điều kiện đo | Cách verify |
|---|---|---|---|---|
| NFR-PERF-01 | Hiệu năng | 🔴 Ngưỡng thời gian tải/cập nhật giỏ hàng chưa chốt riêng — xem `NFR` §1, `OQ-033` | — | — |
| NFR-SEC-01 | Bảo mật | Mọi `PATCH`/`DELETE` cart item kiểm tra quyền sở hữu (chống IDOR), trả `403 ERR_FORBIDDEN_OWNERSHIP` khi không khớp | Theo SAD §4.1.1 | Test gọi thẳng API với token/session khác chủ |
---
## PHẦN 6 — DỮ LIỆU VÀ TÍCH HỢP
### 6.1 API sử dụng
*Chi tiết ở [`API_US002-003_v1.0.md`](API_US002-003_v1.0.md).*
| # | Mục đích | Method | Endpoint | Nguồn contract |
|---|---|---|---|---|
| 1 | Tải giỏ hàng | GET | `/v1/cart` | ✅ method/path BE cung cấp · ⚠️ schema BA đề xuất |
| 2 | Sửa số lượng | PATCH | `/v1/cart/items/{cartItemId}` | ✅ method/path BE cung cấp · ⚠️ schema BA đề xuất |
| 3 | Xoá sản phẩm | DELETE | `/v1/cart/items/{cartItemId}` | ✅ method/path BE cung cấp · ⚠️ schema BA đề xuất |
### 6.2 Tác động dữ liệu
*Tham chiếu `IMPACT_CartCheckout_v1.0.md`.* Không có tác động dữ liệu cũ (`greenfield`). Điểm chạm nội bộ duy nhất liên quan trực tiếp US-002/US-003: module Catalog & Inventory cung cấp tên/ảnh/biến thể sản phẩm (`product_variant_id`) hiển thị ở C03–C06 — độ trễ đồng bộ giá/tồn kho vẫn là `ASM-06`/`OQ-009` (chưa xác minh), ảnh hưởng trực tiếp `OQ-029`/`OQ-030` ở SRS này.
---
## PHẦN 7 — BÀN GIAO CHO DEV
| | |
|---|---|
| **Trạng thái** | 🟡 Chưa sẵn sàng — còn 5 `OQ` chặn G3 (`OQ-028`…`OQ-030`, `OQ-032`, `OQ-033`) |
| **Tài liệu cần đọc kèm** | `UICONV_e-commerce_v1.0.md` v1.1 → `WF_US002-003_v1.0.md`+`.html` → `AC_US-002_v1.0.md` → `AC_US-003_v1.0.md` → `NFR_US002-003_v1.0.md` → `API_US002-003_v1.0.md` |
| **Quyết định đã chốt, không phải mặc định** | Modal xác nhận bắt buộc khi xoá `CartItem` (áp UICONV §5 mặc định, không có ngoại lệ) · Cart không tự chuyển `abandoned` khi rỗng · Checkout (C11) chỉ disable khi 0 `CartItem`, KHÔNG có điều kiện "phải chọn dòng" trong SRS này |
| **Điểm còn mở** | `OQ-028` (quantity=0) · `OQ-029` (nguồn giá) · `OQ-030` (giới hạn trên số lượng/tồn kho) · `OQ-031` (checkout một phần giỏ — chủ yếu ảnh hưởng US-004) · `OQ-032` (ownership Guest) · `OQ-033` (ngưỡng hiệu năng) |
| **Không được sao chép từ đâu** | Không có màn hình cũ (`greenfield`) — không áp dụng |
---
## PHẦN 8 — OPEN QUESTIONS
| ID | Câu hỏi | Hỏi ai | Từ ngày | Chặn gì | 🔴 chặn G3? | Phương án BA đề xuất |
|---|---|---|---|---|---|---|
| OQ-028 | Sửa số lượng `CartItem` về 0 (nhập tay hoặc bấm `-` khi đang ở 1) → hệ thống tự mở modal xoá dòng, hay chặn không cho giảm dưới 1? | PO | 2026-09-08 | F01, `AC-US003-08` | 🔴 | Chặn không cho `-` giảm dưới 1 (nút `-` disable ở `quantity=1`); xoá dòng chỉ qua nút "Xoá" (C07) riêng — tách rõ hai hành động, tránh xoá nhầm khi bấm liên tiếp nút `-` |
| OQ-029 | Giá hiển thị ở SCR-04 (C06) là giá snapshot lúc thêm giỏ (`cart_item.unit_price_snapshot`) hay giá hiện tại real-time từ Catalog? | PO, Tech Lead | 2026-09-08 | C06, `WF_US002-003` §5 #2/#3 | 🔴 | Dùng `unit_price_snapshot` để hiển thị nhất quán với số tiền đã "chốt" khi thêm giỏ, đối chiếu lại giá thật ở bước checkout (US-004) — tránh giỏ hàng nhảy số liên tục do giá đổi ngoài ý muốn khách |
| OQ-030 | Sửa số lượng ở SCR-04 (US-003) có kiểm tra giới hạn trên theo tồn kho hiện tại ngay lúc sửa không, hay chỉ kiểm tra ở checkout (US-004) như `BACKLOG` đã khoanh phạm vi? | PO, Tech Lead | 2026-09-08 | F01, C06 (badge hết hàng/giá đổi), `WF_US002-003` §5 #2/#4 | 🔴 | Giữ đúng khoanh vùng của `BACKLOG`: không kiểm tra tồn kho ở màn Giỏ hàng, chỉ kiểm tra ở US-004 — đơn giản hoá US-002/US-003, tránh phụ thuộc real-time vào Catalog ở màn hình tần suất truy cập cao |
| OQ-031 | Nút "Tiến hành Checkout" áp dụng cho toàn bộ giỏ hàng, hay chỉ cho các dòng được tick chọn (theo checkbox mà SAD SCR-04 mô tả nhưng không `US` nào trong `BACKLOG` định nghĩa)? | PO | 2026-09-08 | C11, `WF_US002-003` §5 #1, ảnh hưởng US-004 | 🔴 | Áp dụng cho toàn bộ giỏ hàng ở MVP (không có tính năng chọn từng phần) — khớp đúng phạm vi `BACKLOG` hiện tại, tính năng chọn từng phần đưa vào backlog cho phiên bản sau nếu PO thấy cần |
| OQ-032 | Với Guest, cơ chế kiểm tra quyền sở hữu `CartItem` khi gọi `PATCH`/`DELETE /v1/cart/items/{cartItemId}` có tương đương cơ chế ownership theo JWT (`403 ERR_FORBIDDEN_OWNERSHIP`) không? SAD §4.1.1/§4.2 chỉ mô tả rõ cho `customerId`/`sellerId` trong JWT | Tech Lead | 2026-09-08 | `AC-US003-13`, `E-CART-0005` | 🔴 | Áp dụng cùng cơ chế: đối chiếu `cart_id` của `cartItemId` với `session_id` trong `X-Guest-Session-Id`, không khớp → `403 ERR_FORBIDDEN_OWNERSHIP` |
| OQ-033 | Ngưỡng thời gian phản hồi (p95) cho `GET /v1/cart` và cho `PATCH`/`DELETE /v1/cart/items` là bao nhiêu giây? SAD NFR-01 chỉ có ngưỡng cho danh mục/tìm kiếm (<2s) và checkout (<3s) | PO, Tech Lead | 2026-09-08 | `NFR_US002-003` §1 | 🔴 | Áp dụng cùng ngưỡng với danh mục/tìm kiếm (≤2s p95) cho `GET /v1/cart` vì cùng là thao tác đọc; ≤1s p95 cho `PATCH`/`DELETE` (thao tác ghi đơn giản, 1 dòng) |
---
## Tự chấm
**Gate G3**
| # | Tiêu chí | ☐/✅ | Ghi chú |
|---|---|---|---|
| 1 | Đủ mọi mục bắt buộc (mục N/A có ghi lý do) | ✅ | Mọi mục N/A (§2.A.3.2 phân trang, §4.2/4.3 phần không áp dụng) đều ghi lý do |
| 2 | Tiêu chí riêng của biến thể PART 2 đã nạp | ☐ | Bảng field F01 còn 1 ô để trống có lý do rõ (giới hạn trên — `OQ-030`); bảng thành phần đủ 8 cột; hai trạng thái rỗng khác nhau (đã có) |
| 3 | Mỗi US có AC đủ 4 nhóm, có luồng lỗi | ✅ (có 2 AC còn mở chờ OQ) | Xem `AC_US-002`, `AC_US-003` |
| 4 | Bảng mã lỗi đầy đủ, mỗi mã có thông điệp | ✅ | 5 mã, đủ thông điệp VI+EN |
| 5 | NFR có số đo + cách verify | ☐ | Ngưỡng hiệu năng chưa chốt — `OQ-033` |
| 6 | API contract có, ghi rõ nguồn | ✅ | Đánh dấu rõ ✅/⚠️ theo từng phần |
| 7 | QA xác nhận mọi AC test được | ☐ | Chờ QA — chưa có QA thật tham gia lần chạy này |
| 8 | Không còn OQ mở ảnh hưởng hành vi | ☐ | 5/6 OQ mới đánh dấu 🔴 chặn G3 |
| 9 | Không còn `TBD` trong bảng đặc tả PART 2 và bảng mã lỗi | ✅ | Không dùng chữ "TBD" — mọi chỗ thiếu dùng 🔴 + `OQ-nnn` cụ thể |
| 10 | Đã áp đúng bảng "Bớt ở light"/"Thêm ở strict" theo RIGOR | ✅ | `RIGOR = standard` — áp đúng checklist §2, không bớt/thêm |
| 11 | *(screen)* `WF` có cho mọi `SCR`, tập `C-id` khớp hai chiều, §5 không còn lệch Tồn tại/Hành vi chưa quyết | ☐ | Xem `WF_US002-003_v1.0.md` §5 — còn 4 dòng chưa quyết |
| 12 | *(screen)* Quy ước chung tham chiếu `UICONV`; chỗ khác có dòng ở `UICONV` §12 | ✅ | 2 dòng ngoại lệ đã thêm vào `UICONV` §12 (text rỗng SCR-04, auto-save không có nút Lưu) |
**Quét bắt buộc trước khi nộp:**
```
grep -niE "nhanh|mượt|thân thiện|v\.v|phù hợp|tương ứng|nên |có thể " SRS_US002-003_v1.0.md
→ chạy thật: 5 dòng khớp ban đầu (§1.5 dòng 1 và 7, §2.A.3.5 hai dòng "tương ứng", §8 OQ-028
"bấm nhanh") — đã sửa lại câu chữ cho cả 5 dòng, chạy lại lần hai: 0 dòng khớp
grep -n "TBD\|TODO\|???" SRS_US002-003_v1.0.md
→ 0 dòng khớp (dùng 🔴 + OQ-nnn thay vì TBD)
```
**Quy tắc viết W1–W13**
| W1 | W2 | W3 | W4 | W5 | W6 | W7 | W8 | W9 | W10 | W11 | W12 | W13 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ✅ | ✅ | ✅ (con số thiếu ⇒ `OQ`, không bịa) | ✅ (sequence đủ 4 nhánh) | ✅ (§4.2) | ✅ (§1.4 tham chiếu BR §3) | ✅ (§2.A.3.1 cột riêng) | ✅ (mọi `OQ` ghi rõ đề xuất, không quyết thay) | ✅ (mọi AC/field ghi US/BR) | ✅ (§1.1 cho PO) | ✅ (có câu quy ước đầu tài liệu) | ✅ (§1.5) | ✅ (2 sơ đồ mermaid, đủ bảng đi kèm) |

View File

@@ -0,0 +1,239 @@
<!--
WF_US002-003_v1.0.html — wireframe low-fi tự chứa, sinh bởi skill ba-3-specification (activity wf).
Nguồn hành vi: SRS_US002-003_v1.0.md · Nguồn bố cục: e-commerce/docs/sections/07-giao-dien.md §7.1.1 SCR-04 (chế độ 🎨)
Nguồn quy ước: UICONV_e-commerce_v1.0.md v1.1
Giữ nguyên <style>/<script> của template gốc (ba-3-specification/templates/wireframe.html) để mọi WF
trong dự án trông và thao tác giống nhau. Chỉ thay nội dung trong <main>.
Thành phần chỉ có ở prototype (SAD §7.1.1) mà SRS chưa có (checkbox chọn dòng, badge "Sản phẩm đã hết
hàng", badge "Giá đã thay đổi") được vẽ trong khối .proto-only, KHÔNG có data-c — xem WF §5 (#1,#2,#3).
-->
<!doctype html>
<html lang="vi">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WF — US-002, US-003 — Giỏ hàng (SCR-04)</title>
<style>
:root{--ink:#222;--mute:#777;--line:#bbb;--fill:#eee;--fill2:#f7f7f7;--accent:#3b6ea5;--danger:#b23a3a;--bg:#fafafa}
*{box-sizing:border-box}
body{margin:0;font:14px/1.45 system-ui,Segoe UI,Roboto,sans-serif;color:var(--ink);background:var(--bg)}
header.toolbar{position:sticky;top:0;z-index:9;background:#fff;border-bottom:1px solid var(--line);padding:8px 16px;display:flex;gap:16px;align-items:center;flex-wrap:wrap}
header.toolbar h1{font-size:15px;margin:0 16px 0 0}
header.toolbar label{font-size:12px;color:var(--mute)}
header.toolbar select{font:inherit}
.legend{font-size:12px;color:var(--mute);margin-left:auto}
main{padding:24px 16px;display:grid;gap:40px;justify-items:center}
.screen{width:100%;max-width:1200px;background:#fff;border:1px solid var(--line);border-radius:6px;overflow:hidden}
.screen>h2{margin:0;padding:8px 12px;font-size:13px;font-weight:600;background:var(--fill2);border-bottom:1px dashed var(--line);color:var(--mute)}
.screen>h2 small{font-weight:400;margin-left:8px}
.body{padding:12px;display:grid;gap:12px}
.zone{border:1px dashed var(--line);border-radius:4px;padding:10px;position:relative;background:#fff;min-height:44px}
.zone::before{content:attr(data-zone) " · " attr(data-name);position:absolute;top:-8px;left:8px;background:#fff;padding:0 4px;font-size:10px;color:var(--mute);letter-spacing:.02em}
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.row.right{justify-content:flex-end}
.row.between{justify-content:space-between}
[data-c]{position:relative}
[data-c]::after{content:attr(data-c);position:absolute;top:-9px;right:-4px;font-size:9px;background:var(--ink);color:#fff;padding:0 4px;border-radius:3px;line-height:14px;opacity:.85}
.label{font-weight:600;font-size:16px}
.btn{display:inline-block;padding:6px 14px;border:1px solid var(--ink);border-radius:4px;background:#fff;cursor:default;white-space:nowrap}
.btn-primary{background:var(--accent);border-color:var(--accent);color:#fff}
.btn-secondary{background:#fff}
.btn-danger{border-color:var(--danger);color:var(--danger)}
.btn[aria-disabled="true"]{opacity:.45;border-style:dashed}
.link{color:var(--accent);text-decoration:underline}
.input,.select{display:inline-block;min-width:160px;padding:6px 8px;border:1px solid var(--line);border-radius:4px;background:#fff;color:var(--mute)}
.select::after{content:" ▾";float:right}
.field{display:grid;gap:4px}
.field>span{font-size:12px}
.field>span .req{color:var(--danger)}
.hint{font-size:12px;color:var(--mute)}
.table{width:100%;border-collapse:collapse}
.table th,.table td{border:1px solid var(--line);padding:6px 8px;text-align:left;font-size:13px}
.table th{background:var(--fill2)}
.card{border:1px solid var(--line);border-radius:4px;padding:10px;background:#fff}
.badge{display:inline-block;padding:0 8px;border:1px solid var(--line);border-radius:10px;font-size:12px;background:var(--fill2)}
.img{background:repeating-linear-gradient(45deg,var(--fill),var(--fill) 6px,var(--fill2) 6px,var(--fill2) 12px);border:1px solid var(--line);min-height:80px;display:grid;place-items:center;color:var(--mute);font-size:12px}
.skeleton{height:14px;background:linear-gradient(90deg,var(--fill),var(--fill2),var(--fill));border-radius:3px;margin:8px 0}
.state{padding:24px;text-align:center;color:var(--mute);border:1px dashed var(--line);border-radius:4px;display:grid;gap:10px;justify-items:center}
.state.error{border-color:var(--danger);color:var(--danger)}
.state .illus{width:80px;height:60px}
.spinner{display:inline-block;width:14px;height:14px;border:2px solid #fff;border-top-color:transparent;border-radius:50%;vertical-align:-2px;margin-right:6px}
.modal{max-width:520px;margin:0 auto;border:1px solid var(--ink);border-radius:6px;background:#fff;box-shadow:0 0 0 2000px rgba(0,0,0,.25)}
.modal .body{padding:16px}
[data-state]{display:none}
[data-state="default"]{display:block}
body[data-view="mobile"] .screen{max-width:390px}
body[data-view="mobile"] .m-hide{display:none!important}
body[data-view="mobile"] .m-stack{flex-direction:column;align-items:stretch}
body[data-view="desktop"] .d-hide{display:none!important}
body[data-view="mobile"] .table{display:none}
body[data-view="mobile"] .cards{display:grid!important;gap:8px}
.cards{display:none}
footer.note{padding:16px;font-size:12px;color:var(--mute);text-align:center}
.proto-only{border:1px dashed var(--danger);border-radius:4px;padding:6px;background:#fdf2f2}
.proto-only .tag{display:inline-block;font-size:10px;color:var(--danger);border:1px solid var(--danger);border-radius:3px;padding:0 4px;margin-bottom:4px}
</style>
</head>
<body data-view="desktop">
<header class="toolbar">
<h1>WF — US-002, US-003 · Giỏ hàng (SCR-04)</h1>
<label>Khung nhìn
<select id="view"><option value="desktop">Desktop ≥1024</option><option value="mobile">Mobile &lt;768</option></select>
</label>
<label>Trạng thái
<select id="state">
<option value="default">Mặc định (có dữ liệu)</option>
<option value="loading">Đang tải</option>
<option value="empty-none">Rỗng — Giỏ hàng trống</option>
<option value="error">Lỗi tải dữ liệu</option>
</select>
</label>
<span class="legend">Nhãn đen = C-id/F-id trong SRS §2.A.3.1 · khung đứt xám = vùng Zn (WF §3.1.1) · khung đứt đỏ = chỉ có ở prototype, chưa có trong SRS (xem WF §5) · hành vi xem SRS, không suy từ hình</span>
</header>
<main>
<section class="screen" id="SCR-04">
<h2>SCR-04 · Giỏ hàng <small>US-002 (xem) + US-003 (sửa/xoá) · vào từ SCR-01 (icon giỏ hàng) · vai trò: ROLE-01 Guest, ROLE-02 Customer</small></h2>
<div class="body">
<div class="zone" data-zone="Z1" data-name="Header màn hình">
<span class="label" data-c="C01" data-key="cart.title">Giỏ hàng của bạn (5 sản phẩm)</span>
</div>
<div class="row m-stack" style="align-items:flex-start">
<!-- Z2 — danh sách nhóm seller -->
<div class="zone" data-zone="Z2" data-name="Danh sách nhóm seller" style="flex:2;min-width:0">
<div data-state="default">
<!-- Nhóm seller "Shop A" -->
<div class="card" style="margin-bottom:10px">
<div class="row between" data-c="C02"><b>Shop A</b></div>
<div class="row between" style="margin-top:8px">
<div class="row">
<div class="img" data-c="C03" style="width:64px;height:64px">ảnh</div>
<div>
<div data-c="C04">Áo thun basic</div>
<div class="hint" data-c="C05">Size M, Đỏ</div>
<div class="hint" data-c="C06" data-key="cart.item.price">100.000 ₫</div>
</div>
</div>
<div class="row">
<div class="proto-only d-hide" style="display:inline-block"><span class="tag">chỉ ở prototype</span><br>☐ chọn dòng</div>
<span class="row" data-c="F01">
<span class="btn btn-secondary">−</span><span class="input" style="min-width:40px;text-align:center;padding:6px">2</span><span class="btn btn-secondary">+</span>
</span>
<span class="btn btn-danger" data-c="C07" data-key="btn.delete">Xoá</span>
</div>
</div>
<div class="row between" style="margin-top:8px">
<div class="row">
<div class="img" style="width:64px;height:64px">ảnh</div>
<div>
<div>Quần jean slimfit</div>
<div class="hint">Size 30</div>
<div class="hint">50.000 ₫</div>
</div>
</div>
<div class="row">
<span class="row"><span class="btn btn-secondary">−</span><span class="input" style="min-width:40px;text-align:center;padding:6px">1</span><span class="btn btn-secondary">+</span></span>
<span class="btn btn-danger" data-key="btn.delete">Xoá</span>
</div>
</div>
<div class="row right" style="margin-top:8px;border-top:1px dashed var(--line);padding-top:6px">
<span data-c="C08">Tạm tính: 250.000 ₫</span>
</div>
</div>
<!-- Nhóm seller "Shop B" -->
<div class="card">
<div class="row between"><b>Shop B</b></div>
<div class="row between" style="margin-top:8px">
<div class="row">
<div class="img" style="width:64px;height:64px">ảnh</div>
<div>
<div>Balo du lịch</div>
<div class="hint">Đen</div>
<div class="hint">200.000 ₫</div>
<div class="proto-only" style="margin-top:4px"><span class="tag">chỉ ở prototype — WF §5 #2</span> badge "Sản phẩm đã hết hàng"</div>
</div>
</div>
<div class="row">
<span class="row"><span class="btn btn-secondary" aria-disabled="true">−</span><span class="input" style="min-width:40px;text-align:center;padding:6px">1</span><span class="btn btn-secondary" aria-disabled="true">+</span></span>
<span class="btn btn-danger" data-key="btn.delete">Xoá</span>
</div>
</div>
<div class="row right" style="margin-top:8px;border-top:1px dashed var(--line);padding-top:6px">
<span>Tạm tính: 200.000 ₫</span>
</div>
</div>
</div>
<div data-state="loading"><div class="skeleton"></div><div class="skeleton" style="width:85%"></div><div class="skeleton" style="width:92%"></div></div>
<div data-state="empty-none">
<div class="state">
<div class="img illus">minh hoạ</div>
<span data-key="cart.empty.title">Giỏ hàng trống</span>
<span class="btn btn-primary" data-c="C10" data-key="cart.empty.cta">Tiếp tục mua sắm</span>
</div>
</div>
<div data-state="error">
<div class="state error" data-c="C12">
<span data-key="cart.error.load">Không tải được dữ liệu, thử lại.</span>
<span class="btn btn-secondary" data-c="C13" data-key="btn.retry">Thử lại</span>
</div>
</div>
</div>
<!-- Z3 — tổng kết -->
<div class="zone m-hide" data-zone="Z3" data-name="Tổng kết (sidebar desktop / footer mobile)" style="flex:1;min-width:220px" data-state-visible-in="default">
<div class="proto-only" style="margin-bottom:8px"><span class="tag">chỉ ở prototype — WF §5 #1</span> "Tổng số lượng đã chọn" (phụ thuộc checkbox chọn dòng, chưa có trong SRS)</div>
<div class="label" data-c="C09">Tổng cộng: 585.000 ₫</div>
<div class="btn btn-primary" data-c="C11" data-key="btn.checkout" style="width:100%;text-align:center;margin-top:10px">Tiến hành Checkout</div>
</div>
</div>
<!-- Footer tổng kết cố định đáy — chỉ hiện ở mobile -->
<div class="zone d-hide" data-zone="Z3" data-name="Tổng kết (footer mobile)" style="position:sticky;bottom:0;background:#fff">
<div class="row between">
<span class="label">Tổng cộng: 585.000 ₫</span>
<span class="btn btn-primary" style="text-align:center">Tiến hành Checkout</span>
</div>
</div>
</div>
</section>
</main>
<footer class="note">Wireframe low-fi · bố cục và trạng thái · không phải thiết kế visual · nguồn hành vi: SRS_US002-003_v1.0.md · nguồn bố cục: e-commerce/docs/sections/07-giao-dien.md §7.1.1 (chế độ 🎨) · nguồn quy ước: UICONV_e-commerce_v1.0.md</footer>
<script>
(function(){
var view=document.getElementById('view'),state=document.getElementById('state');
function apply(){
document.body.setAttribute('data-view',view.value);
var s=state.value;
document.querySelectorAll('[data-state]').forEach(function(el){
var on=el.getAttribute('data-state')===s;
el.style.display=on?(el.tagName==='SPAN'?'inline':'block'):'none';
});
}
view.addEventListener('change',apply);state.addEventListener('change',apply);apply();
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,217 @@
# WF — Wireframe & bố cục — US-002, US-003 (SCR-04 Giỏ hàng)
| | |
|---|---|
| **Version** | 1.0 |
| **Date** | 2026-09-08 |
| **Author** | BA (qua skill ba-3-specification, activity `wf`) |
| **Status** | 🟡 Draft |
| **Approved by** | Designer: — · PO: — *(dự án không có Designer riêng — PO ký thay, `DEC-01`)* |
| **Source** | `SRS_US002-003_v1.0.md` v1.0 · `UICONV_e-commerce_v1.0.md` v1.1 · prototype: `e-commerce/docs/sections/07-giao-dien.md` §7.1.1 SCR-04 (status: approved, version 1) |
| **Scope** | US-002, US-003 · `SCR-04` |
| **Nguồn bố cục** | 🎨 **Prototype tham chiếu** — `e-commerce/docs/sections/07-giao-dien.md` §7.1.1 SCR-04, coi là nguồn sự thật về bố cục dạng văn bản (`DEC-01` v1.2) |
| **Tệp kèm** | `WF_US002-003_v1.0.html` |
## Change Log
| Version | Date | Người sửa | Thay đổi | CR |
|---|---|---|---|---|
| 1.0 | 2026-09-08 | BA (qua skill ba-3-specification, activity `wf`) | Bản đầu — bố cục `SCR-04` chế độ 🎨, 6 dòng lệch ở §5 (4 chưa quyết, 1 đã xử lý theo `UICONV` mặc định, 1 mức thấp không chặn) | — |
> **Quy ước đọc** *(quy tắc W11)*. Bảng thành phần trong `SRS` §2.A.3.1 quyết định **một phần tử
> có tồn tại không** và **hành xử thế nào**. Tài liệu này và tệp HTML kèm theo quyết định **nó
> nằm ở đâu, thứ tự nào, to bằng nào, trông ra sao ở từng trạng thái**. Mâu thuẫn ⇒ SRS thắng về
> tồn tại/hành vi, WF thắng về bố cục — mâu thuẫn đó được ghi ở §5, không im lặng.
---
## 0. Nguồn bố cục và độ tin cậy
| | |
|---|---|
| **Prototype tham chiếu** | `e-commerce/docs/sections/07-giao-dien.md` §7.1.1 SCR-04 — mô tả văn bản có cấu trúc (mục đích, persona, FR phục vụ, bố cục, 3 trạng thái, validation), `status: approved`, `version: 1` |
| **Định dạng** | Mô tả văn bản có cấu trúc (không phải Figma/ảnh) — SAD §7.0 xác nhận "không có brand guideline cố định" |
| **Mức phủ** | 1/1 màn hình của US-002/US-003 (`SCR-04`) có mô tả trong prototype |
| **Design system** | Chưa chọn — `OQ-022` (đã mở ở `UICONV`) |
| **Độ tin cậy** | 🎨 SAD §7 đã `approved` ở cấp SA/dự án ⇒ WF là **bản ghi lại** bố cục của SCR-04 và đối chiếu với `SRS` |
## 1. Bản đồ prototype ↔ màn hình
| SCR | Tên màn hình | Có trong prototype | Frame / trang / mục trong prototype | Mức khớp với SRS | Ghi chú |
|---|---|---|---|---|---|
| SCR-04 | Giỏ hàng | ✅ | SAD §7.1.1 "SCR-04 — Giỏ hàng" | 🔴 khác tồn tại/hành vi | 4 điểm lệch — xem §5 (#1–#4) |
## 2. Khung bố cục chung
*Lấy từ `UICONV` §1. Chỉ ghi phần khác/bổ sung cho SCR-04.*
| | |
|---|---|
| **Grid** | 🔴 Chưa chốt — `UICONV` §1, `OQ-018` (áp dụng chung, không riêng SCR-04) |
| **Vùng cố định** | Header toàn site kế thừa `SCR-01` (logo, tìm kiếm, `LanguageSwitcher`, `CurrencyToggle`, icon giỏ hàng, icon tài khoản) — theo `UICONV` §1. SCR-04 không có sidebar/menu cố định riêng |
| Breakpoint | Khoảng (px) | Bố cục | Nguồn |
|---|---|---|---|
| Desktop | ≥ 1024 *(ngưỡng chính xác — `OQ-018`)* | Danh sách nhóm seller full-width, sidebar tổng kết bên phải | `UICONV` §1 + SAD §7.1.1 (mô tả "Sidebar/footer tổng kết") |
| Tablet | 768–1023 | 🔴 Chưa mô tả riêng trong SAD — BA đề xuất giữ như desktop nhưng tổng kết chuyển xuống dưới danh sách (footer thay vì sidebar) | BA đề xuất, chưa xác nhận — mức thấp, không chặn G3 |
| Mobile | < 768 | Danh sách nhóm seller full-width, tổng kết chuyển thành **footer cố định đáy màn hình** ("Sidebar/footer tổng kết" — SAD dùng cả hai từ, BA diễn giải: sidebar ở desktop, footer ở mobile) | SAD §7.1.1 + BA diễn giải |
---
## 3. Bố cục từng màn hình
### 3.1 SCR-04 — Giỏ hàng
| | |
|---|---|
| **Loại** | Chi tiết (không phân trang) |
| **Prototype** | SAD §7.1.1 SCR-04 |
| **HTML** | `WF_US002-003_v1.0.html#SCR-04` |
| **Thành phần (SRS §2.A.3.1)** | C01–C13, F01 — khớp đủ (xem tự kiểm §"Tự chấm") |
#### 3.1.1 Bảng vùng
| Vùng | Tên | Vị trí desktop | Vị trí mobile | Chứa thành phần | Chiều rộng | Ưu tiên hiển thị |
|---|---|---|---|---|---|---|
| Z1 | Header màn hình | Hàng 1, full | Hàng 1, full | C01 | 100% | 1 |
| Z2 | Danh sách nhóm seller | Hàng 2, cột trái (2/3) | Hàng 2, full (trên footer tổng kết) | C02, C03, C04, C05, C06, F01, C07 (lặp theo nhóm/dòng) | 66% desktop / 100% mobile | 1 |
| Z3 | Tổng kết (sidebar desktop / footer mobile) | Hàng 2, cột phải (1/3), sticky | Cố định đáy màn hình | C09, C11 (C08 nằm trong từng nhóm ở Z2, không lặp lại ở Z3) | 33% desktop / 100% mobile | 1 |
| Z4 | Trạng thái đặc biệt (rỗng/lỗi) | Thay chỗ Z2 khi kích hoạt | Thay chỗ Z2 khi kích hoạt | C10, C12, C13 | Bằng Z2 | 1 |
*Ưu tiên hiển thị: 1 = luôn hiện.*
**Ghi chú vị trí C08 (tạm tính nhóm):** theo mô tả SAD "mỗi nhóm = 1 seller... tạm tính theo VND" nằm cuối mỗi khối nhóm trong Z2 (không phải trong Z3) — khác với cách trình bày "tổng kết ở sidebar" chỉ áp dụng cho **tổng toàn giỏ** (C09), không áp dụng cho tạm tính từng nhóm (C08).
#### 3.1.2 Vị trí thành phần
*Tập `C-id`/`F-id` phải khớp hai chiều với `SRS` §2.A.3.1 — xem "Tự chấm" cuối tài liệu.*
| C-id | Tên thành phần | Vùng | Thứ tự trong vùng | Kích thước / độ rộng | Kiểu trình bày | Phần tử trong prototype | Khớp |
|---|---|---|---|---|---|---|---|
| C01 | lbl_header_title | Z1 | 1 | auto | Tiêu đề cấp 1 | "Header: tiêu đề 'Giỏ hàng của bạn' + số lượng sản phẩm" | ✅ |
| C02 | grp_seller_header | Z2 | 1 (đầu mỗi nhóm, lặp) | 100% | Nhãn | "danh sách nhóm theo seller (mỗi nhóm = 1 seller, hiển thị tên gian hàng)" | ✅ |
| C03 | img_cart_item | Z2 | 1 (trong mỗi dòng) | 64px (đề xuất, chưa xác nhận — mức thấp) | Ảnh | "CartItem (ảnh...)" | ✅ |
| C04 | lbl_product_name | Z2 | 2 | auto | Nhãn | "CartItem (...tên...)" | ✅ |
| C05 | lbl_variant | Z2 | 3 | auto | Nhãn | "CartItem (...biến thể...)" | ✅ |
| C06 | lbl_unit_price | Z2 | 4 | auto | Nhãn | "CartItem (...đơn giá...)" | 🟠 *(nguồn giá — `OQ-029` — bố cục khớp, nội dung/nguồn dữ liệu chưa chốt)* |
| F01 | input_quantity | Z2 | 5 | 96px (đề xuất) | Ô nhập số (bộ đếm +/-) | "CartItem (...bộ đếm số lượng...)" | ✅ |
| C07 | btn_delete_item | Z2 | 6 | auto | Nút nguy hiểm | "CartItem (...nút xoá)" | ✅ |
| C08 | lbl_seller_subtotal | Z2 | 7 (cuối mỗi nhóm) | auto | Nhãn | "mỗi nhóm... hiển thị tên gian hàng" *(SAD không tách riêng "tạm tính theo nhóm" — BA suy ra từ "danh sách nhóm theo seller" + "tạm tính" ở footer; xem §5 #6)* | 🟠 |
| C09 | lbl_cart_total | Z3 | 1 | auto | Nhãn | "Sidebar/footer tổng kết: tổng số lượng đã chọn, tạm tính (subtotal theo VND)" | ✅ |
| C10 | btn_continue_shopping | Z4 | 2 (trong khối rỗng) | auto | Nút phụ | "empty = 'Giỏ hàng trống' + nút 'Tiếp tục mua sắm'" | ✅ |
| C11 | btn_checkout | Z3 | 2 | 100% (nút chính, nổi bật) | Nút chính | "nút 'Tiến hành Checkout'" | ✅ *(bố cục khớp; điều kiện bật/tắt theo checkbox — xem §5 #1, KHÔNG áp dụng ở SRS này)* |
| C12 | banner_error_load | Z4 | 1 | 100% | Banner | "error = cảnh báo dòng sản phẩm hết hàng/giá thay đổi (badge...)" *(SAD mô tả lỗi ở mức dòng, BA áp dụng mẫu banner lỗi tải chung của `UICONV` §6 cho lỗi `GET /v1/cart` — không phải cùng ý nghĩa với badge SAD, xem §5 #2/#3)* | 🟠 |
| C13 | btn_retry_load | Z4 | 1 (trong C12) | auto | Nút phụ | Suy ra từ mẫu "lỗi tải + retry" chung toàn site (`UICONV` §6), SAD không viết riêng cho SCR-04 | 🟠 |
**Thành phần chỉ có ở prototype, chưa có trong SRS này** *(không gán C-id — theo quy tắc "không tự thêm vào SRS")*:
| Mô tả trong prototype (SAD §7.1.1) | Vì sao chưa vào SRS | Xem §5 |
|---|---|---|
| Checkbox chọn/bỏ chọn từng dòng `CartItem` hoặc cả nhóm | Không có `US` nào trong `BACKLOG` định nghĩa tính năng chọn từng phần giỏ hàng | #1 |
| Badge "Sản phẩm đã hết hàng" chặn tick chọn | Phụ thuộc mục trên (checkbox) + kiểm tra tồn kho real-time ngoài scope US-002/US-003 theo `BACKLOG` | #2 |
| Badge "Giá đã thay đổi" | Nguồn giá hiển thị (snapshot/real-time) chưa xác nhận thuộc scope US-002 | #3 |
**Kiểu trình bày** dùng đúng từ vựng `UICONV` §11 (Nhãn, Nút chính, Nút phụ, Nút nguy hiểm, Ô nhập số, Card, Modal, Banner...).
#### 3.1.3 Trạng thái hiển thị
| Trạng thái | Vùng thay đổi | Hiển thị gì | Khoá text (SRS §4.2) | Nút hành động |
|---|---|---|---|---|
| Đang tải lần đầu | Z2 | Skeleton 3 dòng (theo `UICONV` §6) | — | — |
| **Chưa có dữ liệu nào (rỗng)** | Z2→Z4 | Minh hoạ + câu dẫn + nút | `cart.empty.title` | C10 |
| Lỗi tải dữ liệu | Z2→Z4 | Banner C12 thay chỗ danh sách | `cart.error.load` | C13 |
| Đang gửi (sửa số lượng/xoá 1 dòng) | Đúng dòng `CartItem` đang xử lý trong Z2 | Spinner cục bộ tại dòng, F01/C07 disable | — | — |
🔴 Hai trạng thái rỗng: SCR-04 chỉ có MỘT loại rỗng (chưa có dữ liệu), không có "bộ lọc không
khớp" (giỏ hàng không có bộ lọc) — khác với mẫu chung `UICONV` §6 vốn có hai loại rỗng. Đây
không phải lệch, mà là **không áp dụng** loại rỗng thứ hai.
#### 3.1.4 Responsive
| Breakpoint | Thay đổi so với desktop | Thành phần ẩn / gộp / đổi kiểu |
|---|---|---|
| Tablet | 🔴 Chưa mô tả riêng trong SAD — BA đề xuất giữ layout 1 cột, Z3 chuyển xuống footer | Mức thấp, không chặn G3 |
| Mobile | Z3 (C09, C11) chuyển thành footer cố định đáy màn hình; Z2 giữ danh sách dạng dọc | Không ẩn thành phần nào, chỉ đổi vị trí Z3 |
#### 3.1.5 Tương tác trình bày
| Thành phần | Sự kiện | Phản hồi trình bày | Ngưỡng / thời lượng | Nguồn |
|---|---|---|---|---|
| F01 | Bấm +/- hoặc rời field sau khi gõ tay | Gọi `PATCH` ngay, dòng chuyển spinner cục bộ | 🔴 chưa có ngưỡng debounce khi gõ tay — `OQ` mới nếu cần, mức thấp không chặn G3 | BA đề xuất |
| C07 | Bấm "Xoá" | Mở modal xác nhận giữa màn (theo `UICONV` §5) | — | `UICONV` §5 |
| Toàn màn | Sửa/xoá thành công | Cập nhật C08/C09 tại chỗ, không toast (🔴 `UICONV` §5 chưa chốt vị trí/thời lượng toast — `OQ-020`, đã mở) | — | `UICONV` §5 (chưa chốt) |
#### 3.1.6 Thứ tự focus
| Thứ tự | ID | Ghi chú |
|---|---|---|
| 1 | F01 (dòng đầu tiên) | Focus mặc định khi vào màn hình — 🔴 chưa xác nhận với PO/Dev FE, mức thấp không chặn G3 |
---
## 4. Luồng màn hình theo prototype
| Từ | Hành động | Đến | Có trong SRS §2.A.2 | Có trong prototype |
|---|---|---|---|---|
| SCR-01 | Bấm icon giỏ hàng | SCR-04 | ✅ | ✅ (SAD §7.2.1 bước D "Thêm vào giỏ hàng (SCR-04)") |
| SCR-04 | Bấm "Tiến hành Checkout" (C11) | SCR-05 | ✅ (ngoài phạm vi hành vi) | ✅ (SAD §7.2.1 bước E→H) |
| SCR-04 | Bấm "Tiếp tục mua sắm" (C10, giỏ rỗng) | SCR-01 | ✅ | 🟠 SAD không vẽ rõ cạnh này trong `7.2.1` (chỉ mô tả trong bố cục SCR-04, không có trong flowchart) — không mâu thuẫn, chỉ là chưa vẽ |
---
## 5. Lệch giữa prototype và SRS
| # | Màn hình | Prototype có | SRS nói | Loại lệch | Bên thắng theo W11 | Xử lý | Ai quyết | Trạng thái |
|---|---|---|---|---|---|---|---|---|
| 1 | SCR-04 | Checkbox chọn/bỏ chọn từng dòng hoặc cả nhóm seller; nút Checkout ngụ ý chỉ áp dụng cho phần đã chọn | `SRS` §1.5 dòng 3: không có tính năng chọn từng phần; C11 chỉ disable khi giỏ có 0 `CartItem` | Tồn tại | Chưa quyết — đây là khoảng trống scope, không phải "SRS đã nói khác" | PO xác nhận có bổ sung tính năng chọn từng phần vào phạm vi US-002/US-003 (hoặc US mới) hay giữ nguyên "Checkout toàn giỏ" | PO | ☐ (`OQ-031`) |
| 2 | SCR-04 | Badge "Sản phẩm đã hết hàng" chặn tick chọn | `SRS` §1.5 dòng 4: không kiểm tra tồn kho tại màn Giỏ hàng (khoanh vùng theo `BACKLOG`, thuộc US-004) | Tồn tại/Hành vi | Chưa quyết | PO + Tech Lead xác nhận có đưa kiểm tra tồn kho real-time vào US-002/US-003 hay giữ ở US-004 | PO, Tech Lead | ☐ (`OQ-030`) |
| 3 | SCR-04 | Badge "Giá đã thay đổi"; đơn giá hiển thị ngụ ý là giá real-time (để so sánh phát hiện thay đổi) | `SRS` C06: nguồn giá chưa chốt (snapshot hay real-time) | Tồn tại/Hành vi | Chưa quyết | PO + Tech Lead xác nhận nguồn giá trước, sau đó mới quyết có badge này không | PO, Tech Lead | ☐ (`OQ-029`) |
| 4 | SCR-04 | "số lượng ≥ 1 và ≤ tồn kho hiện tại" (validation chính của SAD SCR-04) | `SRS` F01: chỉ xác nhận ràng buộc dưới (≥1); ràng buộc trên chưa chốt | Hành vi | Chưa quyết | PO + Tech Lead xác nhận có kiểm tra tồn kho khi sửa số lượng ở màn Giỏ hàng không (cùng nguồn với #2) | PO, Tech Lead | ☐ (`OQ-030`, gộp cùng #2) |
| 5 | SCR-04 | Chỉ liệt kê "nút xoá" trong `CartItem`, không nhắc xác nhận trước khi xoá | `SRS`/`WF` áp dụng modal xác nhận mặc định theo `UICONV` §5 (quy ước chung cho "xoá") | Hành vi | **SRS** (áp dụng đúng quy ước dự án, không phải SRS tự ý khác `UICONV`) | Đã xử lý — không cần PO quyết thêm, ghi nhận ở đây để không ai tưởng là bỏ sót | — | ✅ Đã xử lý |
| 6 | SCR-04 | Không nêu rõ tiêu chí sắp xếp nhóm seller/dòng `CartItem` trong mỗi nhóm | `SRS` §2.A.3.2: BA đề xuất theo thời điểm thêm vào giỏ, mức thấp không chặn G3 | Bố cục nhỏ | **Prototype** (không có, để BA/Dev FE quyết định tự do) | Giữ đề xuất của BA, PO xác nhận không bắt buộc trước G3 | PO *(không chặn)* | ☐ *(không chặn G3 — mức thấp)* |
**Tổng kết:** đã đối chiếu 1 màn hình (`SCR-04`); **6 dòng lệch** — 4 dòng loại Tồn tại/Hành vi
**chưa quyết** (🔴 chặn G3: #1, #2, #3, #4 — thực chất #2 và #4 cùng một nguồn `OQ-030`, đếm là
2 câu hỏi độc lập `OQ-029`/`OQ-030`/`OQ-031`), 1 dòng Hành vi đã xử lý bằng cách áp dụng mặc định
`UICONV` (#5, không chặn), 1 dòng Bố cục mức thấp không chặn (#6).
## 6. Bàn giao
| | |
|---|---|
| **Designer còn phải làm** | Không có Designer riêng ở dự án này (`DEC-01`) — việc còn lại (màu, typography, icon, minh hoạ trạng thái rỗng, motion, khoảng cách chính xác) chuyển cho **Dev FE**, dùng `UICONV` §11 làm ràng buộc tối thiểu và tự chọn design system theo `OQ-022` khi được Tech Lead xác nhận |
| **Dev FE dùng WF để** | Dựng layout Z1–Z4 và chọn component theo §3.1.2; **không** suy ra hành vi từ WF — hành vi ở `SRS` |
| **Không được suy ra từ WF** | Khoảng cách/kích thước chính xác (chưa có design system); màu; font; ngưỡng debounce F01 (chưa chốt) |
| **Khi prototype thay đổi** | Cập nhật `07-giao-dien.md` SCR-04 (SA/BE) → BA cập nhật §1, §5 và version WF; `SRS` chỉ đổi nếu lệch loại Tồn tại/Hành vi được PO chấp nhận qua `CR` (vì `SRS` chưa `✅ Baselined`, chưa cần `CR` chính thức, chỉ cần sửa trực tiếp + Change Log) |
## 7. Open Questions
| ID | Câu hỏi | Hỏi ai | Từ ngày | Chặn gì | 🔴 chặn G3? |
|---|---|---|---|---|---|
| OQ-031 | Checkout áp dụng toàn giỏ hay theo dòng được chọn? | PO | 2026-09-08 | §5 #1, C11 | 🔴 |
| OQ-030 | Kiểm tra tồn kho khi sửa số lượng tại màn Giỏ hàng — có hay không? | PO, Tech Lead | 2026-09-08 | §5 #2, #4, F01 | 🔴 |
| OQ-029 | Nguồn giá hiển thị C06 — snapshot hay real-time? | PO, Tech Lead | 2026-09-08 | §5 #3, C06 | 🔴 |
---
## Tự chấm
| # | Tiêu chí | ☐/✅ | Ghi chú |
|---|---|---|---|
| 1 | Mọi `SCR` trong `SRS` §2.A.1 có một mục §3 | ✅ | Chỉ 1 SCR (SCR-04), có §3.1 |
| 2 | Mọi `C-id`/`F-id` khớp hai chiều giữa `SRS` §2.A.3.1 và WF §3.1.2 | ✅ | Xem kết quả chạy thật ngay dưới bảng này |
| 3 | Mỗi màn hình có §3.x.3 với trạng thái rỗng | ✅ (có 1 loại rỗng, ghi rõ lý do không có loại thứ 2) | — |
| 4 | Mỗi màn hình web có responsive ≥ 2 breakpoint | ✅ | Desktop/Tablet/Mobile — Tablet mức đề xuất thấp |
| 5 | Mọi con số ở §3.x.5 có nguồn (UICONV/prototype/OQ) | ✅ | Debounce F01 chưa có ngưỡng, ghi rõ mức thấp không chặn |
| 6 | §5 đã điền, mọi lệch Tồn tại/Hành vi có người quyết | ☐ | Đã điền đủ, nhưng 4 dòng còn ☐ chờ PO/Tech Lead — đây là kết quả thật của lần chạy này, không phải thiếu sót của WF |
| 7 | Tệp HTML mở được, mỗi `SCR` một `<section id>`, mỗi thành phần có `data-c` khớp C-id | ✅ | Xem `WF_US002-003_v1.0.html` |
| 8 | Kiểu trình bày chỉ dùng từ vựng `UICONV` §11 | ✅ | — |
| 9 | Có chữ ký Designer *(hoặc PO thay, `DEC-01`)* | ☐ | Chưa ký — chờ PO xác nhận thật ở G3 |
**Kết quả chạy thật tự kiểm C-id/F-id hai chiều** *(trích `[CF][0-9]{2}` từ `SRS` §2.A.3.1 và
từ `WF` .md §3.1.2 + `data-c` trong `.html`)*:
- Tập trong `SRS`: `C01, C02, C03, C04, C05, C06, F01, C07, C08, C09, C10, C11, C12, C13` (13 `C` + 1 `F` = 14)
- Tập trong `WF` .md §3.1.2: `C01, C02, C03, C04, C05, C06, F01, C07, C08, C09, C10, C11, C12, C13` (khớp)
- Tập `data-c` trong `.html`: `C01, C02, C03, C04, C05, C06, F01, C07, C08, C09, C10, C11, C12, C13` (khớp)
- **Kết luận: 0 lệch hai chiều.** Ba thành phần chỉ có ở prototype (checkbox chọn dòng, badge hết hàng, badge giá đổi) **không** được gán `C-id` và **không** xuất hiện trong `data-c` của HTML — đúng quy tắc "không tự thêm vào SRS", được mô tả riêng ở khối chú thích trực quan trong HTML (không có `data-c`).