Skip to content

Events

FieldDetail
Source filespackages/core/src/common/events/payment-events.ts (51 lines), user-onboarding-events.ts (61 lines), websocket-events.ts (95 lines)
Importimport { <ClassName> } from '@nx/core';
TransportRedis pub/sub (events), WebSocket (rooms and topics)
Total channel classes2 event channel classes + 2 WebSocket utility classes

Overview

BANA uses Redis pub/sub for cross-service event communication. All event channels are defined in @nx/core so that both publishers and consumers reference the same constants. 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

ChannelValuePublisherConsumer(s)
PaymentEventChannels.PAYMENT_SUCCESSpayment.order.successSale ServiceFinance Service, Inventory Service
UserOnboardingEventChannels.SELLER_REGISTEREDuser.seller.registeredIdentity ServiceCommerce Service
UserOnboardingEventChannels.COMMERCE_INITIALIZEDcommerce.initializedCommerce ServiceFinance Service, Inventory Service

2. PaymentEventChannels

Source: packages/core/src/common/events/payment-events.ts

Channel

NameValueDescription
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

typescript
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;
};
FieldTypeDescription
saleOrderIdstringSnowflake ID of the sale order
saleOrderNumberstringHuman-readable order number
saleOrderStatusstringCurrent order status after payment
merchantIdstringMerchant ID (needed for finance and inventory operations)
saleChannelIdstringSale channel where the order was placed
payment.totalnumberTotal order amount
payment.paidnumberAmount paid in this payment
payment.currencystringPayment currency code
payment.isFullyPaidbooleanWhether the order is fully paid
payment.paidAtstringISO timestamp of payment completion
payment.financeWalletIdstring?Target finance wallet for income recording
itemsArrayOrder items with quantity and type information
createdBystringUser who created the order
modifiedBystringUser who last modified the order

Payment Flow

Usage

typescript
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

NameValueDescription
SELLER_REGISTERED'user.seller.registered'A seller completes registration
COMMERCE_INITIALIZED'commerce.initialized'Commerce entities (Organizer, Merchant, SaleChannel) are created

Payload: TSellerRegisteredEvent

typescript
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;
  };
};
FieldTypeDescription
userIdstringID of the newly registered user
organizer.name{ en, vi }Organizer display name (bilingual)
organizer.slugstringURL-friendly organizer identifier
merchant.name{ en, vi }Merchant display name (bilingual)
merchant.slugstringURL-friendly merchant identifier
merchant.currencystringDefault currency code (e.g., VND)
merchant.typestringMerchant type (see MerchantTypes constants)
saleChannel.name{ en, vi }Sale channel display name (bilingual)
saleChannel.slugstringURL-friendly sale channel identifier
  • Published by: Identity Service (after user sign-up)
  • Consumed by: Commerce Service (creates Organizer, Merchant, SaleChannel entities)

Payload: TCommerceInitializedEvent

typescript
export type TCommerceInitializedEvent = {
  userId: string;
  organizerId: string;
  merchantId: string;
  saleChannelId: string;
};
FieldTypeDescription
userIdstringID of the user who triggered onboarding
organizerIdstringID of the newly created Organizer
merchantIdstringID of the newly created Merchant
saleChannelIdstringID 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

typescript
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

NameValueDescription
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()

typescript
static build(opts: { prefix?: string; paths: Array<string | number> }): string
ParameterTypeDefaultDescription
prefixstring?'observation'Room scope prefix
pathsArray<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

typescript
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

NameValueDescription
TOPIC_WS_PREFIX'ws'Prefix for all topic identifiers

Topic Format

ws:{path1}.{path2}.{path3}

Method: build()

typescript
static build(opts: { paths: Array<string | number> }): string
ParameterTypeDescription
pathsArray<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

typescript
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:

AspectEvents (pub/sub)Queues (BullMQ)
DeliveryFire-and-forget broadcastGuaranteed delivery with retries
ConsumersMultiple subscribersSingle consumer per partition
PersistenceNo persistencePersisted in Redis
Use caseNotifications, real-time updatesCritical job processing
Defined incommon/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

typescript
// Good -- type-safe and refactorable
eventBus.subscribe(PaymentEventChannels.PAYMENT_SUCCESS, handler);

// Bad -- magic string
eventBus.subscribe('payment.order.success', handler);

2. Type Event Payloads

typescript
// 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

typescript
// 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`;

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