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ộ). Luồng xử lý là router → service cấp dưới → chuyển trạng thái → tác dụng phụ → phát Kafka.
2. Kiến trúc ba service
Trách nhiệm service
| Service | File | Vai trò |
|---|---|---|
PaymentWebhookService | payment-webhook.service.ts (99 dòng) | Chỉ định tuyến - trích checkId + orderId từ payload, điều phối tới service con phù hợp. Không tự chuyển trạng thái. |
SaleOrderPaymentWebhookService | sale-order-payment-webhook.service.ts (376 dòng) | Chuyển trạng thái cấp order, customer points, cập nhật allocation usage, phát Kafka |
SaleCheckPaymentWebhookService | sale-check-payment-webhook.service.ts (282 dòng) | Chuyển trạng thái cấp check; tổng hợp trạng thái các 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) → quay 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 được ưu tiên: nếu phân giải được cả checkId lẫn orderId, check service sẽ xử lý.
5. Handler sự kiện SaleOrder
Tất cả nằm trong
SaleOrderPaymentWebhookService. Các phương thức đều là private (_handle*), được gọi bởi switch tronghandleSaleOrderEvent.
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 + bỏ qua) |
| 2 | Tính isFullyPaid = paid >= total |
| 3 | UPDATE order → COMPLETED (+ completedAt) hoặc PARTIAL (+ partialAt) |
| 4 | Phát WS ORDER_PAYMENT_UPDATED |
| 5 | Phát Kafka PAYMENT_SUCCESS (sau khi commit, kiểu fire-and-forget) - xem §7 |
| 6 | Nếu isFullyPaid VÀ có order.customerId → CustomerPointService.awardPointsForOrder |
| 7 | Tìm các dòng AllocationUsage ACTIVE cho (usageId=order.id, usageType=SALE_ORDER) → UPDATE hàng loạt → 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, phát WS.
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 xác nhận đã nhận.
6. Handler sự kiện SaleCheck
Tất cả nằm trong
SaleCheckPaymentWebhookService. Trạng thái SaleCheck gồm 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 lan truyền lên cấp order - các check anh em vẫn giữ nguyên.
6.3 Hoàn tất order qua check - _checkOrderCompletionViaChecks
Bất đối xứng so với luồng order: luồng order còn đánh dấu
AllocationUsage → SUCCESS. Luồng check không cập nhật allocation usage một cách tường minh. (TODO trong code; sale team đã biết.)
7. Kafka Emit
_enqueuePaymentSuccess (dòng 311-375) - chỉ trên luồng 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 |
| Cách gửi | 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ừ tồn kho, đặt giữ nguyên vật liệu |
@nx/finance | ghi giao dịch wallet INCOME (TODO: xác nhận tên method) |
8. Bảng tóm tắt tác dụng phụ
| Loại sự kiện | Tác dụng luồng Order | Tác dụng luồng 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 luồng 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. Tính idempotent
| Bề mặt | Cơ chế |
|---|---|
| Webhook handler | Guard trên status === PROCESSING - các lần gửi lặp tới order không ở PROCESSING bị âm thầm bỏ qua |
| Customer points | Kiểm tra PointTransactionRepository.existsBySaleOrderId trước khi ghi |
| Kafka emit | không idempotent ở lớp sale; khử trùng lặp phía consumer (vd: tra cứu InventoryTracking của inventory) |
10. Luồng End-to-End
11. Trang liên quan
- Sale Order
- Check Splitting - chi tiết SaleCheck
- Điểm Khách hàng - luồng cộng điểm
- Allocation Usage - transition SUCCESS
- API Sự kiện - spec topic Kafka
- ADR-0003 Payment via webhook