Files
Leonard-ThindPad-P50 2c7bcde741 improve BA skill
2026-09-09 06:34:57 +07:00

380 lines
36 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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) |