Skip to content

Payment Webhooks

1. Overview

PropertyValue
IDFEAT-SALE-PAY
StatusStable
Ownersale-team
Depends on@nx/mq-pay (webhook source), SaleOrder, SaleCheck, AllocationUsage, CustomerPointService, Kafka producer

Sale receives payment events from @nx/mq-pay via HTTP webhook (no auth — trusted internal). The flow is router → subordinate-service → state transition → side effects → Kafka emit.

2. Three-Service Architecture

Service responsibilities

ServiceFileRole
PaymentWebhookServicepayment-webhook.service.ts (99 lines)Router only — extracts checkId + orderId from payload, dispatches to appropriate sub-service. Does not transition state itself.
SaleOrderPaymentWebhookServicesale-order-payment-webhook.service.ts (376 lines)Order-level transitions, customer points, allocation usage updates, Kafka emit
SaleCheckPaymentWebhookServicesale-check-payment-webhook.service.ts (282 lines)Check-level transitions; aggregates check states to potentially complete the order

3. Webhook Endpoint

ItemValue
MethodPOST
Path/v1/api/sale/webhooks/payment
Authnone (trusted; secured via Cilium network policy)
Event type sourceheader X-Webhook-Event-Type (preferred) → falls back to body.eventType
ControllerPaymentWebhookController
ServicePaymentWebhookService.handleEvent

Request schema (zod)

Source: 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. Routing — PaymentWebhookService.handleEvent

Source: 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;
}

Extraction logic

HelperSourceReturns
_extractCheckId(payload)payment-webhook.service.ts:71-81transaction.sourceId if sourceType === 'SaleCheck', else attempt.metadata.source.id if type === 'SaleCheck', else null
_extractOrderId(payload)payment-webhook.service.ts:84-98Same shape but matched against 'SaleOrder'

Check takes precedence: if both checkId and orderId resolve, the check service handles it.

5. SaleOrder Event Handlers

All in SaleOrderPaymentWebhookService. Methods are private (_handle*), invoked by handleSaleOrderEvent switch.

5.1 ATTEMPT_SUCCESS → PROCESSING → PARTIAL or COMPLETED

Source: _handleOrderPaymentSuccess (lines 119-194).

StepAction
1Guard: order.status === PROCESSING (else log + skip)
2Compute isFullyPaid = paid >= total
3UPDATE order → COMPLETED (+ completedAt) or PARTIAL (+ partialAt)
4WS broadcast ORDER_PAYMENT_UPDATED
5Emit Kafka PAYMENT_SUCCESS (post-commit, fire-and-forget) — see §7
6If isFullyPaid AND order.customerIdCustomerPointService.awardPointsForOrder
7Find ACTIVE AllocationUsage rows for (usageId=order.id, usageType=SALE_ORDER) → bulk UPDATE → SUCCESS

5.2 ATTEMPT_FAILED / ATTEMPT_EXPIRED / ATTEMPT_CANCELLED → PROCESSING → CANCELLED

Sources: _handleOrderPaymentFailed (197-228), _handleOrderPaymentExpired (231-257), _handleOrderPaymentCancelled (260-286).

EventCancellation reason
ATTEMPT_FAILEDpayload.attempt.reason or 'Payment failed'
ATTEMPT_EXPIRED'Payment expired'
ATTEMPT_CANCELLED'Payment cancelled'

All three: guard PROCESSING, UPDATE order → CANCELLED + cancelledAt + cancellationReason, WS broadcast.

5.3 TRANSACTION_SETTLED / TRANSACTION_CANCELLED → log-only

Sources: _handleOrderTransactionSettled (289-294), _handleOrderTransactionCancelled (297-306).

Log informational message; no state change. Returns true so the webhook is acknowledged.

6. SaleCheck Event Handlers

All in SaleCheckPaymentWebhookService. SaleCheck statuses are PROCESSING / PARTIAL / COMPLETED / CANCELLED (see Domain Model §4.2).

6.1 ATTEMPT_SUCCESS → PARTIAL or COMPLETED + maybe complete order

Source: _handleCheckPaymentSuccess (lines 112-156).

StepAction
1Guard: check.status === PROCESSING
2Compute isFullyPaid = paid >= total
3UPDATE check → COMPLETED or PARTIAL
4If isFullyPaid: call _checkOrderCompletionViaChecks (lines 237-281)

6.2 ATTEMPT_FAILED / ATTEMPT_EXPIRED / ATTEMPT_CANCELLED → PROCESSING → CANCELLED

Sources: _handleCheckPaymentFailed (159-176), _handleCheckPaymentExpired (179-196), _handleCheckPaymentCancelled (199-216).

All three: guard PROCESSING, UPDATE check → CANCELLED. No order-level cascade — sibling checks remain.

6.3 Order completion via checks — _checkOrderCompletionViaChecks

Asymmetry vs order path: order-path also marks AllocationUsage → SUCCESS. Check-path does not explicitly set allocation usage. (TODO in code; sale team aware.)

7. Kafka Emit

_enqueuePaymentSuccess (lines 311-375) — only on the order-payment-success path, after DB commit.

PropertyValue
TopicKafkaTopics.PAYMENT_SUCCESS ('payment.success')
Keyorder.id
ProducerKafkaProducerHelper from BindingKeys.APPLICATION_KAFKA_PRODUCER
Deliveryfire-and-forget; failure is logged but does not roll back 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

ConsumerWhat it does
@nx/inventoryInventoryWorkerService.handlePaymentSuccess — deduct stock, reserve materials
@nx/financerecord INCOME wallet transaction (TODO: confirm method name)

8. Side-effect Summary Table

Event TypeOrder path effectCheck path effect
ATTEMPT_SUCCESS (full)order → COMPLETED, allocation → SUCCESS, points awarded, Kafka emitcheck → COMPLETED; if all checks COMPLETED → order → COMPLETED + points + WS (no Kafka emit on this path)
ATTEMPT_SUCCESS (partial)order → PARTIAL, Kafka emitcheck → PARTIAL
ATTEMPT_FAILEDorder → CANCELLED + reasoncheck → CANCELLED
ATTEMPT_EXPIREDorder → CANCELLED + 'Payment expired'check → CANCELLED
ATTEMPT_CANCELLEDorder → CANCELLED + 'Payment cancelled'check → CANCELLED
TRANSACTION_SETTLEDlog-onlylog-only
TRANSACTION_CANCELLEDlog-onlylog-only

9. Idempotency

SurfaceMechanism
Webhook handlerGuard on status === PROCESSING — repeated deliveries to a non-PROCESSING order are silently dropped
Customer pointsPointTransactionRepository.existsBySaleOrderId check before write
Kafka emitnot idempotent at sale layer; consumer-side dedup (e.g., inventory InventoryTracking lookup)

10. End-to-End Flow

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