Events
Legacy - not the live cross-service seam
The event-channel constants on this page (PaymentEventChannels, UserOnboardingEventChannels) and the IEventBus / RedisPubSubAdapter they were built for are dead code in @nx/core - never instantiated. The live cross-service seam is Kafka + Debezium CDC - see Event Architecture. payment.order.success is superseded by the Kafka topic nx.bana.evt.payment.succeeded; commerce.initialized onboarding is superseded by CDC nx.bana.cdc.public.Merchant. This page documents the constants that still exist in source.
| Field | Detail |
|---|---|
| Source files | packages/core/src/common/events/payment-events.ts (51 lines), user-onboarding-events.ts (61 lines), websocket-events.ts (95 lines) |
| Import | import { <ClassName> } from '@nx/core'; |
| Transport | WebSocket rooms/topics (Signal); the event-channel constants are legacy (see note above) |
| Total channel classes | 2 event channel classes + 2 WebSocket utility classes |
Overview
These @nx/core constants were built for a Redis-pub/sub event bus that is no longer the live seam (see the note above); they remain defined so legacy references resolve. The real cross-service seam is Kafka + Debezium CDC. Additionally, two utility classes provide standardized naming for WebSocket rooms and topics used by the Signal service.
All event channel names follow a consistent naming convention:
<domain>.<entity>.<action>Examples: payment.order.success, user.seller.registered, commerce.initialized
1. Event Channel Summary
| Channel | Value | Publisher | Consumer(s) |
|---|---|---|---|
PaymentEventChannels.PAYMENT_SUCCESS | payment.order.success | Sale Service | Finance Service, Inventory Service |
UserOnboardingEventChannels.SELLER_REGISTERED | user.seller.registered | Identity Service | Commerce Service |
UserOnboardingEventChannels.COMMERCE_INITIALIZED | commerce.initialized | Commerce Service | Finance Service, Inventory Service |
2. PaymentEventChannels
Source: packages/core/src/common/events/payment-events.ts
Channel
| Name | Value | Description |
|---|---|---|
PAYMENT_SUCCESS | 'payment.order.success' | Payment for a sale order completed successfully (PARTIAL or COMPLETED status) |
- Published by: Sale Service (via
SalePaymentEventHandlerService) - Consumed by: Finance Service (record income transaction), Inventory Service (deduct stock)
Payload: TPaymentSuccessEvent
export type TPaymentSuccessEvent = {
saleOrderId: string;
saleOrderNumber: string;
saleOrderStatus: string;
merchantId: string;
saleChannelId: string;
payment: {
total: number;
paid: number;
currency: string;
isFullyPaid: boolean;
paidAt: string; // ISO timestamp
financeWalletId?: string; // Optional: target wallet for finance recording
};
items: Array<{
id: string;
itemType: string;
itemId: string;
quantity: number;
mode: string;
}>;
createdBy: string;
modifiedBy: string;
};| Field | Type | Description |
|---|---|---|
saleOrderId | string | Snowflake ID of the sale order |
saleOrderNumber | string | Human-readable order number |
saleOrderStatus | string | Current order status after payment |
merchantId | string | Merchant ID (needed for finance and inventory operations) |
saleChannelId | string | Sale channel where the order was placed |
payment.total | number | Total order amount |
payment.paid | number | Amount paid in this payment |
payment.currency | string | Payment currency code |
payment.isFullyPaid | boolean | Whether the order is fully paid |
payment.paidAt | string | ISO timestamp of payment completion |
payment.financeWalletId | string? | Target finance wallet for income recording |
items | Array | Order items with quantity and type information |
createdBy | string | User who created the order |
modifiedBy | string | User who last modified the order |
Payment Flow
Usage
import { PaymentEventChannels } from '@nx/core';
import type { TPaymentSuccessEvent } from '@nx/core';
// Publisher (Sale Service)
await eventBus.publish(PaymentEventChannels.PAYMENT_SUCCESS, {
saleOrderId: order.id,
saleOrderNumber: order.number,
saleOrderStatus: order.status,
merchantId: order.merchantId,
saleChannelId: order.saleChannelId,
payment: {
total: order.total,
paid: attemptAmount,
currency: 'VND',
isFullyPaid: true,
paidAt: new Date().toISOString(),
},
items: order.items.map(item => ({
id: item.id,
itemType: item.itemType,
itemId: item.itemId,
quantity: item.quantity,
mode: item.mode,
})),
createdBy: order.createdBy,
modifiedBy: order.modifiedBy,
} satisfies TPaymentSuccessEvent);
// Consumer (Finance Service)
eventBus.subscribe(PaymentEventChannels.PAYMENT_SUCCESS, async (data: TPaymentSuccessEvent) => {
await financeWorkerService.handlePaymentSuccess(data);
});3. UserOnboardingEventChannels
Source: packages/core/src/common/events/user-onboarding-events.ts
Channels
| Name | Value | Description |
|---|---|---|
SELLER_REGISTERED | 'user.seller.registered' | A seller completes registration |
COMMERCE_INITIALIZED | 'commerce.initialized' | Commerce entities (Organizer, Merchant, SaleChannel) are created |
Payload: TSellerRegisteredEvent
export type TSellerRegisteredEvent = {
userId: string;
organizer: {
name: { en: string; vi: string };
slug: string;
};
merchant: {
name: { en: string; vi: string };
slug: string;
currency: string;
type: string;
};
saleChannel: {
name: { en: string; vi: string };
slug: string;
};
};| Field | Type | Description |
|---|---|---|
userId | string | ID of the newly registered user |
organizer.name | { en, vi } | Organizer display name (bilingual) |
organizer.slug | string | URL-friendly organizer identifier |
merchant.name | { en, vi } | Merchant display name (bilingual) |
merchant.slug | string | URL-friendly merchant identifier |
merchant.currency | string | Default currency code (e.g., VND) |
merchant.type | string | Merchant type (see MerchantTypes constants) |
saleChannel.name | { en, vi } | Sale channel display name (bilingual) |
saleChannel.slug | string | URL-friendly sale channel identifier |
- Published by: Identity Service (after user sign-up)
- Consumed by: Commerce Service (creates Organizer, Merchant, SaleChannel entities)
Payload: TCommerceInitializedEvent
export type TCommerceInitializedEvent = {
userId: string;
organizerId: string;
merchantId: string;
saleChannelId: string;
};| Field | Type | Description |
|---|---|---|
userId | string | ID of the user who triggered onboarding |
organizerId | string | ID of the newly created Organizer |
merchantId | string | ID of the newly created Merchant |
saleChannelId | string | ID of the newly created SaleChannel |
- Published by: Commerce Service (after creating entities)
- Consumed by: Finance Service (creates default Cash wallet), Inventory Service (initializes stock tracking)
Onboarding Flow
Usage
import { UserOnboardingEventChannels } from '@nx/core';
import type { TSellerRegisteredEvent, TCommerceInitializedEvent } from '@nx/core';
// Publisher (Identity Service)
await eventBus.publish(UserOnboardingEventChannels.SELLER_REGISTERED, {
userId: newUser.id,
organizer: { name: { en: 'My Store', vi: 'Cua Hang' }, slug: 'my-store' },
merchant: { name: { en: 'My Store', vi: 'Cua Hang' }, slug: 'my-store', currency: 'VND', type: '000_DEFAULT' },
saleChannel: { name: { en: 'Default', vi: 'Mac dinh' }, slug: 'default' },
} satisfies TSellerRegisteredEvent);
// Consumer (Commerce Service)
eventBus.subscribe(
UserOnboardingEventChannels.SELLER_REGISTERED,
async (data: TSellerRegisteredEvent) => {
await commerceOnboardingService.initializeForSeller(data);
},
);
// Publisher (Commerce Service)
await eventBus.publish(UserOnboardingEventChannels.COMMERCE_INITIALIZED, {
userId,
organizerId: organizer.id,
merchantId: merchant.id,
saleChannelId: saleChannel.id,
} satisfies TCommerceInitializedEvent);
// Consumer (Finance Service)
eventBus.subscribe(
UserOnboardingEventChannels.COMMERCE_INITIALIZED,
async (data: TCommerceInitializedEvent) => {
await financeWorkerService.handleCommerceInitialized(data);
},
);4. WebSocketRooms
Source: packages/core/src/common/events/websocket-events.ts
Utility class for building standardized WebSocket room identifiers used by the Signal service.
Constants
| Name | Value | Description |
|---|---|---|
ROOM_WR_PREFIX | 'wr' | Prefix for all room identifiers |
ROOM_OBSERVATION_PREFIX | 'observation' | Default scope prefix for observation rooms |
Room Format
wr:{prefix}/{path1}/{path2}/...The default prefix is observation. A custom prefix can be provided for non-observation rooms.
Method: build()
static build(opts: { prefix?: string; paths: Array<string | number> }): string| Parameter | Type | Default | Description |
|---|---|---|---|
prefix | string? | 'observation' | Room scope prefix |
paths | Array<string | number> | -- | Ordered path segments appended after the prefix |
Returns: Formatted room string: wr:{prefix}/{paths joined by /}
Throws: HttpError (400) if no valid path segments are provided.
Falsy values in the paths array are automatically filtered out.
Examples
import { WebSocketRooms } from '@nx/core';
// Default prefix (observation)
WebSocketRooms.build({ paths: ['merchants', 'abc-123'] });
// => 'wr:observation/merchants/abc-123'
WebSocketRooms.build({ paths: ['merchants', 'abc-123', 'sale-orders'] });
// => 'wr:observation/merchants/abc-123/sale-orders'
// Custom prefix
WebSocketRooms.build({ prefix: 'control', paths: ['sessions', 'sess-456'] });
// => 'wr:control/sessions/sess-456'
// Falsy values are filtered
WebSocketRooms.build({ paths: ['merchants', '', 'orders'] });
// => 'wr:observation/merchants/orders'5. WebSocketTopics
Source: packages/core/src/common/events/websocket-events.ts
Utility class for building standardized WebSocket topic identifiers. Topics categorize WebSocket messages so clients can subscribe to specific event streams.
Constants
| Name | Value | Description |
|---|---|---|
TOPIC_WS_PREFIX | 'ws' | Prefix for all topic identifiers |
Topic Format
ws:{path1}.{path2}.{path3}Method: build()
static build(opts: { paths: Array<string | number> }): string| Parameter | Type | Description |
|---|---|---|
paths | Array<string | number> | Ordered path segments joined by . after the ws: prefix |
Returns: Formatted topic string: ws:{paths joined by .}
Throws: HttpError (400) if no valid path segments are provided.
Falsy values in the paths array are automatically filtered out.
Examples
import { WebSocketTopics } from '@nx/core';
WebSocketTopics.build({ paths: ['observation', 'sale', 'sale-order'] });
// => 'ws:observation.sale.sale-order'
WebSocketTopics.build({ paths: ['observation', 'payment', 'transaction'] });
// => 'ws:observation.payment.transaction'
WebSocketTopics.build({ paths: ['notification', 'merchant', 'abc-123'] });
// => 'ws:notification.merchant.abc-123'6. Combined Architecture
Event Flow Across Services
Event vs Queue
Events (Redis pub/sub) and queues (BullMQ) serve different purposes in BANA:
| Aspect | Events (pub/sub) | Queues (BullMQ) |
|---|---|---|
| Delivery | Fire-and-forget broadcast | Guaranteed delivery with retries |
| Consumers | Multiple subscribers | Single consumer per partition |
| Persistence | No persistence | Persisted in Redis |
| Use case | Notifications, real-time updates | Critical job processing |
| Defined in | common/events/ | common/queues/ |
TIP
Events and queues often work together. An event handler may enqueue a job for reliable processing. For example, the PAYMENT_SUCCESS event is received by the Sale Service, which then enqueues jobs via SaleQueueDefinitions for both Finance and Inventory processing.
Best Practices
1. Always Use Channel Constants
// Good -- type-safe and refactorable
eventBus.subscribe(PaymentEventChannels.PAYMENT_SUCCESS, handler);
// Bad -- magic string
eventBus.subscribe('payment.order.success', handler);2. Type Event Payloads
// Good -- typed payload
eventBus.subscribe(
PaymentEventChannels.PAYMENT_SUCCESS,
async (data: TPaymentSuccessEvent) => {
// TypeScript knows the shape of data
const { merchantId, payment } = data;
},
);
// Bad -- untyped
eventBus.subscribe(PaymentEventChannels.PAYMENT_SUCCESS, async (data: any) => { });3. Use WebSocket Helpers for Consistent Naming
// Good -- standardized room/topic naming
const room = WebSocketRooms.build({ paths: ['merchants', merchantId, 'sale-orders'] });
const topic = WebSocketTopics.build({ paths: ['observation', 'sale', 'sale-order'] });
// Bad -- manual string construction
const room = `wr:observation/merchants/${merchantId}/sale-orders`;
const topic = `ws:observation.sale.sale-order`;