Appearance
🔄 LUỒNG NGHIỆP VỤ GROUPBUY (spec)
Nguồn truth cho luồng nghiệp vụ — mọi thay đổi code (BE
../gghut-core, FE storefront, FE admin) phải khớp tài liệu này. Chi tiết kỹ thuật API nằm ởdocs/backend-api-integration.md. Cập nhật: 2026-08-26.
1 · Vòng đời campaign
DRAFT ──open──▶ OPEN ──hệ thống tự chốt──▶ CLOSED (thành công — chốt giá)
│ │
│ └──hệ thống auto khi hết giờ & thiếu MOQ──▶ FAILED (thất bại — hoàn cọc)
└──admin cancel──────────────────────────────▶ CANCELLED (hoàn cọc)Quy tắc kết thúc OPEN (hệ thống tự động, cron ~60s trong BE):
| Điều kiện | Kết quả |
|---|---|
taken == capacity (đủ slot) | Chốt giá ngay lập tức — không chờ hết hạn |
Quá closes_at và tổng qty cọc ≥ MOQ | Chốt giá → CLOSED |
Quá closes_at và tổng qty cọc < MOQ | Campaign → FAILED, toàn bộ đơn DEPOSIT_HELD → REFUND_PENDING |
| Admin bấm Close/Cancel thủ công | Vẫn hoạt động như cũ. Cancel: đơn DEPOSIT_HELD → REFUND_PENDING |
- Admin vẫn có thể settle thủ công (
POST /admin/campaigns/{id}/settle) như Phase 0 — auto-settle chỉ thêm đường tự động, không thay đổi semantics chốt giá. - MOQ (số lượng tối thiểu để chốt giá) =
qty_thresholdcủa tier thấp nhất. Được set ngay khi tạo campaign và hiển thị luôn trên storefront từ ngày đầu ("Cần tối thiểu N suất để chốt giá"). Không cần field riêng.
2 · Giá hiển thị trên storefront
Ba loại giá:
| Field | Ý nghĩa |
|---|---|
default_price | Giá gốc (niêm yết) |
promo_price | Giá khuyến mãi |
tiers[].unit_price | Giá theo mốc số lượng |
Giá hiệu lực hiện tại (current_unit_price, BE tính sẵn cả ở list lẫn detail):
- Tier cao nhất mà
takenđã vượt qua (qty_threshold ≤ taken) → dùngunit_pricecủa tier đó. - Chưa đạt mốc nào →
promo_pricenếu có, không thìdefault_price. - Sau khi campaign đã chốt:
final_unit_pricetrên từng đơn là truth; campaign hiển thị giá đã chốt.
Hiển thị FE:
- Luôn show "giá hiện tại" to;
default_pricegạch ngang khi tồn tại giá thấp hơn (promo hoặc tier). - Realtime theo mốc: trang detail poll
GET /campaigns/{slug}mỗi 15–30s (dừng khi tab ẩn) — vừa đủ cập nhật giá + progress đến khi có SSE (Phase 2). - Progress bar kèm thông điệp "Còn X suất nữa để giảm còn Y đ" và chỉ báo MOQ đạt/chưa đạt.
3 · Vòng đời order
RESERVED ──webhook cọc──▶ DEPOSIT_HELD ──settle thành công──▶ SETTLEMENT_DUE
│ │ │ │ │
│ └─15p không cọc──▶ EXPIRED│ ├─admin refund / fail / cancel─▶ REFUND_PENDING ─admin xác nhận đã trả─▶ REFUNDED
│ │ │
└─campaign bị cancel──▶ CANCELLED └─webhook đủ amount_due─▶ CONFIRMED
│
fulfill/"đã về hàng" hoặc ship ▼
SHIPPED ─admin xác nhận─▶ DELIVEREDAi kích hoạt gì:
| Chuyển trạng thái | Ai | Cơ chế |
|---|---|---|
| RESERVED → EXPIRED | Hệ thống | Cron quét đơn quá 15 phút giữ slot (đã có) |
| RESERVED → DEPOSIT_HELD | Webhook SePay | Cọc về đủ deposit_due |
| DEPOSIT_HELD → SETTLEMENT_DUE | Hệ thống/admin | Settle (auto hoặc tay); BE tính sẵn amount_due = qty × final_unit_price + shipping_fee − deposit_paid |
| SETTLEMENT_DUE → CONFIRMED | Webhook SePay | User chuyển đủ phần còn lại (cộng dồn qua settlement_paid) |
| CONFIRMED → SHIPPED | Hệ thống/admin | Campaign-level "Đã về hàng" (fulfill) chuyển tất cả đơn đã trả đủ sang SHIPPED; hoặc admin ship từng đơn |
| SHIPPED → DELIVERED | Admin | Xác nhận giao từng đơn |
| bất kỳ giai đoạn trước settle → REFUND_PENDING | Hệ thống | Auto khi FAILED/Cancel |
| REFUND_PENDING → REFUNDED | Admin | Bấm "Đã hoàn cọc" sau khi đã chuyển tiền ngoài hệ thống |
| DEPOSIT_HELD/SETTLEMENT_DUE → REFUNDED | Admin | Refund chủ động 1 bước (không qua pending) |
Tự động hoàn tất (mặc định): admin KHÔNG phải bấm từng đơn — chỉ cần 1 nút "Đã về hàng" ở campaign; mọi đơn đủ tiền tự vào hàng gửi. Nút refund/ship/deliver per-order chỉ là can thiệp chủ động khi cần (hoàn cọc riêng, ship sớm…).
4 · Thanh toán
- Cọc: đặt slot → giữ 15 phút → user chuyển khoản theo mã đơn (
GB-XXXXXXtrong nội dung CK) → webhook SePay xác nhận tự động →RESERVED→DEPOSIT_HELD. - Phần còn lại: campaign chốt xong, BE trả
amount_duetrên đơn. User chuyển tiếp theo cùngorder_code; webhook ghi nhận dạng thanh toán settlement, cộng dồnsettlement_paid; đủ →CONFIRMED. - FE hiển thị rõ ở lịch sử mua: "Thành công" / "Cần thanh toán còn lại X đ" (+ nút dẫn tới trang thanh toán).
5 · Quyền & lịch sử
- Bắt buộc đăng nhập để đặt hàng: BE đã chặn (401); FE storefront phải gate nút đặt hàng — chưa login thì redirect
/login?next=<trang hiện tại>rồi quay lại đúng chỗ. - Lịch sử mua tại
/orders(storefront, cần đăng nhập): danh sách đơn kèm badge trạng thái (đang giữ/đã cọc/cần thanh toán/thành công/đang giao/đã giao/đã hoàn), số tiền còn phải trả, link thanh toán khi còn thiếu. - Admin thấy đầy đủ đơn theo campaign + hành động refund/ship/deliver + nút fulfill.
6 · Quyết định thiết kế đã chốt
| # | Quyết định | Lý do |
|---|---|---|
| D1 | MOQ = threshold tier thấp nhất, không thêm column | Tái sử dụng model tiers hiện có; tránh 2 nguồn truth |
| D2 | Auto-settle chạy bằng cron in-process (như ExpiryService) | Phase 0 không có queue riêng; Asynq để Phase sau |
| D3 | "Có hàng" = admin bấm fulfill ở campaign-level → bulk ship đơn đủ tiền | Tránh phải tick từng đơn; vẫn ship/refund riêng khi muốn |
| D4 | Hoàn cọc là ghi nhận thủ công (REFUND_PENDING → REFUNDED bởi admin) | Phase 0 chưa có cổng hoàn tiền tự động |
| D5 | Realtime = polling 15–30s trang detail | SSE là Phase 2 |
| D6 | Tiền settlement nhận qua cùng webhook SePay, phân nhánh theo trạng thái đơn (tách mã GB-XXXXXX từ nội dung CK) | Không thêm endpoint/provider mới |
7 · Bản đồ công việc
| Thành phần | Repo | Việc |
|---|---|---|
| BE | ../gghut-core | Migration enum mới (SHIPPED,DELIVERED,REFUND_PENDING,REFUNDED; campaign FAILED), computed fields (moq, moq_reached, current_unit_price), auto-settle/fail cron, webhook settlement, endpoints admin orders + fulfill, tests |
| Storefront | nextjs-storefront/ | Hiển thị giá hiệu lực + MOQ + polling, gate đăng nhập, lịch sử mua theo trạng thái mới, banner campaign FAILED |
| Admin | nextjs-admin/ | Form default/promo price, bảng đơn + hành động refund/ship/deliver, nút fulfill, badge trạng thái mới |