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