From 4b3027491545452b2f32f3e6ff48d8befd8c785c Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Thu, 29 Jan 2026 21:09:44 +0000 Subject: [PATCH] feat: Add incident member notification system with email and WhatsApp templates --- .../Templates/IncidentMemberAdded.hbs | 32 +++ Common/Models/DatabaseModels/IncidentFeed.ts | 2 + .../Models/DatabaseModels/IncidentMember.ts | 20 ++ .../Server/Services/IncidentMemberService.ts | 200 ++++++++++++++ Common/Server/Utils/WhatsAppTemplateUtil.ts | 3 + Common/Types/Email/EmailTemplateType.ts | 1 + .../NotificationSettingEventType.ts | 1 + Common/Types/WhatsApp/WhatsAppTemplates.ts | 5 + .../SendMemberAddedNotification.ts | 257 ++++++++++++++++++ Worker/Routes.ts | 3 + 10 files changed, 524 insertions(+) create mode 100644 App/FeatureSet/Notification/Templates/IncidentMemberAdded.hbs create mode 100644 Worker/Jobs/IncidentMembers/SendMemberAddedNotification.ts diff --git a/App/FeatureSet/Notification/Templates/IncidentMemberAdded.hbs b/App/FeatureSet/Notification/Templates/IncidentMemberAdded.hbs new file mode 100644 index 0000000000..a14f1977ae --- /dev/null +++ b/App/FeatureSet/Notification/Templates/IncidentMemberAdded.hbs @@ -0,0 +1,32 @@ +{{> Start this}} + + +{{> Logo this}} +{{> EmailTitle title=(concat "Incident " incidentNumber ": " incidentTitle) }} + +{{> InfoBlock info=(concat "You have been assigned as " incidentRole " to this incident.")}} + +{{> InfoBlock info="Here are the details: "}} + +{{> DetailBoxStart this }} +{{> DetailBoxField title="Incident Title:" text=incidentTitle }} +{{> DetailBoxField title="Your Role: " text=incidentRole }} +{{> DetailBoxField title="Current State: " text=currentState }} +{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }} +{{> DetailBoxField title="Severity: " text=incidentSeverity }} +{{> DetailBoxField title="Description: " text=incidentDescription }} +{{> DetailBoxEnd this }} + + +{{> InfoBlock info="You can view this incident by clicking on the button below - "}} + +{{> ButtonBlock buttonUrl=incidentViewLink buttonText="View on Dashboard"}} + +{{> InfoBlock info="You can also copy and paste this link:"}} +{{> InfoBlock info=incidentViewLink}} + +{{> InfoBlock info="You will be notified when the status of this incident changes."}} + +{{> Footer this }} + +{{> End this}} diff --git a/Common/Models/DatabaseModels/IncidentFeed.ts b/Common/Models/DatabaseModels/IncidentFeed.ts index 053fcc8af6..a56e9b326e 100644 --- a/Common/Models/DatabaseModels/IncidentFeed.ts +++ b/Common/Models/DatabaseModels/IncidentFeed.ts @@ -38,6 +38,8 @@ export enum IncidentFeedEventType { OwnerTeamRemoved = "OwnerTeamRemoved", OnCallPolicy = "OnCallPolicy", OnCallNotification = "OnCallNotification", + IncidentMemberAdded = "IncidentMemberAdded", + IncidentMemberRemoved = "IncidentMemberRemoved", } @EnableDocumentation() diff --git a/Common/Models/DatabaseModels/IncidentMember.ts b/Common/Models/DatabaseModels/IncidentMember.ts index 3f9d1eb2d5..eb46f65fa6 100644 --- a/Common/Models/DatabaseModels/IncidentMember.ts +++ b/Common/Models/DatabaseModels/IncidentMember.ts @@ -521,4 +521,24 @@ export default class IncidentMember extends BaseModel { transformer: ObjectID.getDatabaseTransformer(), }) public deletedByUserId?: ObjectID = undefined; + + @ColumnAccessControl({ + create: [], + read: [], + update: [], + }) + @TableColumn({ + isDefaultValueColumn: true, + type: TableColumnType.Boolean, + title: "Is Member Notified", + description: "Has the member been notified of this role assignment?", + defaultValue: false, + example: false, + }) + @Column({ + type: ColumnType.Boolean, + nullable: false, + default: false, + }) + public isMemberNotified?: boolean = undefined; } diff --git a/Common/Server/Services/IncidentMemberService.ts b/Common/Server/Services/IncidentMemberService.ts index f79c52ba60..8d420cf049 100644 --- a/Common/Server/Services/IncidentMemberService.ts +++ b/Common/Server/Services/IncidentMemberService.ts @@ -1,9 +1,209 @@ +import ObjectID from "../../Types/ObjectID"; import DatabaseService from "./DatabaseService"; import Model from "../../Models/DatabaseModels/IncidentMember"; +import IncidentFeedService from "./IncidentFeedService"; +import { IncidentFeedEventType } from "../../Models/DatabaseModels/IncidentFeed"; +import { Gray500, Red500 } from "../../Types/BrandColors"; +import User from "../../Models/DatabaseModels/User"; +import UserService from "./UserService"; +import { OnCreate, OnDelete } from "../Types/Database/Hooks"; +import DeleteBy from "../Types/Database/DeleteBy"; +import IncidentService from "./IncidentService"; +import WorkspaceNotificationRuleService from "./WorkspaceNotificationRuleService"; +import NotificationRuleEventType from "../../Types/Workspace/NotificationRules/EventType"; +import WorkspaceNotificationRule from "../../Models/DatabaseModels/WorkspaceNotificationRule"; +import logger from "../Utils/Logger"; +import CaptureSpan from "../Utils/Telemetry/CaptureSpan"; +import IncidentRole from "../../Models/DatabaseModels/IncidentRole"; +import IncidentRoleService from "./IncidentRoleService"; export class Service extends DatabaseService { public constructor() { super(Model); } + + @CaptureSpan() + protected override async onBeforeDelete( + deleteBy: DeleteBy, + ): Promise> { + const itemsToDelete: Model[] = await this.findBy({ + query: deleteBy.query, + limit: deleteBy.limit, + skip: deleteBy.skip, + props: { + isRoot: true, + }, + select: { + incidentId: true, + projectId: true, + userId: true, + incidentRoleId: true, + }, + }); + + return { + carryForward: { + itemsToDelete: itemsToDelete, + }, + deleteBy: deleteBy, + }; + } + + @CaptureSpan() + protected override async onDeleteSuccess( + onDelete: OnDelete, + _itemIdsBeforeDelete: Array, + ): Promise> { + const deleteByUserId: ObjectID | undefined = + onDelete.deleteBy.deletedByUser?.id || onDelete.deleteBy.props.userId; + + const itemsToDelete: Model[] = onDelete.carryForward.itemsToDelete; + + for (const item of itemsToDelete) { + const incidentId: ObjectID | undefined = item.incidentId; + const projectId: ObjectID | undefined = item.projectId; + const userId: ObjectID | undefined = item.userId; + const incidentRoleId: ObjectID | undefined = item.incidentRoleId; + + if (incidentId && userId && projectId) { + const user: User | null = await UserService.findOneById({ + id: userId, + select: { + name: true, + email: true, + }, + props: { + isRoot: true, + }, + }); + + let roleName: string = "Member"; + if (incidentRoleId) { + const role: IncidentRole | null = + await IncidentRoleService.findOneById({ + id: incidentRoleId, + select: { + name: true, + }, + props: { + isRoot: true, + }, + }); + if (role && role.name) { + roleName = role.name; + } + } + + const incidentNumber: number | null = + await IncidentService.getIncidentNumber({ + incidentId: incidentId, + }); + + if (user && user.name) { + await IncidentFeedService.createIncidentFeedItem({ + incidentId: incidentId, + projectId: projectId, + incidentFeedEventType: IncidentFeedEventType.IncidentMemberRemoved, + displayColor: Red500, + feedInfoInMarkdown: `👤 Removed **${user.name.toString()}** (${user.email?.toString()}) as **${roleName}** from [Incident ${incidentNumber}](${(await IncidentService.getIncidentLinkInDashboard(projectId!, incidentId!)).toString()}).`, + userId: deleteByUserId || undefined, + workspaceNotification: { + sendWorkspaceNotification: true, + notifyUserId: userId || undefined, + }, + }); + } + } + } + + return onDelete; + } + + @CaptureSpan() + public override async onCreateSuccess( + onCreate: OnCreate, + createdItem: Model, + ): Promise { + const incidentId: ObjectID | undefined = createdItem.incidentId; + const projectId: ObjectID | undefined = createdItem.projectId; + const userId: ObjectID | undefined = createdItem.userId; + const incidentRoleId: ObjectID | undefined = createdItem.incidentRoleId; + const createdByUserId: ObjectID | undefined = + createdItem.createdByUserId || onCreate.createBy.props.userId; + + if (incidentId && userId && projectId) { + let roleName: string = "Member"; + if (incidentRoleId) { + const role: IncidentRole | null = await IncidentRoleService.findOneById( + { + id: incidentRoleId, + select: { + name: true, + }, + props: { + isRoot: true, + }, + }, + ); + if (role && role.name) { + roleName = role.name; + } + } + + const incidentNumber: number | null = + await IncidentService.getIncidentNumber({ + incidentId: incidentId, + }); + + if (userId) { + await IncidentFeedService.createIncidentFeedItem({ + incidentId: incidentId, + projectId: projectId, + incidentFeedEventType: IncidentFeedEventType.IncidentMemberAdded, + displayColor: Gray500, + feedInfoInMarkdown: `👤 Added **${await UserService.getUserMarkdownString( + { + userId: userId, + projectId: projectId, + }, + )}** as **${roleName}** to [Incident ${incidentNumber}](${(await IncidentService.getIncidentLinkInDashboard(projectId!, incidentId!)).toString()}).`, + userId: createdByUserId || undefined, + workspaceNotification: { + sendWorkspaceNotification: true, + notifyUserId: userId || undefined, + }, + }); + } + } + + // get notification rule where inviteOwners is true. + const notificationRules: Array = + await WorkspaceNotificationRuleService.getNotificationRulesWhereInviteOwnersIsTrue( + { + projectId: projectId!, + notificationFor: { + incidentId: incidentId, + }, + notificationRuleEventType: NotificationRuleEventType.Incident, + }, + ); + + WorkspaceNotificationRuleService.inviteUsersBasedOnRulesAndWorkspaceChannels( + { + notificationRules: notificationRules, + projectId: projectId!, + workspaceChannels: await IncidentService.getWorkspaceChannelForIncident( + { + incidentId: incidentId!, + }, + ), + userIds: [userId!], + }, + ).catch((error: Error) => { + logger.error(error); + }); + + return createdItem; + } } export default new Service(); diff --git a/Common/Server/Utils/WhatsAppTemplateUtil.ts b/Common/Server/Utils/WhatsAppTemplateUtil.ts index 87c71f450b..724ba96742 100644 --- a/Common/Server/Utils/WhatsAppTemplateUtil.ts +++ b/Common/Server/Utils/WhatsAppTemplateUtil.ts @@ -34,6 +34,7 @@ const templateDashboardLinkVariableMap: Partial< [WhatsAppTemplateIds.IncidentNotePostedOwnerNotification]: "incident_link", [WhatsAppTemplateIds.IncidentStateChangedOwnerNotification]: "incident_link", [WhatsAppTemplateIds.IncidentOwnerAddedNotification]: "incident_link", + [WhatsAppTemplateIds.IncidentMemberAddedNotification]: "incident_link", [WhatsAppTemplateIds.MonitorOwnerAddedNotification]: "monitor_link", [WhatsAppTemplateIds.MonitorCreatedOwnerNotification]: "monitor_link", [WhatsAppTemplateIds.MonitorStatusChangedOwnerNotification]: "monitor_link", @@ -75,6 +76,8 @@ const templateIdByEventType: Record< WhatsAppTemplateIds.IncidentStateChangedOwnerNotification, [NotificationSettingEventType.SEND_INCIDENT_OWNER_ADDED_NOTIFICATION]: WhatsAppTemplateIds.IncidentOwnerAddedNotification, + [NotificationSettingEventType.SEND_INCIDENT_MEMBER_ADDED_NOTIFICATION]: + WhatsAppTemplateIds.IncidentMemberAddedNotification, [NotificationSettingEventType.SEND_ALERT_CREATED_OWNER_NOTIFICATION]: WhatsAppTemplateIds.AlertCreatedOwnerNotification, [NotificationSettingEventType.SEND_ALERT_NOTE_POSTED_OWNER_NOTIFICATION]: diff --git a/Common/Types/Email/EmailTemplateType.ts b/Common/Types/Email/EmailTemplateType.ts index 008dff7a88..84b28fdbd6 100644 --- a/Common/Types/Email/EmailTemplateType.ts +++ b/Common/Types/Email/EmailTemplateType.ts @@ -36,6 +36,7 @@ enum EmailTemplateType { IncidentOwnerStateChanged = "IncidentOwnerStateChanged.hbs", IncidentOwnerNotePosted = "IncidentOwnerNotePosted.hbs", IncidentOwnerResourceCreated = "IncidentOwnerResourceCreated.hbs", + IncidentMemberAdded = "IncidentMemberAdded.hbs", AlertOwnerAdded = "AlertOwnerAdded.hbs", AlertOwnerStateChanged = "AlertOwnerStateChanged.hbs", diff --git a/Common/Types/NotificationSetting/NotificationSettingEventType.ts b/Common/Types/NotificationSetting/NotificationSettingEventType.ts index cd0efe0326..bfb9ae1793 100644 --- a/Common/Types/NotificationSetting/NotificationSettingEventType.ts +++ b/Common/Types/NotificationSetting/NotificationSettingEventType.ts @@ -4,6 +4,7 @@ enum NotificationSettingEventType { SEND_INCIDENT_NOTE_POSTED_OWNER_NOTIFICATION = "Send incident note posted notification when I am the owner of the incident", SEND_INCIDENT_STATE_CHANGED_OWNER_NOTIFICATION = "Send incident state changed notification when I am the owner of the incident", SEND_INCIDENT_OWNER_ADDED_NOTIFICATION = "Send notification when I am added as a owner to the incident", + SEND_INCIDENT_MEMBER_ADDED_NOTIFICATION = "Send notification when I am assigned to an incident as a member", // Alerts diff --git a/Common/Types/WhatsApp/WhatsAppTemplates.ts b/Common/Types/WhatsApp/WhatsAppTemplates.ts index 6e9ed59728..20e0ad148f 100644 --- a/Common/Types/WhatsApp/WhatsAppTemplates.ts +++ b/Common/Types/WhatsApp/WhatsAppTemplates.ts @@ -9,6 +9,7 @@ type TemplateIdsMap = { readonly IncidentNotePostedOwnerNotification: "oneuptime_incident_note_posted_owner_notification"; readonly IncidentStateChangedOwnerNotification: "oneuptime_incident_state_change_owner_notification"; readonly IncidentOwnerAddedNotification: "oneuptime_incident_owner_added_notification"; + readonly IncidentMemberAddedNotification: "oneuptime_incident_member_added_notification"; readonly AlertCreatedOwnerNotification: "oneuptime_alert_created_owner_notification"; readonly AlertNotePostedOwnerNotification: "oneuptime_alert_note_posted_owner_notification"; readonly AlertStateChangedOwnerNotification: "oneuptime_alert_state_changed_owner_notification"; @@ -58,6 +59,8 @@ const templateIds: TemplateIdsMap = { IncidentStateChangedOwnerNotification: "oneuptime_incident_state_change_owner_notification", IncidentOwnerAddedNotification: "oneuptime_incident_owner_added_notification", + IncidentMemberAddedNotification: + "oneuptime_incident_member_added_notification", AlertCreatedOwnerNotification: "oneuptime_alert_created_owner_notification", AlertNotePostedOwnerNotification: "oneuptime_alert_note_posted_owner_notification", @@ -144,6 +147,7 @@ export const WhatsAppTemplateMessages: WhatsAppTemplateMessagesDefinition = { [WhatsAppTemplateIds.IncidentNotePostedOwnerNotification]: `A new note was posted on incident #{{incident_number}} ({{incident_title}}). Review the incident using {{incident_link}} on the OneUptime dashboard for more context.`, [WhatsAppTemplateIds.IncidentStateChangedOwnerNotification]: `Incident #{{incident_number}} ({{incident_title}}) state changed to {{incident_state}}. Track the incident status using {{incident_link}} on the OneUptime dashboard for more context.`, [WhatsAppTemplateIds.IncidentOwnerAddedNotification]: `You have been added as an owner of incident #{{incident_number}} ({{incident_title}}). Manage the incident using {{incident_link}} on the OneUptime dashboard.`, + [WhatsAppTemplateIds.IncidentMemberAddedNotification]: `You have been assigned as {{incident_role}} to incident #{{incident_number}} ({{incident_title}}). Manage the incident using {{incident_link}} on the OneUptime dashboard.`, [WhatsAppTemplateIds.AlertCreatedOwnerNotification]: `Alert #{{alert_number}} ({{alert_title}}) has been created for project {{project_name}}. View alert details using {{alert_link}} on the OneUptime dashboard `, [WhatsAppTemplateIds.AlertNotePostedOwnerNotification]: `A new note was posted on alert #{{alert_number}} ({{alert_title}}). Review the alert using {{alert_link}} on the OneUptime dashboard for updates.`, [WhatsAppTemplateIds.AlertStateChangedOwnerNotification]: `Alert #{{alert_number}} ({{alert_title}}) state changed to {{alert_state}}. Track the alert status using {{alert_link}} on the OneUptime dashboard to stay informed.`, @@ -190,6 +194,7 @@ export const WhatsAppTemplateLanguage: Record = { [WhatsAppTemplateIds.IncidentNotePostedOwnerNotification]: "en", [WhatsAppTemplateIds.IncidentStateChangedOwnerNotification]: "en", [WhatsAppTemplateIds.IncidentOwnerAddedNotification]: "en", + [WhatsAppTemplateIds.IncidentMemberAddedNotification]: "en", [WhatsAppTemplateIds.AlertCreatedOwnerNotification]: "en", [WhatsAppTemplateIds.AlertNotePostedOwnerNotification]: "en", [WhatsAppTemplateIds.AlertStateChangedOwnerNotification]: "en", diff --git a/Worker/Jobs/IncidentMembers/SendMemberAddedNotification.ts b/Worker/Jobs/IncidentMembers/SendMemberAddedNotification.ts new file mode 100644 index 0000000000..8c63421a65 --- /dev/null +++ b/Worker/Jobs/IncidentMembers/SendMemberAddedNotification.ts @@ -0,0 +1,257 @@ +import RunCron from "../../Utils/Cron"; +import { CallRequestMessage } from "Common/Types/Call/CallRequest"; +import Dictionary from "Common/Types/Dictionary"; +import { EmailEnvelope } from "Common/Types/Email/EmailMessage"; +import EmailTemplateType from "Common/Types/Email/EmailTemplateType"; +import NotificationSettingEventType from "Common/Types/NotificationSetting/NotificationSettingEventType"; +import ObjectID from "Common/Types/ObjectID"; +import { SMSMessage } from "Common/Types/SMS/SMS"; +import PushNotificationMessage from "Common/Types/PushNotification/PushNotificationMessage"; +import { EVERY_MINUTE } from "Common/Utils/CronTime"; +import IncidentMemberService from "Common/Server/Services/IncidentMemberService"; +import IncidentService from "Common/Server/Services/IncidentService"; +import IncidentRoleService from "Common/Server/Services/IncidentRoleService"; +import UserNotificationSettingService from "Common/Server/Services/UserNotificationSettingService"; +import PushNotificationUtil from "Common/Server/Utils/PushNotificationUtil"; +import { createWhatsAppMessageFromTemplate } from "Common/Server/Utils/WhatsAppTemplateUtil"; +import Markdown, { MarkdownContentType } from "Common/Server/Types/Markdown"; +import Incident from "Common/Models/DatabaseModels/Incident"; +import IncidentMember from "Common/Models/DatabaseModels/IncidentMember"; +import IncidentRole from "Common/Models/DatabaseModels/IncidentRole"; +import Monitor from "Common/Models/DatabaseModels/Monitor"; +import User from "Common/Models/DatabaseModels/User"; +import { WhatsAppMessagePayload } from "Common/Types/WhatsApp/WhatsAppMessage"; + +RunCron( + "IncidentMember:SendMemberAddedEmail", + { schedule: EVERY_MINUTE, runOnStartup: false }, + async () => { + const incidentMembers: Array = + await IncidentMemberService.findAllBy({ + query: { + isMemberNotified: false, + }, + props: { + isRoot: true, + }, + skip: 0, + select: { + _id: true, + incidentId: true, + userId: true, + incidentRoleId: true, + user: { + email: true, + name: true, + }, + }, + }); + + const incidentMembersMap: Dictionary< + Array<{ user: User; roleName: string }> + > = {}; + + for (const incidentMember of incidentMembers) { + const incidentId: ObjectID = incidentMember.incidentId!; + const user: User = incidentMember.user!; + const incidentRoleId: ObjectID | undefined = incidentMember.incidentRoleId; + + let roleName: string = "Member"; + if (incidentRoleId) { + const role: IncidentRole | null = await IncidentRoleService.findOneById( + { + id: incidentRoleId, + select: { + name: true, + }, + props: { + isRoot: true, + }, + }, + ); + if (role && role.name) { + roleName = role.name; + } + } + + if (incidentMembersMap[incidentId.toString()] === undefined) { + incidentMembersMap[incidentId.toString()] = []; + } + + ( + incidentMembersMap[incidentId.toString()] as Array<{ + user: User; + roleName: string; + }> + ).push({ user, roleName }); + + // mark this as notified. + await IncidentMemberService.updateOneById({ + id: incidentMember.id!, + data: { + isMemberNotified: true, + }, + props: { + isRoot: true, + }, + }); + } + + // send email to all of these users. + + for (const incidentId in incidentMembersMap) { + if (!incidentMembersMap[incidentId]) { + continue; + } + + if ( + ( + incidentMembersMap[incidentId] as Array<{ + user: User; + roleName: string; + }> + ).length === 0 + ) { + continue; + } + + const members: Array<{ user: User; roleName: string }> = + incidentMembersMap[incidentId] as Array<{ + user: User; + roleName: string; + }>; + + // get incident details + const incident: Incident | null = await IncidentService.findOneById({ + id: new ObjectID(incidentId), + props: { + isRoot: true, + }, + + select: { + _id: true, + title: true, + description: true, + projectId: true, + project: { + name: true, + }, + currentIncidentState: { + name: true, + }, + incidentSeverity: { + name: true, + }, + monitors: { + name: true, + }, + incidentNumber: true, + }, + }); + + if (!incident) { + continue; + } + + const incidentNumber: string = incident.incidentNumber + ? `#${incident.incidentNumber}` + : ""; + + for (const member of members) { + const user: User = member.user; + const roleName: string = member.roleName; + + const vars: Dictionary = { + incidentTitle: incident.title!, + incidentNumber: incidentNumber, + projectName: incident.project!.name!, + currentState: incident.currentIncidentState!.name!, + incidentRole: roleName, + incidentDescription: await Markdown.convertToHTML( + incident.description! || "", + MarkdownContentType.Email, + ), + resourcesAffected: + incident + .monitors!.map((monitor: Monitor) => { + return monitor.name!; + }) + .join(", ") || "None", + incidentSeverity: incident.incidentSeverity!.name!, + incidentViewLink: ( + await IncidentService.getIncidentLinkInDashboard( + incident.projectId!, + incident.id!, + ) + ).toString(), + }; + + const incidentIdentifier: string = + incident.incidentNumber !== undefined + ? `#${incident.incidentNumber} (${incident.title})` + : incident.title!; + + const emailMessage: EmailEnvelope = { + templateType: EmailTemplateType.IncidentMemberAdded, + vars: vars, + subject: `You have been assigned as ${roleName} to Incident ${incidentNumber} - ${incident.title}`, + }; + + const sms: SMSMessage = { + message: `This is a message from OneUptime. You have been assigned as ${roleName} to the incident ${incidentIdentifier}. To unsubscribe from this notification go to User Settings in OneUptime Dashboard.`, + }; + + const callMessage: CallRequestMessage = { + data: [ + { + sayMessage: `This is a message from OneUptime. You have been assigned as ${roleName} to the incident ${incidentIdentifier}. To unsubscribe from this notification go to User Settings in OneUptime Dashboard. Good bye.`, + }, + ], + }; + + const pushMessage: PushNotificationMessage = + PushNotificationUtil.createGenericNotification({ + title: `Assigned as ${roleName} to Incident ${incidentNumber}`, + body: `You have been assigned as ${roleName} to the incident ${incidentIdentifier}. Click to view details.`, + clickAction: ( + await IncidentService.getIncidentLinkInDashboard( + incident.projectId!, + incident.id!, + ) + ).toString(), + tag: "incident-member-added", + requireInteraction: false, + }); + + const eventType: NotificationSettingEventType = + NotificationSettingEventType.SEND_INCIDENT_MEMBER_ADDED_NOTIFICATION; + + const whatsAppMessage: WhatsAppMessagePayload = + createWhatsAppMessageFromTemplate({ + eventType, + templateVariables: { + incident_title: incident.title!, + incident_number: + incident.incidentNumber !== undefined + ? incident.incidentNumber.toString() + : "", + incident_role: roleName, + incident_link: vars["incidentViewLink"] || "", + }, + }); + + await UserNotificationSettingService.sendUserNotification({ + userId: user.id!, + projectId: incident.projectId!, + emailEnvelope: emailMessage, + smsMessage: sms, + callRequestMessage: callMessage, + pushNotificationMessage: pushMessage, + whatsAppMessage, + incidentId: incident.id!, + eventType, + }); + } + } + }, +); diff --git a/Worker/Routes.ts b/Worker/Routes.ts index 4b0f2e2709..ea274c3f8a 100644 --- a/Worker/Routes.ts +++ b/Worker/Routes.ts @@ -13,6 +13,9 @@ import "./Jobs/IncidentOwners/SendNotePostedNotification"; import "./Jobs/IncidentOwners/SendOwnerAddedNotification"; import "./Jobs/IncidentOwners/SendStateChangeNotification"; +// Incident Members +import "./Jobs/IncidentMembers/SendMemberAddedNotification"; + // Monitor Jobs. import "./Jobs/Monitor/KeepCurrentStateConsistent";