API Sự kiện
Sale là emitter-only với Kafka — component Kafka đăng ký Producer nhưng không có Consumer. Inbound async đến qua webhook HTTP từ MQ-Pay.
1. Inbound — HTTP Webhook
| Endpoint | Auth | Nguồn | Handler |
|---|---|---|---|
POST /v1/api/sale/webhooks/payment | không (tin cậy nội bộ) | dịch vụ thanh toán @nx/mq-pay | PaymentWebhookService.handleEvent |
Loại sự kiện
Định nghĩa trong
src/common/webhook-types.ts:9(PaymentWebhookEventTypes). Loại sự kiện đọc từ headerX-Webhook-Event-Typehoặc body JSONeventType.
| Sự kiện | Constant | Hiệu ứng |
|---|---|---|
mq-pay:attempt.success | ATTEMPT_SUCCESS | Đánh dấu thanh toán một phần/đầy đủ dựa trên transaction.paid so với total |
mq-pay:attempt.failed | ATTEMPT_FAILED | Đánh dấu order/check CANCELLED nếu không có attempt thành công khác |
mq-pay:attempt.expired | ATTEMPT_EXPIRED | Giống như failed |
mq-pay:attempt.cancelled | ATTEMPT_CANCELLED | Giống như failed |
mq-pay:transaction.settled | TRANSACTION_SETTLED | Xác nhận cuối cấp transaction |
mq-pay:transaction.cancelled | TRANSACTION_CANCELLED | Cascade hủy |
Định tuyến
payload.transaction.sourceType (hoặc attempt.metadata.source.type) | Định tuyến đến |
|---|---|
SaleOrder | SaleOrderPaymentWebhookService.handleSaleOrderEvent |
SaleCheck | SaleCheckPaymentWebhookService.handleCheckEvent |
| khác | warn-log; bỏ qua |
Schema payload (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
Cấu hình Producer:
src/components/kafka/component.ts. Idempotent producer, lz4 compression, không có consumer.
| Topic | Constant | Trigger | Producer Site |
|---|---|---|---|
payment.success | KafkaTopics.PAYMENT_SUCCESS | Sau khi _handleOrderPaymentSuccess commit trạng thái PARTIAL/COMPLETED | sale-order-payment-webhook.service.ts:_enqueuePaymentSuccess |
kitchen-ticket-item.status-changed | KafkaTopics.KITCHEN_TICKET_ITEM_STATUS_CHANGED | Mỗi lần KitchenTicketItem đổi trạng thái (PENDING/COOKING/READY/SERVED/VOIDED) | kitchen-ticket-item.service.ts:_emitKitchenTicketItemStatusChanged |
Consumers (downstream)
| Topic | Consumer |
|---|---|
payment.success | @nx/inventory (giảm stock + reserve materials), @nx/finance (ghi INCOME) |
kitchen-ticket-item.status-changed | @nx/inventory (tiêu thụ materials khi READY, hoàn lại khi VOIDED) |
Schema payload
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;
};
}Cấu hình Producer
src/components/kafka/component.tsKHÔNG đặt rõ ràngidempotent,acks, hoặccompression. Producer dùng mặc định củaKafkaProducerHelper. Khác với@nx/inventory, sale không bật chế độ idempotent hoặc nén lz4 trong code.
| Cài đặt | Giá trị |
|---|---|
requestTimeout | 60_000ms |
connectTimeout | 30_000ms |
acks / idempotent / compression | không đặt — mặc định KafkaProducerHelper |
| Client ID | SVC-00030-SALE_PRODUCER (override qua APP_ENV_KAFKA_CLIENT_ID) |
| SASL | có điều kiện theo APP_ENV_KAFKA_SASL_ENABLE=true |
3. Phát WebSocket
src/common/websocket.ts. 7 topic × nhiều helper room cho fanout.
Topics
| Topic | Constant | Nguồn |
|---|---|---|
observation/sale/sale-order | SaleWebSocketTopics.ORDER | Sự kiện vòng đời order |
observation/sale/sale-order-item | SaleWebSocketTopics.ORDER_ITEM | Thêm/cập nhật/xóa item |
observation/sale/sale-check | SaleWebSocketTopics.CHECK | Check create/payment-update |
observation/sale/kitchen-ticket | SaleWebSocketTopics.KITCHEN_TICKET | Vòng đời ticket |
observation/sale/kitchen-ticket-item | SaleWebSocketTopics.KITCHEN_TICKET_ITEM | Trạng thái item / 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 (mỗi lần phát)
| Helper | Rooms phát đến |
|---|---|
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> nếu được gán) |
getKitchenTicketItemRooms({...}) | Tất cả rooms của kitchen ticket + kitchen-tickets/<id>/items + kitchen-ticket-items/<itemId> |
getAllocationUsageRooms({ merchantId, unitId, zoneId?, parentId?, grandparentId? }) | merchants/<m> + merchants/<m>/allocation-usages + allocation-units/<unitId> (+ cây phân cấp zone) |
getReservationRooms({...}) | merchants/<m> + merchants/<m>/reservations + reservations/<id> (+ allocation rooms) |
Event actions (trường event của payload)
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 & Thứ tự
| Bề mặt | Phân phối | Thứ tự | Phục hồi |
|---|---|---|---|
| HTTP webhook | at-most-once (HTTP) | request-id | MQ-Pay retry khi 5xx |
Kafka payment.success | at-least-once | per-saleOrderId qua key | dedup phía consumer downstream (inventory tracking, finance idempotency) |
Kafka kitchen-ticket-item.status-changed | at-least-once | per-saleOrderId | dedup phía consumer downstream |
| WebSocket | best-effort | không (broadcast) | client refetch từ REST khi reconnect |
5. Trang liên quan
- Tích hợp — hợp đồng MQ-Pay + downstream
- Payment Webhooks — chi tiết cấp tính năng
- Kiến trúc — Kịch bản runtime