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
| Endpoint | Auth | Source | Handler |
|---|---|---|---|
POST /v1/api/sale/webhooks/payment | none (trusted internal) | @nx/mq-pay payment service | PaymentWebhookService.handleEvent |
Event types
Defined in
src/common/webhook-types.ts:9(PaymentWebhookEventTypes). Event type read from headerX-Webhook-Event-Typeor JSON bodyeventType.
| Event | Constant | Effect |
|---|---|---|
mq-pay:attempt.success | ATTEMPT_SUCCESS | Mark partial/full payment based on transaction.paid vs total |
mq-pay:attempt.failed | ATTEMPT_FAILED | Mark order/check CANCELLED if no other successful attempts |
mq-pay:attempt.expired | ATTEMPT_EXPIRED | Same as failed |
mq-pay:attempt.cancelled | ATTEMPT_CANCELLED | Same as failed |
mq-pay:transaction.settled | TRANSACTION_SETTLED | Final transaction-level confirmation |
mq-pay:transaction.cancelled | TRANSACTION_CANCELLED | Cancel cascade |
Routing
payload.transaction.sourceType (or attempt.metadata.source.type) | Routed to |
|---|---|
SaleOrder | SaleOrderPaymentWebhookService.handleSaleOrderEvent |
SaleCheck | SaleCheckPaymentWebhookService.handleCheckEvent |
| other | warn-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.
| Topic | Constant | Trigger | Producer Site |
|---|---|---|---|
payment.success | KafkaTopics.PAYMENT_SUCCESS | After _handleOrderPaymentSuccess commits status PARTIAL/COMPLETED | sale-order-payment-webhook.service.ts:_enqueuePaymentSuccess |
kitchen-ticket-item.status-changed | KafkaTopics.KITCHEN_TICKET_ITEM_STATUS_CHANGED | Per KitchenTicketItem status change (PENDING/COOKING/READY/SERVED/VOIDED) | kitchen-ticket-item.service.ts:_emitKitchenTicketItemStatusChanged |
Consumers (downstream)
| Topic | Consumer |
|---|---|
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.tsdoes NOT explicitly setidempotent,acks, orcompression. The producer usesKafkaProducerHelperdefaults. Unlike@nx/inventory, sale does not enable idempotent mode or lz4 compression in code.
| Setting | Value |
|---|---|
requestTimeout | 60_000ms |
connectTimeout | 30_000ms |
acks / idempotent / compression | not set — KafkaProducerHelper defaults |
| Client ID | SVC-00030-SALE_PRODUCER (override via APP_ENV_KAFKA_CLIENT_ID) |
| SASL | conditional on APP_ENV_KAFKA_SASL_ENABLE=true |
3. WebSocket Emissions
src/common/websocket.ts. 7 topics × multiple room helpers for fanout.
Topics
| Topic | Constant | Source |
|---|---|---|
observation/sale/sale-order | SaleWebSocketTopics.ORDER | Order lifecycle events |
observation/sale/sale-order-item | SaleWebSocketTopics.ORDER_ITEM | Item add/update/delete |
observation/sale/sale-check | SaleWebSocketTopics.CHECK | Check create/payment-update |
observation/sale/kitchen-ticket | SaleWebSocketTopics.KITCHEN_TICKET | Ticket lifecycle |
observation/sale/kitchen-ticket-item | SaleWebSocketTopics.KITCHEN_TICKET_ITEM | Item status / void |
observation/allocation/allocation-usage | SaleWebSocketTopics.ALLOCATION_USAGE | Usage create/complete/cancel |
observation/sale/reservation | SaleWebSocketTopics.RESERVATION | Reservation create/confirm/cancel/check-in |
Room fanout (per emission)
| Helper | Rooms 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
| Surface | Delivery | Ordering | Recovery |
|---|---|---|---|
| HTTP webhook | at-most-once (HTTP) | request-id | MQ-Pay retries on 5xx |
Kafka payment.success | at-least-once | per-saleOrderId via key | downstream consumer dedup (inventory tracking, finance idempotency) |
Kafka kitchen-ticket-item.status-changed | at-least-once | per-saleOrderId | downstream consumer dedup |
| WebSocket | best-effort | none (broadcast) | clients refetch from REST on reconnect |
5. Related Pages
- Integration — MQ-Pay + downstream contracts
- Payment Webhooks — feature-level detail
- Architecture — Runtime Scenarios