Skip to content

Payment Webhooks

1. Tổng quan

Thuộc tínhGiá trị
IDFEAT-SALE-PAY
Trạng tháiStable
Ownersale-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

ServiceFileVai trò
PaymentWebhookServicepayment-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.
SaleOrderPaymentWebhookServicesale-order-payment-webhook.service.ts (376 dòng)Transition cấp order, customer points, cập nhật allocation usage, phát Kafka
SaleCheckPaymentWebhookServicesale-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ụcGiá trị
MethodPOST
Path/v1/api/sale/webhooks/payment
Authkhông (tin cậy; bảo mật qua Cilium network policy)
Nguồn loại sự kiệnheader X-Webhook-Event-Type (ưu tiên) → fallback về body.eventType
ControllerPaymentWebhookController
ServicePaymentWebhookService.handleEvent

Schema request (zod)

Nguồn: src/common/webhook-types.tsPaymentWebhookRequestSchema.

ts
{
  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.

ts
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

HelperNguồnTrả về
_extractCheckId(payload)payment-webhook.service.ts:71-81transaction.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-98Cù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ủa handleSaleOrderEvent.

5.1 ATTEMPT_SUCCESS → PROCESSING → PARTIAL hoặc COMPLETED

Nguồn: _handleOrderPaymentSuccess (dòng 119-194).

BướcHành động
1Guard: order.status === PROCESSING (ngược lại log + skip)
2Tính isFullyPaid = paid >= total
3UPDATE order → COMPLETED (+ completedAt) hoặc PARTIAL (+ partialAt)
4WS broadcast ORDER_PAYMENT_UPDATED
5Phát Kafka PAYMENT_SUCCESS (post-commit, fire-and-forget) — xem §7
6Nếu isFullyPaidorder.customerIdCustomerPointService.awardPointsForOrder
7Tì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ệnLý do hủy
ATTEMPT_FAILEDpayload.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ướcHành động
1Guard: check.status === PROCESSING
2Tính isFullyPaid = paid >= total
3UPDATE check → COMPLETED hoặc PARTIAL
4Nế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ínhGiá trị
TopicKafkaTopics.PAYMENT_SUCCESS ('payment.success')
Keyorder.id
ProducerKafkaProducerHelper từ BindingKeys.APPLICATION_KAFKA_PRODUCER
Deliveryfire-and-forget; lỗi được log nhưng không rollback DB

Payload (TSalePaymentSuccess)

ts
{
  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

ConsumerLàm gì
@nx/inventoryInventoryWorkerService.handlePaymentSuccess — trừ stock, reserve materials
@nx/financeghi transaction wallet INCOME (TODO: xác nhận tên method)

8. Bảng tóm tắt Side-effect

Loại sự kiệnHiệu ứng path OrderHiệu ứng path Check
ATTEMPT_SUCCESS (đầy đủ)order → COMPLETED, allocation → SUCCESS, cộng điểm, phát Kafkacheck → 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 Kafkacheck → PARTIAL
ATTEMPT_FAILEDorder → CANCELLED + lý docheck → CANCELLED
ATTEMPT_EXPIREDorder → CANCELLED + 'Payment expired'check → CANCELLED
ATTEMPT_CANCELLEDorder → CANCELLED + 'Payment cancelled'check → CANCELLED
TRANSACTION_SETTLEDchỉ logchỉ log
TRANSACTION_CANCELLEDchỉ logchỉ log

9. Idempotency

Bề mặtCơ chế
Webhook handlerGuard trên status === PROCESSING — delivery lặp lại tới order không-PROCESSING bị âm thầm bỏ
Customer pointsKiểm tra PointTransactionRepository.existsBySaleOrderId trước khi ghi
Kafka emitkhô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

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.