Payment Webhooks
1. Tổng quan
| Thuộc tính | Giá trị |
|---|---|
| ID | FEAT-SALE-PAY |
| Trạng thái | Stable |
| Owner | sale-team |
| Phụ thuộc | @nx/mq-pay (nguồn webhook), SaleOrder, SaleCheck, AllocationUsage, CustomerPointService, Kafka producer |
Sale nhận sự kiện thanh toán từ @nx/mq-pay qua HTTP webhook (không auth — tin cậy nội bộ). Flow là router → subordinate-service → transition trạng thái → side effects → phát Kafka.
2. Kiến trúc Three-Service
Trách nhiệm service
| Service | File | Vai trò |
|---|---|---|
PaymentWebhookService | payment-webhook.service.ts (99 dòng) | Chỉ router — trích checkId + orderId từ payload, dispatch đến sub-service phù hợp. Không tự transition trạng thái. |
SaleOrderPaymentWebhookService | sale-order-payment-webhook.service.ts (376 dòng) | Transition cấp order, customer points, cập nhật allocation usage, phát Kafka |
SaleCheckPaymentWebhookService | sale-check-payment-webhook.service.ts (282 dòng) | Transition cấp check; tổng hợp trạng thái check để có thể hoàn tất order |
3. Endpoint Webhook
| Mục | Giá trị |
|---|---|
| Method | POST |
| Path | /v1/api/sale/webhooks/payment |
| Auth | không (tin cậy; bảo mật qua Cilium network policy) |
| Nguồn loại sự kiện | header X-Webhook-Event-Type (ưu tiên) → fallback về body.eventType |
| Controller | PaymentWebhookController |
| Service | PaymentWebhookService.handleEvent |
Schema request (zod)
Nguồn:
src/common/webhook-types.ts—PaymentWebhookRequestSchema.
{
eventType: string,
timestamp: number,
payload: {
timestamp: string,
source?: string,
transaction?: {
id: string, uid: string, status: string,
total: number | string, paid: number | string,
sourceType?: string, // 'SaleOrder' | 'SaleCheck'
sourceId?: string,
metadata?: Record<string, unknown>,
},
attempt?: {
id: string, uid: string, status: string,
amount: number | string, paymentProvider: string,
reason?: string,
metadata?: { source: { id: string, uid: string, type: string } },
},
},
}4. Định tuyến — PaymentWebhookService.handleEvent
Nguồn: payment-webhook.service.ts:36-68.
async handleEvent(opts: { eventType: string; payload }): Promise<boolean> {
const checkId = this._extractCheckId(payload);
const orderId = this._extractOrderId(payload);
if (!checkId && !orderId) return false; // log warn
if (checkId) return SaleCheckPaymentWebhookService.handleCheckEvent({...});
if (orderId) return SaleOrderPaymentWebhookService.handleSaleOrderEvent({...});
return false;
}Logic trích
| Helper | Nguồn | Trả về |
|---|---|---|
_extractCheckId(payload) | payment-webhook.service.ts:71-81 | transaction.sourceId nếu sourceType === 'SaleCheck', ngược lại attempt.metadata.source.id nếu type === 'SaleCheck', ngược lại null |
_extractOrderId(payload) | payment-webhook.service.ts:84-98 | Cùng dạng nhưng đối chiếu với 'SaleOrder' |
Check ưu tiên trước: nếu cả checkId và orderId được phân giải, check service sẽ xử lý.
5. Handler sự kiện SaleOrder
Tất cả ở
SaleOrderPaymentWebhookService. Phương thức là private (_handle*), được gọi bởi switch củahandleSaleOrderEvent.
5.1 ATTEMPT_SUCCESS → PROCESSING → PARTIAL hoặc COMPLETED
Nguồn: _handleOrderPaymentSuccess (dòng 119-194).
| Bước | Hành động |
|---|---|
| 1 | Guard: order.status === PROCESSING (ngược lại log + skip) |
| 2 | Tính isFullyPaid = paid >= total |
| 3 | UPDATE order → COMPLETED (+ completedAt) hoặc PARTIAL (+ partialAt) |
| 4 | WS broadcast ORDER_PAYMENT_UPDATED |
| 5 | Phát Kafka PAYMENT_SUCCESS (post-commit, fire-and-forget) — xem §7 |
| 6 | Nếu isFullyPaid VÀ order.customerId → CustomerPointService.awardPointsForOrder |
| 7 | Tìm row AllocationUsage ACTIVE cho (usageId=order.id, usageType=SALE_ORDER) → bulk UPDATE → SUCCESS |
5.2 ATTEMPT_FAILED / ATTEMPT_EXPIRED / ATTEMPT_CANCELLED → PROCESSING → CANCELLED
Nguồn: _handleOrderPaymentFailed (197-228), _handleOrderPaymentExpired (231-257), _handleOrderPaymentCancelled (260-286).
| Sự kiện | Lý do hủy |
|---|---|
ATTEMPT_FAILED | payload.attempt.reason hoặc 'Payment failed' |
ATTEMPT_EXPIRED | 'Payment expired' |
ATTEMPT_CANCELLED | 'Payment cancelled' |
Cả ba: guard PROCESSING, UPDATE order → CANCELLED + cancelledAt + cancellationReason, WS broadcast.
5.3 TRANSACTION_SETTLED / TRANSACTION_CANCELLED → chỉ log
Nguồn: _handleOrderTransactionSettled (289-294), _handleOrderTransactionCancelled (297-306).
Log thông tin; không đổi trạng thái. Trả về true để webhook được acknowledge.
6. Handler sự kiện SaleCheck
Tất cả ở
SaleCheckPaymentWebhookService. Status SaleCheck là PROCESSING / PARTIAL / COMPLETED / CANCELLED (xem Mô hình miền §4.2).
6.1 ATTEMPT_SUCCESS → PARTIAL hoặc COMPLETED + có thể hoàn tất order
Nguồn: _handleCheckPaymentSuccess (dòng 112-156).
| Bước | Hành động |
|---|---|
| 1 | Guard: check.status === PROCESSING |
| 2 | Tính isFullyPaid = paid >= total |
| 3 | UPDATE check → COMPLETED hoặc PARTIAL |
| 4 | Nếu isFullyPaid: gọi _checkOrderCompletionViaChecks (dòng 237-281) |
6.2 ATTEMPT_FAILED / ATTEMPT_EXPIRED / ATTEMPT_CANCELLED → PROCESSING → CANCELLED
Nguồn: _handleCheckPaymentFailed (159-176), _handleCheckPaymentExpired (179-196), _handleCheckPaymentCancelled (199-216).
Cả ba: guard PROCESSING, UPDATE check → CANCELLED. Không cascade cấp order — check anh em vẫn còn.
6.3 Hoàn tất order qua check — _checkOrderCompletionViaChecks
Bất đối xứng so với order path: order-path còn đánh dấu
AllocationUsage → SUCCESS. Check-path không đặt allocation usage rõ ràng. (TODO trong code; sale team đã biết.)
7. Kafka Emit
_enqueuePaymentSuccess (dòng 311-375) — chỉ trên path order-payment-success, sau khi commit DB.
| Thuộc tính | Giá trị |
|---|---|
| Topic | KafkaTopics.PAYMENT_SUCCESS ('payment.success') |
| Key | order.id |
| Producer | KafkaProducerHelper từ BindingKeys.APPLICATION_KAFKA_PRODUCER |
| Delivery | fire-and-forget; lỗi được log nhưng không rollback DB |
Payload (TSalePaymentSuccess)
{
saleOrderId, saleOrderNumber, saleOrderStatus,
merchantId, saleChannelId,
createdBy, modifiedBy,
payment: {
total, paid, currency, isFullyPaid,
paidAt: ISO,
sessionId?: string, // order.closedInSessionId
finance?: any, // from order.metadata.finance
},
items: Array<{
id, itemType, itemId,
quantity: number,
mode: 'PRODUCT' | 'CUSTOM',
}>,
}Consumers
| Consumer | Làm gì |
|---|---|
@nx/inventory | InventoryWorkerService.handlePaymentSuccess — trừ stock, reserve materials |
@nx/finance | ghi transaction wallet INCOME (TODO: xác nhận tên method) |
8. Bảng tóm tắt Side-effect
| Loại sự kiện | Hiệu ứng path Order | Hiệu ứng path Check |
|---|---|---|
ATTEMPT_SUCCESS (đầy đủ) | order → COMPLETED, allocation → SUCCESS, cộng điểm, phát Kafka | check → COMPLETED; nếu tất cả check COMPLETED → order → COMPLETED + cộng điểm + WS (không phát Kafka trên path này) |
ATTEMPT_SUCCESS (một phần) | order → PARTIAL, phát Kafka | check → PARTIAL |
ATTEMPT_FAILED | order → CANCELLED + lý do | check → CANCELLED |
ATTEMPT_EXPIRED | order → CANCELLED + 'Payment expired' | check → CANCELLED |
ATTEMPT_CANCELLED | order → CANCELLED + 'Payment cancelled' | check → CANCELLED |
TRANSACTION_SETTLED | chỉ log | chỉ log |
TRANSACTION_CANCELLED | chỉ log | chỉ log |
9. Idempotency
| Bề mặt | Cơ chế |
|---|---|
| Webhook handler | Guard trên status === PROCESSING — delivery lặp lại tới order không-PROCESSING bị âm thầm bỏ |
| Customer points | Kiểm tra PointTransactionRepository.existsBySaleOrderId trước khi ghi |
| Kafka emit | không idempotent ở lớp sale; dedup phía consumer (vd: lookup InventoryTracking của inventory) |
10. Flow End-to-End
11. Trang liên quan
- Sale Order
- Check Splitting — chi tiết SaleCheck
- Điểm Khách hàng — flow cộng điểm
- Allocation Usage — transition SUCCESS
- API Sự kiện — spec topic Kafka
- ADR-0003 Payment via webhook