improve BA skill

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

View File

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