Skip to content

API Events

Sale is emitter-only for Kafka — the Kafka component registers a Producer but no Consumer. Inbound async comes via HTTP webhook from MQ-Pay.

1. Inbound — HTTP Webhook

EndpointAuthSourceHandler
POST /v1/api/sale/webhooks/paymentnone (trusted internal)@nx/mq-pay payment servicePaymentWebhookService.handleEvent

Event types

Defined in src/common/webhook-types.ts:9 (PaymentWebhookEventTypes). Event type read from header X-Webhook-Event-Type or JSON body eventType.

EventConstantEffect
mq-pay:attempt.successATTEMPT_SUCCESSMark partial/full payment based on transaction.paid vs total
mq-pay:attempt.failedATTEMPT_FAILEDMark order/check CANCELLED if no other successful attempts
mq-pay:attempt.expiredATTEMPT_EXPIREDSame as failed
mq-pay:attempt.cancelledATTEMPT_CANCELLEDSame as failed
mq-pay:transaction.settledTRANSACTION_SETTLEDFinal transaction-level confirmation
mq-pay:transaction.cancelledTRANSACTION_CANCELLEDCancel cascade

Routing

payload.transaction.sourceType (or attempt.metadata.source.type)Routed to
SaleOrderSaleOrderPaymentWebhookService.handleSaleOrderEvent
SaleCheckSaleCheckPaymentWebhookService.handleCheckEvent
otherwarn-log; ignored

Payload schema (zod)

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,
      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 } },
    },
  },
}

2. Outbound — Kafka

Producer config: src/components/kafka/component.ts. Idempotent producer, lz4 compression, no consumer.

TopicConstantTriggerProducer Site
payment.successKafkaTopics.PAYMENT_SUCCESSAfter _handleOrderPaymentSuccess commits status PARTIAL/COMPLETEDsale-order-payment-webhook.service.ts:_enqueuePaymentSuccess
kitchen-ticket-item.status-changedKafkaTopics.KITCHEN_TICKET_ITEM_STATUS_CHANGEDPer KitchenTicketItem status change (PENDING/COOKING/READY/SERVED/VOIDED)kitchen-ticket-item.service.ts:_emitKitchenTicketItemStatusChanged

Consumers (downstream)

TopicConsumer
payment.success@nx/inventory (deduct stock + reserve materials), @nx/finance (record INCOME)
kitchen-ticket-item.status-changed@nx/inventory (consume materials on READY, restore on VOIDED)

Payload schemas

ts
// PAYMENT_SUCCESS — TSalePaymentSuccess
export interface TSalePaymentSuccess {
  saleOrderId: string;
  saleOrderNumber: string;
  saleOrderStatus: string;
  merchantId: string;
  saleChannelId: string;
  createdBy?: string;
  modifiedBy?: string;
  payment: { total: number; paid: number; status: string };
  items: Array<{
    itemId: string;
    itemType: 'PRODUCT_VARIANT' | 'MATERIAL';
    quantity: number;
    unitPrice: number;
    tax: number;
    discount: number;
    subtotal: number;
    total: number;
  }>;
}

// KITCHEN_TICKET_ITEM_STATUS_CHANGED — TKitchenTicketItemStatusChangedMessage
export interface TKitchenTicketItemStatusChangedMessage {
  kitchenTicketItemId: string;
  kitchenTicketId: string;
  saleOrderId: string;
  merchantId: string;
  status: 'PENDING' | 'COOKING' | 'READY' | 'SERVED' | 'VOIDED';
  item: {
    saleOrderItemId: string;
    itemType: 'PRODUCT_VARIANT' | 'MATERIAL';
    itemId: string;
    quantity: number;
  };
}

Producer config

src/components/kafka/component.ts does NOT explicitly set idempotent, acks, or compression. The producer uses KafkaProducerHelper defaults. Unlike @nx/inventory, sale does not enable idempotent mode or lz4 compression in code.

SettingValue
requestTimeout60_000ms
connectTimeout30_000ms
acks / idempotent / compressionnot set — KafkaProducerHelper defaults
Client IDSVC-00030-SALE_PRODUCER (override via APP_ENV_KAFKA_CLIENT_ID)
SASLconditional on APP_ENV_KAFKA_SASL_ENABLE=true

3. WebSocket Emissions

src/common/websocket.ts. 7 topics × multiple room helpers for fanout.

Topics

TopicConstantSource
observation/sale/sale-orderSaleWebSocketTopics.ORDEROrder lifecycle events
observation/sale/sale-order-itemSaleWebSocketTopics.ORDER_ITEMItem add/update/delete
observation/sale/sale-checkSaleWebSocketTopics.CHECKCheck create/payment-update
observation/sale/kitchen-ticketSaleWebSocketTopics.KITCHEN_TICKETTicket lifecycle
observation/sale/kitchen-ticket-itemSaleWebSocketTopics.KITCHEN_TICKET_ITEMItem status / void
observation/allocation/allocation-usageSaleWebSocketTopics.ALLOCATION_USAGEUsage create/complete/cancel
observation/sale/reservationSaleWebSocketTopics.RESERVATIONReservation create/confirm/cancel/check-in

Room fanout (per emission)

HelperRooms emitted to
getSaleOrderRooms({ orderId, merchantId })merchants/<m> + merchants/<m>/sale-orders + sale-orders/<orderId>
getSaleOrderItemRooms({ itemId, orderId, merchantId })+ sale-orders/<orderId>/sale-order-items + sale-orders/<orderId>/sale-order-items/<itemId>
getKitchenTicketRooms({ ticketId, orderId, merchantId, stationId? })merchants/<m> + merchants/<m>/kitchen + sale-orders/<orderId>/kitchen + kitchen-tickets/<ticketId> (+ kitchen-stations/<stationId> if assigned)
getKitchenTicketItemRooms({...})All kitchen ticket rooms + kitchen-tickets/<id>/items + kitchen-ticket-items/<itemId>
getAllocationUsageRooms({ merchantId, unitId, zoneId?, parentId?, grandparentId? })merchants/<m> + merchants/<m>/allocation-usages + allocation-units/<unitId> (+ zone hierarchy)
getReservationRooms({...})merchants/<m> + merchants/<m>/reservations + reservations/<id> (+ allocation rooms)

Event actions (payload event field)

ts
SaleOrderEventActions = {
  ORDER_CREATED, ORDER_UPDATED, ORDER_DELETED, ORDER_CLEARED,
  ORDER_CHECKOUT, ORDER_REVERT_CHECKOUT, ORDER_PAYMENT_UPDATED,
  ITEM_ADDED, ITEM_CREATED, ITEM_UPDATED, ITEM_DELETED,
}
KitchenTicketEventActions = {
  TICKET_CREATED, TICKET_VOIDED, TICKET_RUSHED, TICKET_STATUS_CHANGED,
  ITEM_VOIDED, ITEM_STATUS_CHANGED,
}
AllocationUsageEventActions = {
  USAGE_CREATED, USAGE_COMPLETED, USAGE_CANCELLED,
}
ReservationEventActions = {
  RESERVATION_CREATED, RESERVATION_CONFIRMED, RESERVATION_CANCELLED, RESERVATION_CHECKED_IN,
}

4. Idempotency & Ordering

SurfaceDeliveryOrderingRecovery
HTTP webhookat-most-once (HTTP)request-idMQ-Pay retries on 5xx
Kafka payment.successat-least-onceper-saleOrderId via keydownstream consumer dedup (inventory tracking, finance idempotency)
Kafka kitchen-ticket-item.status-changedat-least-onceper-saleOrderIddownstream consumer dedup
WebSocketbest-effortnone (broadcast)clients refetch from REST on reconnect

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