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