FEA_005 — Sản phẩm, Giỏ hàng & Đơn hàng
Tổng quan
Module products (NestJS) gộp 3 domain: Sản phẩm (CRUD), Giỏ hàng (thêm/xóa/cập nhật), và Đơn hàng (đặt hàng, lịch sử, cập nhật thanh toán). Toàn bộ controller dùng JwtAuthGuard + PermissionGuard; riêng GET /products và GET /products/:id là @Public().
- Controller:
src/products/products.controller.ts - Service:
src/products/products.service.ts - Phụ thuộc (từ constructor
ProductsService:18):PrismaService, Bull queueemail(@InjectQueue('email')),MailService.
Luồng gọi (Call Graph — CodeGraph)
Trích từ CodeGraph index của laptop-shop (codegraph_callees), là quan hệ gọi thực tế trong code.
handlePlaceOrder(đặt hàng) gọi:prisma.product.create/update(qua transaction) — tạo order, trừ kho.MailService.sendOrderConfirmationEmail(src/mail/services/mail.service.ts:109) — đường fallback gửi email đồng bộ khi Bull queue lỗi.
updatePaymentMethod(chọn thanh toán) gọi:prisma.order.update— cập nhật trạng thái + thông tin người nhận.UpdatePaymentMethodDto— validatepaymentMethod ∈ {COD, BANKING},receiverMaillà email.
Việc gửi email còn đi qua Bull queue
Danh sách APIs
Prefix toàn cục:
/api/v1. Quyền yêu cầu trong ngoặc.
Sản phẩm
| Method | Endpoint | Quyền | Mô tả |
|---|---|---|---|
| POST | /api/v1/products | PRODUCTS_CREATE | Tạo sản phẩm |
| GET | /api/v1/products?page= | Public | Danh sách (phân trang) |
| GET | /api/v1/products/:id | Public | Chi tiết sản phẩm |
| PATCH | /api/v1/products/:id | PRODUCTS_UPDATE | Cập nhật sản phẩm |
| DELETE | /api/v1/products/:id | PRODUCTS_DELETE | Xóa mềm sản phẩm |
Giỏ hàng
| Method | Endpoint | Quyền | Mô tả |
|---|---|---|---|
| GET | /api/v1/products/cart | PRODUCTS_READ | Lấy giỏ hàng + tổng tiền của user |
| POST | /api/v1/products/:id/add-to-cart | PRODUCTS_READ | Thêm sản phẩm vào giỏ |
| POST | /api/v1/products/delete-product-in-cart/:id | PRODUCTS_READ | Xóa 1 dòng giỏ (theo cartDetailId) |
| POST | /api/v1/products/update-cart-before-checkout | PRODUCTS_READ | Cập nhật số lượng trước khi checkout |
Đơn hàng
| Method | Endpoint | Quyền | Mô tả |
|---|---|---|---|
| POST | /api/v1/products/place-order | PRODUCTS_READ | Đặt hàng từ giỏ (transaction) |
| GET | /api/v1/products/orders-history | PRODUCTS_READ | Lịch sử đơn của user |
| GET | /api/v1/products/order/:id | PRODUCTS_READ | Chi tiết 1 đơn (của chính user) |
| PATCH | /api/v1/products/order/:id/payment-method | PRODUCTS_READ | Chọn phương thức TT + thông tin nhận |
Payload mẫu
POST /api/v1/products (CreateProductDto)
{
"name": "Laptop Dell XPS 13",
"price": 25000000,
"image": "https://example.com/xps13.jpg",
"detailDesc": "Mô tả chi tiết...",
"shortDesc": "Mô tả ngắn",
"quantity": 10,
"sold": 0,
"factory": "Dell",
"target": "Doanh nhân"
}
- Validation:
name/detailDesc/shortDesc/factory/targetbắt buộc;price ≥ 1,quantity ≥ 0;imagephải là URL hợp lệ (optional).UpdateProductDto= PartialType (mọi field optional).
POST /api/v1/products/:id/add-to-cart
{ "quantity": 2 }
POST /api/v1/products/update-cart-before-checkout
{ "currentCartDetail": [{ "id": "12", "quantity": "3" }] }
POST /api/v1/products/place-order
{ "totalPrice": 50000000 }
PATCH /api/v1/products/order/:id/payment-method (UpdatePaymentMethodDto)
{
"paymentMethod": "COD",
"receiverName": "Nguyễn Văn A",
"receiverAddress": "123 Đường ABC",
"receiverPhone": "0900000000",
"receiverMail": "a@example.com"
}
paymentMethodchỉ nhận"COD"hoặc"BANKING";receiverMailphải là email hợp lệ.
Business Logic (Quy tắc Nghiệp vụ)
Tạo sản phẩm (create)
- Chặn trùng
name→409 ConflictExceptionnếu đã tồn tại.
Danh sách / Chi tiết (findAll, findOne)
findAll: phân trangPAGE_SIZE(src/config/constant), chỉ lấydeletedAt = null; trả{ data, currentPage, totalPages, totalProducts }.page ≤ 0→ ép về 1.findOne: chỉ lấydeletedAt = null; không thấy →404.
Xóa (remove)
- Soft delete qua
prisma.softDelete(setdeletedAt), không xóa cứng. Không thấy →404.
Thêm vào giỏ (addProductToCart)
- Nếu user chưa có giỏ → tạo
Cartmới kèmCartDetail. - Nếu đã có giỏ →
incrementcart.sum, rồiupsertCartDetail(cộng dồnquantitynếu sản phẩm đã có trong giỏ). quantitymặc định 1 nếu body rỗng. Giá lưu theoproduct.pricetại thời điểm thêm.- ⚠️ Không kiểm tra tồn kho ở bước thêm giỏ (chỉ kiểm tra khi đặt hàng).
Xóa dòng giỏ (deleteProductInCart)
- Nhận
cartDetailId;decrementcart.sumtheoquantitycủa dòng rồi xóaCartDetail.
Đặt hàng (handlePlaceOrder) — luồng phức tạp nhất
Chạy trong prisma.$transaction với isolationLevel: 'Serializable', timeout 10s:
- Lấy giỏ + chi tiết; giỏ rỗng →
400 BadRequest. - Lock các sản phẩm bằng
SELECT ... FOR UPDATE(raw query) chống race condition. - Re-check tồn kho từng dòng; thiếu hàng →
400(kèm số lượng available/required). - Tạo
Order(statusPENDING,paymentMethod = NOT_DEFINED,paymentStatus = PAYMENT_UNPAID) +OrderDetailtừ giỏ. decrementproduct.quantity,incrementproduct.sold.- Xóa
CartDetail+Cartcủa user. - Sau transaction: đẩy email
order-confirmationvào Bull queue (3 attempts, exponential backoff); nếu queue lỗi → fallback gửi đồng bộ quaMailService; lỗi email không làm fail đơn.
Cập nhật phương thức thanh toán (updatePaymentMethod)
- Chỉ cho đơn
status = PENDINGcủa chính user; không thấy →404. - Chỉ cho khi
paymentMethod = NOT_DEFINED(chưa set); đã set →400. - Cập nhật →
paymentStatus = PAYMENT_SUCCESS,status = CONFIRM, lưu thông tin người nhận. Gửi email xác nhận qua queue (lỗi email không rollback). - ⚠️ Map
paymentMethod:COD→ "Thanh toán khi nhận hàng", còn lại → "Chuyển khoản ngân hàng".
Xem đơn (getOrderById, getOrderHistoryForUser)
- Phân quyền dữ liệu: luôn lọc
userIdcủa người gọi → user chỉ xem được đơn của chính mình.
Database Tương tác
- Schema: laptop-shop-db
- Bảng: Product, Cart, CartDetail, Order, OrderDetail
- Phụ:
User(lấy email gửi xác nhận);EmailQueue(qua Bull queueemail).
Cần human review
- ⚠️ Giá trị trạng thái không nhất quán: đặt hàng dùng
paymentStatus = PAYMENT_UNPAIDcòn schema/cập nhật dùngPAYMENT_SUCCESS;statusđi quaPENDING → CONFIRM. Nên chuẩn hóa enum. - ⚠️ Tồn kho không kiểm ở bước add-to-cart — chỉ kiểm khi
place-order. Có thể gây trải nghiệm xấu (thêm được nhưng không đặt được). - ⚠️
place-ordernhậntotalPricetừ client thay vì tính lại từ giỏ ở server → rủi ro sai/giả mạo giá. Cần review. - ⚠️
Order.userIdkhông có FK@relationtrong Prisma (xem ghi chú ở schema doc).
Source of truth
- Controller:
laptop-shop/src/products/products.controller.ts - Service:
laptop-shop/src/products/products.service.ts - DTO:
laptop-shop/src/products/dto/ - Catalog:
01_Raw/features/Features.json