380 lines
36 KiB
Markdown
380 lines
36 KiB
Markdown
# 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) |
|