Shop It Docs
Developer ResourcesNotification

Notification Implementation

Concrete implementation details for schemas, DTOs, producer logic, realtime events, and operational settings.

Notification Implementation

Schema and Indexes

Implemented in packages/mongodb/src/schemas/notifications:

  • notification.schema.ts
  • notification-setting.schema.ts

Key implementation points:

  • surfaces + statusBySurface replace legacy channel/read fields.
  • Unique setting key: { userId, notificationType, surface }.
  • Surface-focused read indexes for web and mobile in-app states.
  • TTL index on expiresAt and partial unique index on externalRef.

Producer and Target Dispatch

Implemented in apps/api/src/modules/notifications/notifications.processor.ts.

  • Producer payload accepts explicit inAppTargets and pushTargets.
  • One persistence write for all targets.
  • Emits notification:new for in-app targets.
  • Applies presence-driven push suppression policy.

Read API Module

Implemented in apps/api/src/modules/notification.

  • Surface required in query/body DTOs.
  • Surface-scoped list/count/read/mark-all behavior.
  • Realtime fanout on read operations via notification:read and notification:read_all.

Realtime and Presence

Implemented in:

  • apps/api/src/services/realtime/realtime.gateway.ts
  • apps/api/src/services/realtime/realtime.service.ts
  • apps/api/src/services/realtime/notification-room-presence.service.ts
  • packages/realtime-core/src/events/event-publisher.ts
  • packages/realtime-core/src/room-keys/room-keys.helper.ts

Details:

  • Join/leave/sync events for notification rooms.
  • Origin session exclusion to avoid duplicate self updates.
  • Presence TTL via NOTIFICATION_ROOM_PRESENCE_TTL_SECONDS.
  • Mobile suppression toggle via NOTIFICATION_MOBILE_PRESENCE_RELIABLE.

Testing Status

  • Unit coverage includes producer mapping/suppression and realtime gateway/service paths.
  • Realtime e2e spec includes notification delivery, dedup, and sync replay checks.
  • In environments without Redis connectivity, realtime e2e suite auto-skips with explicit warning.

Outbox Cleanup Integration (NEW)

Outbox cleanup is implemented as internal maintenance runtime in the order processing module and applies to all modules writing to outbox_events.

Implementation path:

  • Scheduler: apps/api/src/modules/order/processing/orders-maintenance.scheduler.ts
  • Processor: apps/api/src/modules/order/processing/orders-maintenance.processor.ts
  • Service: apps/api/src/modules/order/processing/outbox-cleanup.service.ts

Execution flow:

  1. Daily scheduler enqueues OrdersMaintenanceJob.CLEANUP_OUTBOX on QueueName.ORDERS_MAINTENANCE.
  2. Processor executes OutboxCleanupService.cleanup().
  3. Cleanup service deletes old rows by status/age:
    • completed rows older than OUTBOX_RETENTION_DAYS (default 7)
    • failed rows older than OUTBOX_FAILED_RETENTION_DAYS (default 30)
  4. Processor runs DLQ health check (checkDlqHealth) and logs alert-level errors when failed-job count exceeds threshold.

Operational configuration:

  • OUTBOX_RETENTION_DAYS
  • OUTBOX_FAILED_RETENTION_DAYS
  • OUTBOX_CLEANUP_ENABLED
  • OUTBOX_CLEANUP_DLQ_THRESHOLD

This is an internal maintenance feature with no external REST API surface.


Transactional Notification Layer (NEW)

Architecture Overview


                                                        TransactionalNotificationCatalog

                                                    NotificationRecipientResolverService

                                                          Template Renderers

                                                         NotificationsService

                                                      BullMQ Notifications Queue

                                                    NotificationsProcessor (base)

Implementation Files

FilePurpose
notification-event-type.tsEnum defining transactional order/refund event types
transactional-notification.catalog.tsEvent-to-template/channel configuration catalog
notification-router.service.tsCore routing logic with deduplication
notification-recipient-resolver.service.tsResolves recipients from event context
types.tsTypeScript interfaces for EventContext, Recipient, EventConfig
email-templates/*React-based email template renderers
transactional.module.tsNestJS module composition

Entry Point (Order Module)

apps/api/src/modules/order/processing/order-notification.service.ts provides domain-specific notification methods:

  • notifyOrderCreated(orderId) - Emits ORDER_PLACED_CHECKOUT or ORDER_PLACED_BUYNOW based on order_source
  • notifyPaymentSuccess(orderId) - Emits ORDER_PAYMENT_RECEIVED_CHECKOUT or ORDER_PAYMENT_RECEIVED_BUYNOW
  • notifyPaymentFailed(orderId, reason?) - Emits ORDER_PAYMENT_FAILED
  • notifyOrderCancelled(orderId, reason?) - Emits ORDER_CANCELLED
  • notifyRefundRequested(orderId) - Emits ORDER_REFUND_REQUESTED
  • notifyRefundApproved(orderId) - Emits ORDER_REFUND_APPROVED
  • notifyRefundRejected(orderId) - Emits ORDER_REFUND_REJECTED

Event Catalog

Event TypeTemplate IDChannelsPriorityTrigger
ORDER_PLACED_CHECKOUTcheckout_order_placedpush + emailnormalOrder created with source=checkout
ORDER_PAYMENT_RECEIVED_CHECKOUTcheckout_payment_receivedpush + emailhighPayment success for checkout order
ORDER_PAYMENT_FAILEDorder_payment_failedpush + emailhighPayment failed or cancelled
ORDER_CANCELLEDorder_cancelledpush + emailnormalOrder cancelled
ORDER_REFUND_REQUESTEDrefund_requestedpush + emailhighCustomer requests refund
ORDER_REFUND_APPROVEDrefund_approvedpush + emailhighAdmin approves refund
ORDER_REFUND_REJECTEDrefund_rejectedpush + emailhighAdmin rejects refund

EventContext Type

The EventContext interface provides all data needed for template rendering:

interface EventContext {
  orderId: number;
  orderNumber: string;
  userId: string;
  email?: string;
  userName?: string;
  finalPayable: number;
  orderStatus: OrderStatus;
  productId?: number;
  productTitle?: string;
  reason?: string;
  placedAt?: Date;
  paidAt?: Date;
  refundedAt?: Date;
  items?: NotificationProductItem[];  // For checkout multi-item orders
  product?: NotificationProductItem; // For buynow single-item orders
  subtotalPaisa?: number;
  discountPaisa?: number;
  couponCode?: string;
  amountPaisa?: number;
  ctaUrl?: string;
  ctaLabel?: string;
  retryCtaUrl?: string;
  supportEmail?: string;
}

Routing and Deduplication Behavior

The NotificationRouterService.routeEvent() method implements:

  1. Config Lookup: Resolves event configuration from TransactionalNotificationCatalog
  2. Dedupe Reservation: Attempts Redis SET with NX EX (7-day TTL)
    • Key format: notification_sent:transactional:<event>:<orderId>:<productId>
    • If Redis unavailable: fail-open (allow send)
    • If duplicate detected: skip silently
  3. Recipient Resolution: Calls NotificationRecipientResolverService.resolve(context)
  4. Channel Dispatch: For each channel (PUSH, EMAIL):
    • Push: Calls notificationsService.sendToUser() with inAppTargets=["mobile_in_app", "web_in_app"] and pushTargets=["mobile_push"]
    • Email: Calls notificationsService.sendEmailToUsers() with rendered HTML/text

Deduplication Key

const DEDUPE_KEY_PREFIX = "notification_sent:transactional:";
const DEDUPE_TTL_SECONDS = 7 * 24 * 60 * 60; // 7 days

// Built using CacheKeyUtil
dedupeKey = CacheKeyUtil.build(DEDUPE_KEY_PREFIX, [
  ["event", event],
  ["orderId", context.orderId],
  ["productId", context.productId],
]);

Push Notification Payload

await notificationsService.sendToUser({
  userId: recipient.userId,
  type: "transactional",
  priority: config.priority,
  title: config.title,                          // e.g., "Order Received", "Payment Successful"
  body: this.buildNotificationBody(event, context), // Event-specific body text
  data: {
    source: "order_transactional",
    event,
    orderId: String(context.orderId),
    orderNumber: context.orderNumber,
    productId: context.productId ? String(context.productId) : "",
    productType: context.productType ?? "",
  },
  inAppTargets: ["mobile_in_app", "web_in_app"],
  pushTargets: ["mobile_push"],
  externalRef: `${dedupeKey}:${recipient.userId}`,
  persistOnSkip: true,  // Always persist transactional notifications
});

Email Notification Payload

await notificationsService.sendEmailToUsers({
  userIds: recipients.map(r => r.userId),
  subject: rendered.subject,
  html: rendered.html,
  text: rendered.text,
  notificationType: "transactional",
  data: {
    source: "order_transactional",
    event,
    orderId: String(context.orderId),
    orderNumber: context.orderNumber,
    productId: context.productId ? String(context.productId) : "",
    productType: context.productType ?? "",
    previewText: rendered.previewText ?? "",
  },
  externalRefBase: dedupeKey,
  persistOnSkip: true,
});

Template Rendering

Each event type has a dedicated renderer in email-templates/:

  • render-checkout-email.ts - ORDER_PLACED_CHECKOUT, ORDER_PAYMENT_RECEIVED_CHECKOUT
  • render-buynow-email.ts - ORDER_PLACED_BUYNOW, ORDER_PAYMENT_RECEIVED_BUYNOW
  • render-lifecycle-email.ts - ORDER_PAYMENT_FAILED, ORDER_CANCELLED, ORDER_REFUND_REQUESTED, ORDER_REFUND_APPROVED, ORDER_REFUND_REJECTED

All renderers produce:

interface RenderedEmail {
  subject: string;
  html: string;
  text: string;
  previewText?: string;
}

Fallback Behavior

If template rendering fails:

  1. Router catches error and logs
  2. Falls back to buildFallbackEmail() which generates minimal HTML:
    • Subject: event-specific subject line
    • Body: simple paragraph with order number and amount
  3. Continues with dispatch (fail-open)

Fail-Open Reliability Model

  • Redis unavailable for dedupe: Allow send (fail-open)
  • Recipient resolution empty: Log warning, skip dispatch
  • Template rendering fails: Use fallback, continue dispatch
  • Notification delivery fails: Log error, do not block order/payment workflows

Order Source Aware Routing

The OrderNotificationService resolves event types based on order_source:

  • order_source = "checkout" → ORDER_PLACED_CHECKOUT, ORDER_PAYMENT_RECEIVED_CHECKOUT
  • Falls back to buynow events if order_source is null/unknown

Testing

Subphase validation commands:

pnpm --filter @nomor/api check-types
pnpm --filter @nomor/api exec biome check src/modules/notifications
pnpm --filter @nomor/api test -- src/modules/notifications

Test coverage includes:

  • transactional-notification.catalog.spec.ts - Catalog config tests
  • notification-router.service.spec.ts - Router routing/dedupe tests
  • notification-recipient-resolver.service.spec.ts - Recipient resolution tests
  • email-templates/*.spec.ts - Template rendering tests