From 68c60f9e618a3ceadd525829ed513c205741da73 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Mon, 6 Jul 2026 08:08:07 +0000 Subject: [PATCH] feat: add destination to the notification --- .../notification-inbox.service.ts | 34 ++++++++++++++++--- packages/types/src/freight/notifications.ts | 25 +++++++++++++- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/apps/edr-freight-api/src/modules/notification-inbox/notification-inbox.service.ts b/apps/edr-freight-api/src/modules/notification-inbox/notification-inbox.service.ts index d6c1fbc92..0c899fc29 100644 --- a/apps/edr-freight-api/src/modules/notification-inbox/notification-inbox.service.ts +++ b/apps/edr-freight-api/src/modules/notification-inbox/notification-inbox.service.ts @@ -1,5 +1,6 @@ import { NotificationAudience, + NotificationChannels, NotificationChannelsSent, NotificationDto, NotificationListResult, @@ -144,18 +145,41 @@ export class NotificationInboxService { const unreadCount = await this.repo.countUnread(userId); this.gateway.emitNew(userId, this.toDto(entity), unreadCount); - if (priority === NotificationPriority.HIGH) { - const channelsSent = await this.fanOut(userId, input); + const channels = this.resolveChannels(input, priority); + if (channels.email || channels.sms) { + const channelsSent = await this.fanOut(userId, input, channels); if (channelsSent) { await this.repo.update(entity.id, { channelsSent }); } } } - /** Best-effort email/SMS fan-out for HIGH-priority items. Never throws. */ + /** + * Decide which outbound channels to use. An explicit `input.channels` + * selection wins; otherwise fall back to priority (HIGH ⇒ email + SMS). + */ + private resolveChannels( + input: NotifyInput, + priority: NotificationPriority, + ): Required { + if (input.channels) { + return { + email: input.channels.email === true, + sms: input.channels.sms === true, + }; + } + const high = priority === NotificationPriority.HIGH; + return { email: high, sms: high }; + } + + /** + * Best-effort email/SMS fan-out for the requested channels. Skips a channel + * the recipient has no address for. Never throws. + */ private async fanOut( userId: string, input: NotifyInput, + channels: Required, ): Promise { try { const user = await this.users.findOne({ @@ -166,7 +190,7 @@ export class NotificationInboxService { const sent: NotificationChannelsSent = {}; const text = `${input.title}\n\n${input.body}`; - if (user.email) { + if (channels.email && user.email) { const res = await this.emailClient.sendEmail({ to: user.email, subject: input.title, @@ -174,7 +198,7 @@ export class NotificationInboxService { }); sent.email = res.queued; } - if (user.phoneNumber) { + if (channels.sms && user.phoneNumber) { const res = await this.smsClient.sendSms({ to: user.phoneNumber, message: text, diff --git a/packages/types/src/freight/notifications.ts b/packages/types/src/freight/notifications.ts index 546d2d91b..bdf83a719 100644 --- a/packages/types/src/freight/notifications.ts +++ b/packages/types/src/freight/notifications.ts @@ -13,7 +13,11 @@ export enum NotificationAudience { BACKOFFICE = "BACKOFFICE", } -/** Drives channel fan-out: HIGH also pushes email/SMS, NORMAL is in-app only. */ +/** + * Default channel fan-out when {@link NotifyInput.channels} is not given: + * HIGH also pushes email/SMS, NORMAL is in-app only. An explicit `channels` + * selection overrides this. + */ export enum NotificationPriority { NORMAL = "NORMAL", HIGH = "HIGH", @@ -36,6 +40,19 @@ export enum NotificationType { CLEARANCE_REVIEW = "CLEARANCE_REVIEW", } +/** + * Explicit fan-out channel selection for a single `notify(...)` call. + * + * In-app delivery is always performed (this module is an inbox) and is not + * listed here. These flags control the *extra* outbound channels. When omitted + * on {@link NotifyInput}, channels fall back to priority: HIGH ⇒ email + SMS, + * NORMAL ⇒ none. + */ +export interface NotificationChannels { + email?: boolean; + sms?: boolean; +} + /** Optional per-channel delivery outcome recorded on the notification row. */ export interface NotificationChannelsSent { email?: boolean; @@ -82,6 +99,12 @@ export interface NotifyInput { link?: string | null; data?: Record | null; priority?: NotificationPriority; + /** + * Explicit email/SMS fan-out. Overrides the priority-based default when set; + * omit to let `priority` decide (HIGH ⇒ email + SMS, NORMAL ⇒ in-app only). + * In-app is always delivered regardless. + */ + channels?: NotificationChannels; } /** Paginated list envelope for the notifications list endpoint. */