refactor structor folder

This commit is contained in:
Canhchimlac
2026-09-08 12:35:40 +07:00
parent c81f249920
commit 119792967c
40 changed files with 6 additions and 6 deletions

View 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.