init git
This commit is contained in:
548
docs/sections/06-luong-xu-ly.md
Normal file
548
docs/sections/06-luong-xu-ly.md
Normal file
@@ -0,0 +1,548 @@
|
||||
---
|
||||
section: "06"
|
||||
title: Thiết kế luồng xử lý chi tiết
|
||||
status: approved
|
||||
version: 2
|
||||
reviewer_notes: ""
|
||||
---
|
||||
|
||||
# 6. Thiết kế luồng xử lý chi tiết (Detailed Design)
|
||||
|
||||
> Đầu vào: `02-phan-tich-yeu-cau.md` (FR-01..FR-27), `03-kien-truc.md` (service boundary, event: `OrderPlaced`, `PaymentConfirmed`, `OrderDelivered`, `CommissionCalculated`, `PayoutScheduled`, `InventoryReserved`, `ReviewEligible`, `LoyaltyPointsEarned`, `NotificationRequested`), `04-api-design.md` (endpoint theo service, v3), `05-thiet-ke-du-lieu.md` (entity/bảng, enum trạng thái, v3).
|
||||
>
|
||||
> **Right-sizing:** do `profile.scale = large`, `hasPayment = true`, `hasPII = true` và mô hình marketplace nhiều bên (Customer, Seller, Admin, CSR, Ops, VNPay/Momo, GHN/GHTK, Ngân hàng), mục này vẽ sequence diagram cho **6 luồng phức tạp/rủi ro cao nhất**: (1) Checkout & thanh toán đa seller, (2) Xử lý đơn & vận chuyển, (3) Đổi trả/tranh chấp, (4) Tính hoa hồng & payout có kỳ giữ tiền, (5) Seller onboarding & KYC, (6) Đăng nhập + MFA/OAuth. Các CRUD đơn giản (wishlist, review, quản lý địa chỉ, cấu hình ngôn ngữ/tiền tệ...) không vẽ sequence riêng vì không có rẽ nhánh nghiệp vụ đáng kể.
|
||||
>
|
||||
> **(v2 — revision theo findings mục 8 và đồng bộ mục 4/5 v3):** bổ sung tối thiểu vào các luồng hiện có — không vẽ lại toàn bộ sequence/class/state diagram đã duyệt: (a) 6.1.5 KYC — Admin xem `KYCDocument` qua pre-signed URL TTL ngắn; (b) 6.1.4 payout — nêu kênh truyền batch file ngân hàng (giả định); (c) ghi chú `audit_log` (mục 5.2.11 v3) tại các hành động nhạy cảm (duyệt/từ chối KYC, cấu hình commission/`holdDays`, quyết định dispute, retry payout, khoá/mở seller); (d) 6.1.6 đăng nhập — bổ sung nhánh khoá tài khoản theo `failed_login_count`/`locked_until` (mục 5.2.1 v3); (e) phản ánh `403 ERR_FORBIDDEN_OWNERSHIP`, chống replay webhook (timestamp ±5 phút + idempotency theo `gatewayTransactionRef`), và OAuth `state`/`409 ERR_ACCOUNT_LINK_REQUIRED` (mục 4 v3) ở 6.1.1 và 6.1.6.
|
||||
|
||||
## 6.1 Sơ đồ tuần tự (Sequence Diagram)
|
||||
|
||||
### 6.1.1 Checkout & thanh toán đa seller (FR-05, FR-06, FR-07, FR-12, FR-13, FR-18)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor Customer
|
||||
participant Web as Web Storefront (Guest/Customer)
|
||||
participant CartOrder as Cart & Order Service
|
||||
participant Catalog as Catalog & Inventory Service
|
||||
participant Payment as Payment Service
|
||||
participant VNPay as VNPay/Momo
|
||||
participant MQ as Message Broker
|
||||
participant Notify as Notification Service
|
||||
participant Commission as Commission & Payout Service
|
||||
|
||||
Customer->>Web: Xem giỏ hàng, bấm "Đặt hàng"
|
||||
Web->>CartOrder: POST /v1/cart/apply-coupon (nếu có coupon)
|
||||
CartOrder-->>Web: Cart đã áp giảm giá (FR-13)
|
||||
Web->>CartOrder: POST /v1/checkout (Idempotency-Key, shippingAddressId, paymentMethod)
|
||||
CartOrder->>Catalog: Kiểm tra & giữ tồn kho (reserve) từng ProductVariant trong Cart (BR-02)
|
||||
alt Đủ tồn kho
|
||||
Catalog-->>CartOrder: reserved OK (InventoryReserved)
|
||||
CartOrder->>CartOrder: Tách Cart đa seller thành Order (cha) + nhiều OrderSeller theo seller_id (BR-01)
|
||||
CartOrder->>CartOrder: Lưu Order, OrderSeller, OrderItem (status=pending_payment)
|
||||
CartOrder-->>Web: 201 { parentOrderId, orders[], paymentRedirectUrl? }
|
||||
Web->>Payment: POST /v1/payments (orderId, method, Idempotency-Key)
|
||||
Payment->>VNPay: Khởi tạo giao dịch (redirect URL)
|
||||
VNPay-->>Payment: paymentRedirectUrl
|
||||
Payment-->>Web: paymentRedirectUrl
|
||||
Customer->>VNPay: Thanh toán trên trang gateway
|
||||
VNPay->>Payment: POST /v1/payments/webhooks/vnpay (IPN, checksum)
|
||||
Payment->>Payment: Xác thực chữ ký; kiểm tra timestamp lệch <=5 phút so với giờ nhận (chống replay — quá hạn thì từ chối, 400 ERR_VALIDATION, không xử lý); kiểm tra idempotency theo gatewayTransactionRef (đã ghi nhận trước đó → 200 OK, không lặp side-effect); nếu hợp lệ, cập nhật Payment.status=success (v3 — mục 4.1.6)
|
||||
Payment->>MQ: publish PaymentConfirmed(orderId)
|
||||
MQ->>CartOrder: consume PaymentConfirmed → Order/OrderSeller.status=confirmed
|
||||
MQ->>Catalog: consume PaymentConfirmed → chuyển reserved → trừ kho thật (commit)
|
||||
MQ->>Commission: consume PaymentConfirmed → tạo CommissionTransaction (BR-03, tạm ghi nhận, chưa release)
|
||||
MQ->>Notify: consume PaymentConfirmed → gửi email/SMS xác nhận đơn hàng (FR-12)
|
||||
else Không đủ tồn kho
|
||||
Catalog-->>CartOrder: 409 ERR_CONFLICT (insufficient stock)
|
||||
CartOrder-->>Web: 409 ERR_CONFLICT — yêu cầu điều chỉnh giỏ hàng
|
||||
end
|
||||
Note over Web,Payment: Các endpoint tra cứu sau đó — GET /v1/orders/{orderId}, GET /v1/payments/{paymentId} — đều kiểm tra ownership (customerId trong JWT phải khớp chủ đơn); không khớp → 403 ERR_FORBIDDEN_OWNERSHIP (mục 4.1.1, v3)
|
||||
```
|
||||
|
||||
### 6.1.2 Xử lý đơn & vận chuyển (FR-19, FR-26, FR-14 điểm thưởng, FR-22 khởi tạo hold)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor Seller
|
||||
actor Ops as Ops/Warehouse
|
||||
participant SellerPortal as Seller Portal
|
||||
participant CartOrder as Cart & Order Service
|
||||
participant Shipping as Shipping & Fulfillment Service
|
||||
participant GHN as GHN/GHTK
|
||||
participant MQ as Message Broker
|
||||
participant Commission as Commission & Payout Service
|
||||
participant Loyalty as Promotion & Loyalty Service
|
||||
participant Notify as Notification Service
|
||||
|
||||
Seller->>SellerPortal: Xác nhận đơn con của mình
|
||||
SellerPortal->>CartOrder: PATCH /v1/seller/orders/{orderId}/status (confirmed)
|
||||
CartOrder->>CartOrder: Ghi OrderStatusHistory, OrderSeller.status=confirmed
|
||||
CartOrder->>MQ: publish OrderSellerConfirmed
|
||||
MQ->>Shipping: consume → tạo yêu cầu fulfillment (status=created)
|
||||
Ops->>Shipping: GET/PATCH /v1/ops/orders/{orderId}/fulfillment (đóng gói xong → packed)
|
||||
Shipping->>GHN: POST /v1/ops/shipments (tạo vận đơn)
|
||||
GHN-->>Shipping: tracking_number
|
||||
Shipping->>CartOrder: cập nhật OrderSeller.status=shipped (qua event OrderShipped)
|
||||
GHN->>Shipping: POST /v1/webhooks/ghn (cập nhật in_transit/delivered, idempotent)
|
||||
Shipping->>Shipping: Ghi ShipmentEvent, cập nhật Shipment.status
|
||||
alt status=delivered
|
||||
Shipping->>MQ: publish OrderDelivered(orderSellerId, deliveredAt, categoryId)
|
||||
MQ->>CartOrder: consume → OrderSeller.status=delivered
|
||||
MQ->>Commission: consume → tạo PayoutHold, hold_until_date = deliveredAt + holdDays (BR-04)
|
||||
MQ->>Loyalty: consume → tính & ghi LoyaltyTransaction earn (BR-06)
|
||||
MQ->>Notify: consume → thông báo giao hàng thành công cho Customer
|
||||
end
|
||||
Note over GHN,Shipping: Nếu GHN timeout — fallback thử GHTK hoặc đưa vào hàng đợi Ops xử lý thủ công (BR-15, theo mục 3.4)
|
||||
```
|
||||
|
||||
### 6.1.3 Đổi trả & xử lý tranh chấp (FR-09, FR-25)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor Customer
|
||||
participant Web as Web Storefront
|
||||
participant CartOrder as Cart & Order Service
|
||||
participant MQ as Message Broker
|
||||
actor CSR
|
||||
participant AdminBO as Admin/CSR Backoffice
|
||||
participant Payment as Payment Service
|
||||
participant Commission as Commission & Payout Service
|
||||
participant Notify as Notification Service
|
||||
|
||||
Customer->>Web: Yêu cầu đổi trả cho Order đã giao
|
||||
Web->>CartOrder: POST /v1/orders/{orderId}/return-requests (reason)
|
||||
CartOrder->>CartOrder: Tạo ReturnRequest (status=requested)
|
||||
CartOrder->>MQ: publish ReturnRequested
|
||||
MQ->>Commission: consume → nếu PayoutHold liên quan đang holding, chuyển release_status=disputed_frozen (BR-14a)
|
||||
MQ->>CartOrder: (nếu seller từ chối/không phản hồi trong SLA) tạo Dispute (status=open, assigned_csr_id=null)
|
||||
CSR->>AdminBO: GET /v1/admin/disputes (danh sách cần xử lý)
|
||||
CSR->>AdminBO: Điều tra: xem lịch sử Order, trao đổi Customer/Seller (status=investigating)
|
||||
CSR->>CartOrder: PATCH /v1/admin/disputes/{disputeId} (quyết định: refund/reject/escalate)
|
||||
Note over CartOrder,MQ: Quyết định dispute (refund/reject/escalate) phát event ghi audit_log tại Audit & Compliance Service (actor=CSR/Admin, action=dispute_decision, resource=disputeId) — mục 5.2.11 (v3)
|
||||
alt Quyết định hoàn tiền (refund)
|
||||
CartOrder->>MQ: publish DisputeResolved(decision=refund)
|
||||
MQ->>Payment: consume → khởi tạo hoàn tiền qua VNPay/Momo API (hoặc điều chỉnh COD)
|
||||
MQ->>Commission: consume → PayoutHold liên quan không được release (loại khỏi kỳ payout — xem Finding mục 6.6)
|
||||
CartOrder->>CartOrder: ReturnRequest.status=refunded, OrderSeller.status=returned
|
||||
else Từ chối khiếu nại (reject)
|
||||
CartOrder->>MQ: publish DisputeResolved(decision=reject)
|
||||
MQ->>Commission: consume → PayoutHold.release_status=holding (chờ đến hold_until_date để release bình thường)
|
||||
CartOrder->>CartOrder: ReturnRequest.status=rejected
|
||||
end
|
||||
MQ->>Notify: consume DisputeResolved → thông báo kết quả cho Customer và Seller
|
||||
```
|
||||
|
||||
### 6.1.4 Tính hoa hồng & payout định kỳ có kỳ giữ tiền (FR-20, FR-21, FR-22)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Scheduler as Weekly Payout Job (cron)
|
||||
participant Commission as Commission & Payout Service
|
||||
participant DB as Commission & Payout DB
|
||||
actor Admin as Platform Admin
|
||||
participant AdminBO as Admin Backoffice
|
||||
participant Bank as Ngân hàng (batch transfer)
|
||||
participant Notify as Notification Service
|
||||
actor Seller
|
||||
participant SellerPortal as Seller Portal
|
||||
|
||||
Scheduler->>Commission: Trigger payout run (hàng tuần)
|
||||
Commission->>DB: SELECT PayoutHold WHERE release_status='holding' AND hold_until_date<=today
|
||||
loop Với mỗi PayoutHold đủ điều kiện
|
||||
Commission->>DB: Kiểm tra không có Dispute đang open/investigating cho order_seller liên quan
|
||||
alt Không có tranh chấp mở
|
||||
Commission->>DB: release_status='released'; cộng CommissionTransaction.net_amount vào batch payout của seller
|
||||
else Có tranh chấp mở
|
||||
Commission->>DB: giữ nguyên 'holding' (chờ CSR xử lý xong — xem 6.1.3)
|
||||
end
|
||||
end
|
||||
Commission->>DB: Tạo Payout (status=scheduled) theo seller, period_start/period_end
|
||||
Commission->>DB: Lấy SellerBankAccount đang active
|
||||
Commission->>Bank: Gửi batch file chuyển khoản (Payout.status=processing) — kênh truyền: SFTP + mã hoá PGP hoặc API HTTPS của ngân hàng đối tác (v3, xem ghi chú giả định bên dưới)
|
||||
Bank-->>Commission: Kết quả xử lý batch (ack/reject theo dòng)
|
||||
alt Chuyển khoản thành công
|
||||
Commission->>DB: Payout.status='paid', paid_at=now
|
||||
Commission->>Notify: publish PayoutCompleted → thông báo Seller
|
||||
else Thất bại (sai thông tin NH, bị NH từ chối)
|
||||
Commission->>DB: Payout.status='failed'
|
||||
Commission->>AdminBO: Cảnh báo Admin — không tự động thử lại (tránh double-payout)
|
||||
Admin->>Commission: POST /v1/admin/payouts/{payoutId}/retry (thủ công, sau khi xác minh)
|
||||
Note over Commission: Retry payout ghi audit_log (actor=Admin, action=payout_retry, resource=payoutId) — mục 5.2.11 (v3)
|
||||
end
|
||||
Seller->>SellerPortal: GET /v1/seller/payouts (xem lịch sử/trạng thái)
|
||||
Admin->>AdminBO: GET /v1/admin/payouts (giám sát toàn sàn theo kỳ)
|
||||
```
|
||||
|
||||
> **(v3)** Kênh truyền batch file payout tới ngân hàng: **giả định** dùng SFTP với mã hoá PGP cho file định dạng chuẩn ngân hàng nội địa, hoặc API HTTPS của ngân hàng đối tác (nếu ngân hàng hỗ trợ) — **ngân hàng đối tác và chuẩn kết nối cụ thể chưa được chốt trong brief**, cần chủ dự án/đối tác ngân hàng xác nhận trước go-live (ảnh hưởng cách hiện thực `Commission & Payout Service` gọi ra bên ngoài, xem mục 3 tích hợp bên thứ ba).
|
||||
>
|
||||
> **(v3)** Hành động cấu hình `CommissionRule`/`holdDays` (`PUT /v1/admin/commission-rules/{categoryId}`, mục 4.1.8) và khoá/mở khoá `Seller` (`PATCH /v1/admin/sellers/{sellerId}/status`, mục 4.1.7) là CRUD đơn giản nên không có sequence diagram riêng, nhưng đều là hành động nhạy cảm — mỗi lần ghi đều phát event ghi `audit_log` (actor, `before_json`/`after_json`, resource) tại Audit & Compliance Service, theo mục 5.2.11.
|
||||
|
||||
### 6.1.5 Seller onboarding & KYC (FR-17, FR-23)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor Seller
|
||||
participant SellerPortal as Seller Portal
|
||||
participant SellerSvc as Seller Management Service
|
||||
participant S3 as S3 (KYC bucket)
|
||||
actor Admin
|
||||
participant AdminBO as Admin Backoffice
|
||||
participant MQ as Message Broker
|
||||
participant Notify as Notification Service
|
||||
|
||||
Seller->>SellerPortal: Đăng ký gian hàng
|
||||
SellerPortal->>SellerSvc: POST /v1/sellers/register
|
||||
SellerSvc->>SellerSvc: Tạo Seller (status=pending_kyc)
|
||||
Seller->>SellerPortal: Upload giấy phép kinh doanh/CMND
|
||||
SellerPortal->>SellerSvc: POST /v1/sellers/{sellerId}/kyc-documents (multipart)
|
||||
SellerSvc->>S3: Lưu file (mã hoá at-rest)
|
||||
SellerSvc->>SellerSvc: Tạo KYCDocument (verified_status=pending) cho từng document_type bắt buộc
|
||||
Admin->>AdminBO: GET /v1/admin/sellers?status=pending_kyc
|
||||
Admin->>SellerSvc: GET /v1/admin/sellers/{sellerId}/kyc-documents/{documentId}/view-url (v3 — yêu cầu link xem tài liệu; endpoint cần bổ sung ở mục 4, xem Finding 6.6)
|
||||
SellerSvc->>S3: Sinh pre-signed URL, quyền đọc duy nhất object đó, TTL <= 5 phút (v3)
|
||||
S3-->>SellerSvc: presignedUrl (hết hạn sau tối đa 300 giây)
|
||||
SellerSvc-->>Admin: 200 { viewUrl, expiresInSeconds<=300 } — Admin không được cấp quyền truy cập trực tiếp bucket/object storage
|
||||
Admin->>AdminBO: Mở viewUrl trong trình duyệt, đối chiếu từng KYCDocument thủ công (không auto-approve — BR-13)
|
||||
Admin->>SellerSvc: PATCH /v1/admin/sellers/{sellerId}/kyc-review (approved|rejected, reason)
|
||||
Note over SellerSvc,MQ: Quyết định duyệt/từ chối KYC phát event ghi audit_log (actor=Admin, action=kyc_review, resource=kycDocumentId/sellerId) — mục 5.2.11 (v3)
|
||||
alt Tất cả document bắt buộc đều verified
|
||||
SellerSvc->>SellerSvc: Seller.status=active
|
||||
SellerSvc->>MQ: publish SellerApproved
|
||||
else Có document bị rejected
|
||||
SellerSvc->>SellerSvc: Seller.status=rejected (giữ pending_kyc nếu seller có thể nộp lại)
|
||||
SellerSvc->>MQ: publish SellerRejected(reason)
|
||||
end
|
||||
MQ->>Notify: gửi email kết quả duyệt cho Seller
|
||||
Seller->>SellerPortal: GET /v1/sellers/{sellerId}/kyc-status (tự kiểm tra)
|
||||
```
|
||||
|
||||
### 6.1.6 Đăng nhập, MFA và Social login (FR-01, FR-02, FR-27)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor User as Customer/Seller/Admin
|
||||
participant Web as Web/Seller/Admin Portal
|
||||
participant IDSvc as Identity & Access Service
|
||||
participant DB as Identity DB
|
||||
|
||||
User->>Web: Nhập email/password
|
||||
Web->>IDSvc: POST /v1/auth/login
|
||||
IDSvc->>DB: Đọc user_account (password_hash, role, mfa_enabled, failed_login_count, locked_until) — v3
|
||||
alt Tài khoản đang bị khoá (locked_until > now) — v3
|
||||
IDSvc-->>Web: 401 sai thông tin đăng nhập / tài khoản tạm khoá do đăng nhập sai nhiều lần (mã lỗi cụ thể và khoảng thời gian khoá do mục 8 — security-architect quy định)
|
||||
else Không bị khoá
|
||||
IDSvc->>IDSvc: So khớp password_hash
|
||||
alt Mật khẩu sai — v3
|
||||
IDSvc->>DB: Tăng failed_login_count += 1, ghi last_failed_login_at=now
|
||||
alt failed_login_count vượt ngưỡng cho phép (ngưỡng cụ thể do mục 8 quy định) — v3
|
||||
IDSvc->>DB: Đặt locked_until = now + khoảng thời gian khoá (khoảng thời gian do mục 8 quy định)
|
||||
end
|
||||
IDSvc-->>Web: 401 sai thông tin đăng nhập
|
||||
else Mật khẩu đúng
|
||||
IDSvc->>DB: Reset failed_login_count=0, last_failed_login_at=null — v3
|
||||
alt role=platform_admin (bắt buộc MFA) hoặc role=seller có mfa_enabled=true
|
||||
IDSvc-->>Web: 200 { mfaRequired:true, mfaChallengeToken, mfaMethod }
|
||||
Web->>User: Yêu cầu nhập mã OTP
|
||||
User->>Web: Nhập OTP (TOTP/SMS)
|
||||
Web->>IDSvc: POST /v1/auth/mfa/challenge (mfaChallengeToken, otp)
|
||||
IDSvc->>DB: Xác minh MFA_DEVICE.secret_encrypted
|
||||
IDSvc-->>Web: 200 { accessToken, refreshToken }
|
||||
else Không cần MFA (Customer, hoặc Seller chưa bật MFA)
|
||||
IDSvc-->>Web: 200 { accessToken, refreshToken }
|
||||
end
|
||||
end
|
||||
end
|
||||
Note over User,IDSvc: Luồng Social login (FR-02, v3): User chọn "Đăng nhập Google/Facebook" → redirect OAuth2 kèm tham số state (sinh ngẫu nhiên, lưu tạm phía server) → provider → POST /v1/auth/oauth/{provider}/callback (state, code) → IDSvc xác thực state khớp giá trị đã phát hành (thiếu/không khớp → 400 ERR_OAUTH_STATE_INVALID, chống CSRF) → nếu email trả về từ provider đã có tài khoản email/password đăng ký sẵn (chưa liên kết OAuth), KHÔNG tự động merge (no auto-merge) → trả 409 ERR_ACCOUNT_LINK_REQUIRED, yêu cầu xác minh sở hữu email trước khi liên kết → nếu email chưa tồn tại, tạo Customer mới liên kết OAuthIdentity → phát hành accessToken/refreshToken tương tự trên
|
||||
```
|
||||
|
||||
## 6.2 Sơ đồ lớp (Class Diagram) & Trạng thái (State Diagram)
|
||||
|
||||
### 6.2.1 Class Diagram — Cart & Order domain (FR-05, FR-06, FR-08, FR-09)
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Cart {
|
||||
+UUID id
|
||||
+UUID customerId
|
||||
+String sessionId
|
||||
+String status
|
||||
+addItem(productVariantId, sellerId, quantity)
|
||||
+applyCoupon(code)
|
||||
}
|
||||
class CartItem {
|
||||
+UUID id
|
||||
+UUID cartId
|
||||
+UUID productVariantId
|
||||
+UUID sellerId
|
||||
+int quantity
|
||||
+Decimal unitPriceSnapshot
|
||||
}
|
||||
class Order {
|
||||
+UUID id
|
||||
+UUID customerId
|
||||
+String orderNumber
|
||||
+Decimal totalAmount
|
||||
+String status
|
||||
+splitBySeller() OrderSeller[]
|
||||
+cancel()
|
||||
}
|
||||
class OrderSeller {
|
||||
+UUID id
|
||||
+UUID orderId
|
||||
+UUID sellerId
|
||||
+String subOrderNumber
|
||||
+Decimal subtotalAmount
|
||||
+String status
|
||||
+confirm()
|
||||
+markShipped()
|
||||
+markDelivered()
|
||||
}
|
||||
class OrderItem {
|
||||
+UUID id
|
||||
+UUID orderSellerId
|
||||
+UUID productVariantId
|
||||
+int quantity
|
||||
+Decimal unitPrice
|
||||
+Decimal lineTotal
|
||||
}
|
||||
class ReturnRequest {
|
||||
+UUID id
|
||||
+UUID orderSellerId
|
||||
+UUID customerId
|
||||
+String status
|
||||
+String reason
|
||||
}
|
||||
class Dispute {
|
||||
+UUID id
|
||||
+UUID orderSellerId
|
||||
+String raisedBy
|
||||
+UUID assignedCsrId
|
||||
+String status
|
||||
+resolve(decision)
|
||||
}
|
||||
|
||||
Cart "1" *-- "many" CartItem
|
||||
Order "1" *-- "many" OrderSeller
|
||||
OrderSeller "1" *-- "many" OrderItem
|
||||
OrderSeller "1" o-- "0..1" ReturnRequest
|
||||
OrderSeller "1" o-- "0..*" Dispute
|
||||
```
|
||||
|
||||
### 6.2.2 Class Diagram — Commission & Payout domain (FR-20, FR-21, FR-22)
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class CommissionRule {
|
||||
+UUID id
|
||||
+UUID categoryId
|
||||
+Decimal commissionPercent
|
||||
+Date effectiveFrom
|
||||
+Date effectiveTo
|
||||
+calculateCommission(grossAmount) Decimal
|
||||
}
|
||||
class CommissionTransaction {
|
||||
+UUID id
|
||||
+UUID orderSellerId
|
||||
+UUID sellerId
|
||||
+Decimal grossAmount
|
||||
+Decimal commissionAmount
|
||||
+Decimal netAmount
|
||||
}
|
||||
class Payout {
|
||||
+UUID id
|
||||
+UUID sellerId
|
||||
+Date periodStart
|
||||
+Date periodEnd
|
||||
+Decimal totalNetAmount
|
||||
+String status
|
||||
+submitToBank()
|
||||
+markPaid()
|
||||
+markFailed()
|
||||
}
|
||||
class PayoutHold {
|
||||
+UUID id
|
||||
+UUID commissionTransactionId
|
||||
+Date holdUntilDate
|
||||
+String releaseStatus
|
||||
+release()
|
||||
+freeze()
|
||||
}
|
||||
class Seller {
|
||||
+UUID id
|
||||
+String status
|
||||
+approve()
|
||||
+suspend()
|
||||
}
|
||||
|
||||
CommissionRule "1" --> "many" CommissionTransaction : applies
|
||||
CommissionTransaction "1" --> "0..1" PayoutHold : held_by
|
||||
CommissionTransaction "many" --> "1" Payout : settled_in
|
||||
Seller "1" --> "many" Payout : receives
|
||||
```
|
||||
|
||||
## 6.3 State Diagram — vòng đời entity nhiều trạng thái
|
||||
|
||||
### 6.3.1 OrderSeller (FR-06, FR-08, FR-09, FR-19)
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> pending: Checkout thành công (Cart & Order Service)
|
||||
pending --> confirmed: Seller xác nhận (PATCH /v1/seller/orders/{orderId}/status) hoặc auto sau PaymentConfirmed
|
||||
pending --> cancelled: Customer huỷ (BR-10) hoặc hết hạn thanh toán
|
||||
confirmed --> cancelled: Customer huỷ trong điều kiện cho phép (BR-10) — Seller/CSR cũng có thể huỷ khi hết hàng
|
||||
confirmed --> packed: Ops đóng gói xong (PATCH /v1/ops/orders/{orderId}/fulfillment)
|
||||
packed --> shipped: Shipping & Fulfillment Service tạo vận đơn GHN/GHTK thành công
|
||||
shipped --> delivered: Webhook GHN/GHTK báo giao thành công
|
||||
delivered --> returned: CSR/Admin duyệt ReturnRequest (refund) — kích hoạt bởi Dispute resolution (FR-25)
|
||||
cancelled --> [*]
|
||||
returned --> [*]
|
||||
delivered --> [*]: Hết thời gian khiếu nại, đơn coi như hoàn tất
|
||||
```
|
||||
|
||||
### 6.3.2 Payment (FR-07)
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> pending: POST /v1/payments khởi tạo giao dịch
|
||||
pending --> success: Webhook VNPay/Momo xác nhận thành công (chữ ký hợp lệ)
|
||||
pending --> failed: Webhook báo thất bại hoặc timeout không có callback (qua job đối soát, mục 3.4)
|
||||
success --> refunded: CSR/Admin duyệt hoàn tiền sau Dispute resolution (FR-25)
|
||||
failed --> [*]
|
||||
success --> [*]
|
||||
refunded --> [*]
|
||||
```
|
||||
|
||||
### 6.3.3 Seller — trạng thái KYC/hoạt động (FR-17, FR-23)
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> pending_kyc: Seller đăng ký (POST /v1/sellers/register)
|
||||
pending_kyc --> active: Admin duyệt toàn bộ KYCDocument bắt buộc (PATCH .../kyc-review, chỉ Admin)
|
||||
pending_kyc --> rejected: Admin từ chối KYC (chỉ Admin), Seller có thể nộp lại → về pending_kyc
|
||||
rejected --> pending_kyc: Seller nộp lại giấy tờ
|
||||
active --> suspended: Admin khoá do vi phạm (PATCH /v1/admin/sellers/{sellerId}/status, chỉ Admin)
|
||||
suspended --> active: Admin mở khoá sau xác minh (chỉ Admin)
|
||||
```
|
||||
|
||||
> **(v3)** Mọi chuyển trạng thái do Admin thực hiện ở trên (`pending_kyc→active`, `pending_kyc→rejected`, `active↔suspended`) đều phát event ghi `audit_log` (actor=Admin, action tương ứng, resource=sellerId) tại Audit & Compliance Service — mục 5.2.11.
|
||||
|
||||
### 6.3.4 ReturnRequest (FR-09)
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> requested: Customer gửi yêu cầu (POST .../return-requests)
|
||||
requested --> approved: CSR/Admin hoặc Seller đồng ý đổi trả
|
||||
requested --> rejected: CSR/Admin hoặc Seller từ chối (có thể mở Dispute nếu Customer không đồng ý)
|
||||
approved --> refunded: Payment Service hoàn tất hoàn tiền
|
||||
rejected --> [*]
|
||||
refunded --> [*]
|
||||
```
|
||||
|
||||
### 6.3.5 Dispute (FR-25)
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> open: Tạo tự động khi Seller từ chối/không phản hồi ReturnRequest trong SLA, hoặc Customer/Seller khiếu nại trực tiếp
|
||||
open --> investigating: CSR nhận xử lý (assigned_csr_id được gán)
|
||||
investigating --> resolved: CSR/Admin ra quyết định (refund/reject) — chỉ CSR/Admin
|
||||
investigating --> escalated: CSR chuyển cấp cao hơn (Admin) khi vượt thẩm quyền
|
||||
escalated --> resolved: Admin ra quyết định cuối cùng
|
||||
resolved --> [*]
|
||||
```
|
||||
|
||||
### 6.3.6 Payout & PayoutHold (FR-22)
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> holding: PayoutHold tạo khi nhận event OrderDelivered (hold_until_date = deliveredAt + holdDays, BR-04)
|
||||
holding --> disputed_frozen: Dispute được mở cho order_seller liên quan trước hold_until_date (chỉ hệ thống, tự động qua event)
|
||||
disputed_frozen --> holding: Dispute resolved với quyết định "reject" (từ chối khiếu nại) — chờ đến hold_until_date bình thường
|
||||
holding --> released: Job payout hàng tuần release khi hold_until_date đã qua và không còn Dispute mở (chỉ hệ thống/Commission & Payout Service)
|
||||
disputed_frozen --> [*]: Dispute resolved với quyết định "refund" — hoa hồng bị loại khỏi payout vĩnh viễn (xem Finding 6.4 — cần bổ sung trạng thái kết thúc rõ ràng ở mục 5)
|
||||
```
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> scheduled: Commission & Payout Service tạo Payout theo kỳ (chỉ hệ thống, job hàng tuần)
|
||||
scheduled --> processing: Gửi batch file chuyển khoản tới Ngân hàng
|
||||
processing --> paid: Ngân hàng xác nhận chuyển thành công
|
||||
processing --> failed: Ngân hàng từ chối/lỗi định dạng
|
||||
failed --> processing: Admin xác nhận thủ công và gọi POST /v1/admin/payouts/{payoutId}/retry (chỉ Admin, không tự động)
|
||||
paid --> [*]
|
||||
```
|
||||
|
||||
## 6.4 Logic nghiệp vụ (Business Rules)
|
||||
|
||||
| Mã | FR liên quan | Mô tả quy tắc |
|
||||
|---|---|---|
|
||||
| BR-01 | FR-06 | **Tách đơn theo seller:** khi checkout, `Cart` (nhiều `CartItem` từ nhiều seller) được nhóm theo `seller_id`; mỗi nhóm sinh ra một `OrderSeller` con thuộc `Order` cha; `Order.totalAmount` = tổng `OrderSeller.subtotalAmount`; mỗi `OrderSeller` có vòng đời trạng thái độc lập (xem 6.3.1) vì mỗi seller xử lý/giao hàng riêng. |
|
||||
| BR-02 | FR-05, FR-06, FR-18 | **Giữ tồn kho khi checkout (chống oversell):** tại thời điểm `POST /v1/checkout`, hệ thống tăng `inventory_stock.quantity_reserved` và kiểm tra `quantity_available - quantity_reserved >= quantity` cho từng `ProductVariant`; nếu không đủ, trả `409 ERR_CONFLICT` trước khi tạo `Order`. Sau khi `PaymentConfirmed`, phần reserved được commit trừ vào `quantity_available` thật; nếu thanh toán thất bại/timeout, phần reserved được nhả lại (release) sau một khoảng thời gian chờ. |
|
||||
| BR-03 | FR-21 | **Tính hoa hồng:** `commissionAmount = orderItem.lineTotal × commissionRule.commissionPercent / 100`, trong đó `commissionRule` là bản ghi `CommissionRule` có `effective_from <= orderDate` và (`effective_to` là null hoặc `>= orderDate`) cho `category_id` tương ứng sản phẩm; `netAmount = grossAmount − commissionAmount`. Nếu một `Category` chưa có `CommissionRule` nào hiệu lực, hệ thống chặn seller đăng bán sản phẩm thuộc category đó cho tới khi Admin cấu hình (ràng buộc bổ sung, cần Admin xác nhận trước go-live). |
|
||||
| BR-04 | FR-22 | **Kỳ giữ tiền (payout hold) — chốt giá trị mặc định + cấu hình theo ngành hàng:** brief chỉ xác nhận cơ chế "3-7 ngày sau giao hàng thành công" như một khoảng, không có giá trị cụ thể. Để Commission & Payout Service vận hành được, thiết kế chốt: **giá trị mặc định toàn sàn = 5 ngày** (điểm giữa khoảng 3-7, cân bằng giữa bảo vệ quyền lợi đổi trả của khách và dòng tiền của seller), và **cho phép Admin cấu hình số ngày hold khác nhau theo từng `Category`** (VD ngành hàng tỷ lệ đổi trả cao như thời trang có thể đặt 7 ngày; ngành hàng ít đổi trả như thực phẩm có thể đặt 3 ngày). Pseudo-code: <br>`holdDays = CommissionRule.findByCategory(categoryId).holdDays` <br>`if holdDays is null: holdDays = PLATFORM_DEFAULT_HOLD_DAYS # = 5`<br>`PayoutHold.hold_until_date = OrderDelivered.deliveredAt + holdDays days` <br>**Đây là giả định mặc định cần chủ dự án xác nhận** trước go-live (số ngày cụ thể + có nên giới hạn admin trong khoảng 3-7 hay cho phép vượt khoảng cho ngành hàng đặc thù) — xem `openQuestions` và Finding bên dưới (cần bổ sung cột `hold_days` ở mục 5 và field tương ứng ở endpoint mục 4). |
|
||||
| BR-05 | FR-22 | **Điều kiện release payout:** job hàng tuần chỉ release `PayoutHold` khi `hold_until_date <= ngày chạy job` **và** không tồn tại `Dispute` ở trạng thái `open`/`investigating` cho `OrderSeller` liên quan; nếu có Dispute mở, giữ nguyên `holding` (hoặc chuyển `disputed_frozen`) cho đến khi Dispute được `resolved`. Một `Payout` gộp toàn bộ `CommissionTransaction.netAmount` đã released trong kỳ của một seller thành một lần chuyển khoản (không chuyển riêng từng đơn) — theo brief "payout hàng tuần". |
|
||||
| BR-06 | FR-14 | **Tích điểm loyalty:** `pointsEarned = floor(orderSeller.subtotalAmount / 10000) × 1`, ghi nhận khi nhận event `OrderDelivered` (không tích điểm khi mới đặt hàng, tránh gian lận huỷ đơn sau khi tích). *Giả định cần xác nhận:* brief ghi "1 điểm/10.000đ giá trị đơn hàng" nhưng không nói rõ tính trên `Order` cha hay từng `OrderSeller`, và có trừ phí vận chuyển/giảm giá coupon hay không — thiết kế tạm tính trên `subtotalAmount` (đã trừ giảm giá) của từng `OrderSeller`, chưa gồm phí ship — xem `openQuestions`. |
|
||||
| BR-07 | FR-14 | **Xếp hạng thành viên (tier):** `LoyaltyAccount.total_spend_12m` là tổng chi tiêu (theo `subtotalAmount` các đơn `delivered`) trong cửa sổ trượt 12 tháng gần nhất, được tính lại bởi batch job định kỳ (đề xuất: hằng đêm) vì đơn hàng cũ hơn 12 tháng phải rớt khỏi cửa sổ tính toán, không chỉ cộng dồn một chiều. Tier được gán theo ngưỡng `MembershipTier.min_spend_threshold` (Bạc < Vàng < Kim Cương). *Giả định cần xác nhận:* brief xác nhận có 3 hạng nhưng **không cho số VND ngưỡng cụ thể** cho từng hạng — xem `openQuestions`. |
|
||||
| BR-08 | FR-14 | **Đổi điểm lấy giảm giá:** `100 điểm = 10.000đ`; chỉ cho đổi theo bội số 100 điểm; điểm đổi được áp làm giảm giá cho `Cart`/`Order` hiện tại qua `POST /v1/customers/me/loyalty/redeem`, ghi `LoyaltyTransaction(type=redeem, points=-N)`; không cho đổi vượt quá `points_balance` hiện có. |
|
||||
| BR-09 | FR-13 | **Điều kiện áp dụng Promotion/coupon:** `promotion.status='active'`, `valid_from <= now <= valid_to`, số lượt đã dùng (đếm từ `promotion_usage`) `< usage_limit` (nếu có), và `cart.subtotal >= min_order_amount` (nếu có). Giảm giá tính theo `type` (`percent`: `value%` trên subtotal; `fixed_amount`: trừ thẳng `value`, không âm). Mỗi coupon chỉ áp dụng một lần cho một `Order` (`UNIQUE(promotion_id, order_id)`). |
|
||||
| BR-10 | FR-08 | **Điều kiện huỷ đơn (Customer tự huỷ):** chỉ cho phép khi `OrderSeller.status` ∈ {`pending`, `confirmed`} (chưa đóng gói); từ `packed` trở đi, Customer phải gửi yêu cầu qua đổi trả/khiếu nại (FR-09/FR-25) thay vì huỷ trực tiếp. *Giả định:* brief/FR-08 chỉ nói "huỷ đơn (trong điều kiện cho phép)" mà không định nghĩa ngưỡng chính xác — mốc `packed` là giả định hợp lý theo luồng vận hành (mục 6.1.2), cần chủ dự án xác nhận. |
|
||||
| BR-11 | FR-11 | **Điều kiện được đánh giá sản phẩm:** Customer chỉ được tạo `Review` cho một `order_item_id` khi `OrderSeller.status = delivered` (đã nhận hàng) và tồn tại `order_item` thuộc `customer_id` đó; ràng buộc `UNIQUE(customer_id, order_item_id)` đảm bảo mỗi lượt mua chỉ đánh giá một lần (khớp mục 5.2.8). |
|
||||
| BR-12 | FR-27 | **Chính sách MFA:** `role='platform_admin'` → bắt buộc `mfa_enabled=true`, chặn hoàn toàn truy cập scope `admin:*` cho đến khi hoàn tất `mfa/enroll`; `role='seller'` → khuyến khích, không chặn đăng nhập nhưng Seller Portal hiển thị nhắc bật MFA liên tục cho đến khi bật; `role='customer'` → không áp dụng MFA ở MVP. **(v3)** Ngoài MFA, đăng nhập sai mật khẩu liên tiếp làm tăng `user_account.failed_login_count`; vượt ngưỡng (do mục 8 quy định) → đặt `locked_until` tạm khoá đăng nhập — xem sequence 6.1.6. |
|
||||
| BR-13 | FR-17 | **Duyệt KYC thủ công, không auto-approve:** `Seller.status` chỉ chuyển `active` khi **toàn bộ** `KYCDocument` bắt buộc (`business_license`, `id_card_front`, `id_card_back`) có `verified_status='verified'`, mỗi tài liệu được một Admin xem xét và duyệt riêng lẻ (không có quy tắc tự động duyệt theo brief — marketplace xác nhận "admin duyệt thủ công"). Nếu bất kỳ tài liệu nào `rejected`, `Seller.status='rejected'` kèm `reason`, Seller có thể nộp lại. **(v3 — theo review mục 8)** Admin xem nội dung `KYCDocument` qua pre-signed URL sinh bởi `Seller Management Service`, TTL tối đa 5 phút, không truy cập trực tiếp object storage; mọi quyết định duyệt/từ chối ghi `audit_log` (xem sequence 6.1.5). |
|
||||
| BR-14 | FR-09, FR-25 | **Xử lý tranh chấp — nguyên tắc chung (không có công thức hoàn tiền cụ thể trong brief):** (a) khi `ReturnRequest` được tạo hoặc `Dispute` mở, `PayoutHold` liên quan (nếu còn `holding`) được tự động chuyển `disputed_frozen` để tránh giải ngân trước khi có quyết định cuối; (b) quyết định `refund`/`reject` chỉ do CSR/Admin thực hiện qua `PATCH /v1/admin/disputes/{disputeId}` (ghi `audit_log`, xem 6.1.3); (c) khi `refund`, `Payment.status` chuyển `refunded` và hoa hồng tương ứng bị loại khỏi payout. **Brief không quy định**: mức hoàn tiền (toàn phần/một phần theo tỷ lệ đã sử dụng), ai chịu phí vận chuyển hoàn trả, và SLA phản hồi của seller trước khi hệ thống tự mở Dispute — đây là **openQuestions**, không tự đặt công thức cụ thể. |
|
||||
| BR-15 | FR-26 | **Fallback vận chuyển:** khi tạo vận đơn qua GHN timeout/lỗi sau tối đa 3 lần retry (theo mục 3.4), hệ thống thử tạo lại qua GHTK nếu khu vực giao hàng được GHTK hỗ trợ; nếu cả hai đều lỗi, đưa vào hàng đợi để Ops xử lý thủ công, không chặn trạng thái `OrderSeller` (vẫn giữ `confirmed`/`packed` chờ xử lý). |
|
||||
|
||||
## 6.5 Ma trận truy vết bổ sung cho mục 2.4
|
||||
|
||||
| FR | Sequence/State/Business Rule liên quan |
|
||||
|---|---|
|
||||
| FR-01 | 6.1.6 (Sequence đăng nhập) |
|
||||
| FR-02 | 6.1.6 (Social login) |
|
||||
| FR-05 | 6.1.1 (Checkout), BR-02 |
|
||||
| FR-06 | 6.1.1, BR-01, State 6.3.1 |
|
||||
| FR-07 | 6.1.1, State 6.3.2 |
|
||||
| FR-08 | State 6.3.1, BR-10 |
|
||||
| FR-09 | 6.1.3, State 6.3.4, BR-14 |
|
||||
| FR-11 | BR-11 |
|
||||
| FR-12 | 6.1.1, 6.1.2 (Notification qua event) |
|
||||
| FR-13 | 6.1.1, BR-09 |
|
||||
| FR-14 | 6.1.2, BR-06, BR-07, BR-08 |
|
||||
| FR-17 | 6.1.5, State 6.3.3, BR-13 |
|
||||
| FR-18 | 6.1.1, BR-02 |
|
||||
| FR-19 | 6.1.2, State 6.3.1 |
|
||||
| FR-20 | 6.1.4, Class Diagram 6.2.2 |
|
||||
| FR-21 | 6.1.4, BR-03 |
|
||||
| FR-22 | 6.1.4, State 6.3.6, BR-04, BR-05 |
|
||||
| FR-23 | State 6.3.3 |
|
||||
| FR-25 | 6.1.3, State 6.3.5, BR-14 |
|
||||
| FR-26 | 6.1.2, BR-15 |
|
||||
| FR-27 | 6.1.6, BR-12 |
|
||||
|
||||
> Các FR không xuất hiện ở trên (FR-03, FR-04, FR-10, FR-15, FR-16, FR-24) là các luồng CRUD/tra cứu/cross-cutting đơn giản, không có rẽ nhánh nghiệp vụ đáng kể cần sequence/state diagram riêng — đã được đặc tả đầy đủ qua endpoint mục 4 và schema mục 5.
|
||||
|
||||
## 6.6 Findings (nhắm mục 4/5)
|
||||
|
||||
1. **[severity: medium — đã giải quyết ở mục 4/5 v3]** Bảng `commission_rule` (mục 5.2.6) trước đây (v1) thiếu cột lưu số ngày hold theo ngành hàng (BR-04); mục 5 v3 đã bổ sung `commission_rule.hold_days` (nullable, fallback mặc định 5 ngày) và mục 4 v3 đã bổ sung field `holdDays` ở `GET/PUT /v1/admin/commission-rules` (mục 4.1.8). Không còn khoảng trống — giữ lại mục này như ghi nhận lịch sử.
|
||||
2. **[severity: medium — đã giải quyết ở mục 4/5 v3]** Enum `payout_hold.release_status` (mục 5.2.6) trước đây (v1) thiếu trạng thái kết thúc rõ ràng cho trường hợp Dispute được duyệt hoàn tiền; mục 5 v3 đã bổ sung trạng thái kết thúc `reversed` để phân biệt với `disputed_frozen` (tạm giữ). Không còn khoảng trống — giữ lại mục này như ghi nhận lịch sử.
|
||||
3. **[severity: low]** Bảng `membership_tier` (mục 5.2.7) có cột `min_spend_threshold` nhưng brief/mục 2 không cung cấp giá trị VND cụ thể cho từng hạng Bạc/Vàng/Kim Cương — cần chủ dự án xác nhận trước khi seed dữ liệu (liên quan BR-07).
|
||||
4. **[severity: low]** FR-08 (mục 2) mô tả "huỷ đơn (trong điều kiện cho phép)" nhưng không định nghĩa ngưỡng trạng thái chính xác — BR-10 tạm giả định mốc `packed`, cần bổ sung rõ trong mục 2 hoặc xác nhận với chủ dự án.
|
||||
5. **[severity: low, mới — v2]** Mục 4 (4.1.7 Seller Management Service) hiện chưa có endpoint cho Admin lấy pre-signed URL để xem nội dung một `KYCDocument` cụ thể (chỉ có `POST .../kyc-documents` để upload và `PATCH .../kyc-review` để duyệt). Theo ghi chú người duyệt (findings bảo mật mục 8), cần bổ sung một endpoint dạng `GET /v1/admin/sellers/{sellerId}/kyc-documents/{documentId}/view-url` trả về `{ viewUrl, expiresInSeconds<=300 }` để Admin không truy cập trực tiếp object storage — xem sequence 6.1.5.
|
||||
6. **[severity: low, mới — v2]** Mục 4 (4.1.3 Identity & Access) chưa có mã lỗi cụ thể cho trường hợp tài khoản bị khoá tạm do vượt ngưỡng đăng nhập sai (`user_account.locked_until`, mục 5.2.1 v3) — hiện chỉ có `401 ERR_AUTH_REQUIRED`/`ERR_AUTH_INVALID_TOKEN`/`ERR_MFA_REQUIRED`. Đề xuất bổ sung mã lỗi riêng (VD `403/423 ERR_ACCOUNT_LOCKED`) tại mục 4 khi ngưỡng/khoảng thời gian khoá được chốt ở mục 8.
|
||||
|
||||
## 6.7 Giả định (Assumptions)
|
||||
|
||||
- Giá trị mặc định kỳ giữ tiền (payout hold) = **5 ngày** (giữa khoảng 3-7 ngày theo brief), có thể cấu hình khác theo từng `Category` — **cần chủ dự án xác nhận** trước go-live (BR-04).
|
||||
- Điểm loyalty tính trên `subtotalAmount` của từng `OrderSeller` (đã trừ giảm giá, chưa gồm phí vận chuyển), kích hoạt khi đơn `delivered` — cần xác nhận với chủ dự án (BR-06).
|
||||
- Ngưỡng huỷ đơn tự phục vụ của Customer dừng ở trạng thái `packed` — cần xác nhận (BR-10).
|
||||
- SLA phản hồi của Seller trước khi hệ thống tự động mở `Dispute` từ một `ReturnRequest` bị từ chối/không phản hồi chưa được định nghĩa số ngày cụ thể — tạm không đặt giá trị cứng, cần chủ dự án cung cấp.
|
||||
- **(v2, mới)** Kênh truyền batch file chuyển khoản payout tới ngân hàng: giả định SFTP + mã hoá PGP hoặc API HTTPS của ngân hàng đối tác — ngân hàng đối tác và chuẩn kết nối cụ thể **chưa được chốt** trong brief, cần xác nhận trước go-live (6.1.4).
|
||||
- **(v2, mới)** Ngưỡng số lần đăng nhập sai (`failed_login_count`) và khoảng thời gian khoá tài khoản (`locked_until`) trong luồng 6.1.6 **chưa có giá trị cụ thể** ở mục này — theo ghi chú người duyệt, đây là phạm vi của mục 8 (security-architect) quy định; thiết kế luồng chỉ mô tả cơ chế (đếm, khoá, mở khoá tự động), không tự đặt số.
|
||||
|
||||
## 6.8 Câu hỏi còn mở (Open Questions)
|
||||
|
||||
- Số ngày hold payout chính xác (đã chốt giá trị mặc định 5 ngày + cơ chế cấu hình theo category ở BR-04) có cần giới hạn cứng trong khoảng 3-7 ngày hay cho phép Admin đặt ngoài khoảng này cho ngành hàng đặc thù?
|
||||
- Ngưỡng chi tiêu 12 tháng (VND) cụ thể cho từng hạng thành viên Bạc/Vàng/Kim Cương là bao nhiêu?
|
||||
- Công thức/mức hoàn tiền khi Dispute được duyệt: hoàn toàn phần hay theo tỷ lệ đã sử dụng? Ai chịu phí vận chuyển hoàn trả (Customer/Seller/Sàn)?
|
||||
- SLA cụ thể (số ngày) để Seller phản hồi một `ReturnRequest` trước khi hệ thống tự động leo thang thành `Dispute`?
|
||||
- Điểm loyalty tính trên giá trị đơn hàng gộp (`Order` cha) hay theo từng `OrderSeller` — và có gồm phí vận chuyển/thuế hay không?
|
||||
- **(v2, mới)** Ngân hàng đối tác cụ thể cho payout và chuẩn kết nối (SFTP+PGP nội bộ hay API HTTPS của ngân hàng) — cần chủ dự án/đối tác ngân hàng xác nhận (6.1.4).
|
||||
- **(v2, mới)** Ngưỡng `failed_login_count` và khoảng thời gian `locked_until` (khoá tài khoản tạm thời) cụ thể là bao nhiêu — cần mục 8 (security-architect) quy định để hoàn thiện luồng 6.1.6 và mã lỗi tương ứng ở mục 4.
|
||||
Reference in New Issue
Block a user