feature/IO-2433-esignature - Add in Notifications
This commit is contained in:
268
server/notifications/dispatchJobWatcherNotification.js
Normal file
268
server/notifications/dispatchJobWatcherNotification.js
Normal file
@@ -0,0 +1,268 @@
|
||||
const { client: gqlClient } = require("../graphql-client/graphql-client");
|
||||
const { GET_JOB_WATCHERS, GET_NOTIFICATION_ASSOCIATIONS } = require("../graphql-client/queries");
|
||||
const { dispatchEmailsToQueue } = require("./queues/emailQueue");
|
||||
const { dispatchAppsToQueue } = require("./queues/appQueue");
|
||||
const { dispatchFcmsToQueue } = require("./queues/fcmQueue");
|
||||
|
||||
/**
|
||||
* Default channel preferences to fall back on if a recipient doesn't have specific preferences set for a scenario.
|
||||
* @type {Readonly<{app: boolean, email: boolean, fcm: boolean}>}
|
||||
*/
|
||||
const DEFAULT_CHANNEL_PREFERENCES = Object.freeze({
|
||||
app: false,
|
||||
email: false,
|
||||
fcm: false
|
||||
});
|
||||
|
||||
/**
|
||||
* Normalizes channel preferences for a recipient based on their specific preferences for a scenario, falling back to
|
||||
* default preferences if not set.
|
||||
* @param preferences
|
||||
* @param fallbackPreferences
|
||||
* @returns {{app, email, fcm}}
|
||||
*/
|
||||
const normalizeChannelPreferences = (preferences, fallbackPreferences = DEFAULT_CHANNEL_PREFERENCES) => ({
|
||||
app: preferences?.app ?? fallbackPreferences.app ?? false,
|
||||
email: preferences?.email ?? fallbackPreferences.email ?? false,
|
||||
fcm: preferences?.fcm ?? fallbackPreferences.fcm ?? false
|
||||
});
|
||||
|
||||
/**
|
||||
* Builds notification payloads for app, email, and FCM channels based on the provided parameters and recipient
|
||||
* preferences.
|
||||
* @param param0
|
||||
* @param param0.jobId
|
||||
* @param param0.jobRoNumber
|
||||
* @param param0.bodyShopId
|
||||
* @param param0.bodyShopName
|
||||
* @param param0.bodyShopTimezone
|
||||
* @param param0.scenarioKey
|
||||
* @param param0.scenarioTable
|
||||
* @param param0.key
|
||||
* @param param0.body
|
||||
* @param param0.variables
|
||||
* @param param0.recipients
|
||||
* @returns {{app: {jobId: *, jobRoNumber: *, bodyShopId: *, scenarioKey: *, scenarioTable: *, key: *, body: *, variables: *, recipients: *}, email: {jobId: *, jobRoNumber: *, bodyShopName: *, bodyShopTimezone: *, body: *, recipients: *}, fcm: {jobId: *, jobRoNumber: *, bodyShopId: *, bodyShopName: *, bodyShopTimezone: *, scenarioKey: *, scenarioTable: *, key: *, body: *, variables: *, recipients: *}}}
|
||||
*/
|
||||
const buildNotificationPayloads = ({
|
||||
jobId,
|
||||
jobRoNumber,
|
||||
bodyShopId,
|
||||
bodyShopName,
|
||||
bodyShopTimezone,
|
||||
scenarioKey,
|
||||
scenarioTable,
|
||||
key,
|
||||
body,
|
||||
variables,
|
||||
recipients
|
||||
}) => ({
|
||||
app: {
|
||||
jobId,
|
||||
jobRoNumber,
|
||||
bodyShopId,
|
||||
scenarioKey,
|
||||
scenarioTable,
|
||||
key,
|
||||
body,
|
||||
variables,
|
||||
recipients: recipients
|
||||
.filter((recipient) => recipient.app)
|
||||
.map(({ user, employeeId, associationId }) => ({
|
||||
user,
|
||||
bodyShopId,
|
||||
employeeId,
|
||||
associationId
|
||||
}))
|
||||
},
|
||||
email: {
|
||||
jobId,
|
||||
jobRoNumber,
|
||||
bodyShopName,
|
||||
bodyShopTimezone,
|
||||
body,
|
||||
recipients: recipients
|
||||
.filter((recipient) => recipient.email)
|
||||
.map(({ user, firstName, lastName }) => ({ user, firstName, lastName }))
|
||||
},
|
||||
fcm: {
|
||||
jobId,
|
||||
jobRoNumber,
|
||||
bodyShopId,
|
||||
bodyShopName,
|
||||
bodyShopTimezone,
|
||||
scenarioKey,
|
||||
scenarioTable,
|
||||
key,
|
||||
body,
|
||||
variables,
|
||||
recipients: recipients
|
||||
.filter((recipient) => recipient.fcm)
|
||||
.map(({ user, employeeId, associationId }) => ({
|
||||
user,
|
||||
bodyShopId,
|
||||
employeeId,
|
||||
associationId
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Dispatches notifications to job watchers based on their preferences for a given scenario. It retrieves the watchers
|
||||
* of a job, determines their notification preferences, builds the appropriate payloads for each channel, and dispatches
|
||||
* the notifications to the respective queues.
|
||||
* @param param0
|
||||
* @param param0.jobId
|
||||
* @param param0.scenarioKey
|
||||
* @param param0.key
|
||||
* @param param0.body
|
||||
* @param param0.variables
|
||||
* @param param0.scenarioTable
|
||||
* @param param0.extraRecipientEmails
|
||||
* @param param0.defaultChannelPreferences
|
||||
* @param param0.logger
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function dispatchJobWatcherNotification({
|
||||
jobId,
|
||||
scenarioKey,
|
||||
key,
|
||||
body,
|
||||
variables = {},
|
||||
scenarioTable = "esignature_documents",
|
||||
extraRecipientEmails = [],
|
||||
defaultChannelPreferences = DEFAULT_CHANNEL_PREFERENCES,
|
||||
logger
|
||||
}) {
|
||||
if (!jobId || !scenarioKey || !key || !body) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const watcherData = await gqlClient.request(GET_JOB_WATCHERS, { jobid: jobId });
|
||||
const bodyShopId = watcherData?.job?.bodyshop?.id;
|
||||
const bodyShopName = watcherData?.job?.bodyshop?.shopname;
|
||||
const bodyShopTimezone = watcherData?.job?.bodyshop?.timezone;
|
||||
const jobRoNumber = watcherData?.job?.ro_number;
|
||||
|
||||
if (!bodyShopId || !bodyShopName) {
|
||||
logger?.log?.("dispatch-job-watcher-notification-missing-job-meta", "WARN", "notifications", "api", {
|
||||
jobId,
|
||||
scenarioKey
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const recipientsByEmail = new Map();
|
||||
|
||||
for (const watcher of watcherData?.job_watchers || []) {
|
||||
if (!watcher?.user_email) continue;
|
||||
|
||||
recipientsByEmail.set(watcher.user_email, {
|
||||
email: watcher.user_email,
|
||||
firstName: watcher?.user?.employee?.first_name || null,
|
||||
lastName: watcher?.user?.employee?.last_name || null,
|
||||
employeeId: watcher?.user?.employee?.id || null
|
||||
});
|
||||
}
|
||||
|
||||
for (const recipientEmail of extraRecipientEmails) {
|
||||
if (!recipientEmail || recipientsByEmail.has(recipientEmail)) continue;
|
||||
recipientsByEmail.set(recipientEmail, {
|
||||
email: recipientEmail,
|
||||
firstName: null,
|
||||
lastName: null,
|
||||
employeeId: null
|
||||
});
|
||||
}
|
||||
|
||||
const recipientEmails = [...recipientsByEmail.keys()];
|
||||
|
||||
if (!recipientEmails.length) {
|
||||
logger?.log?.("dispatch-job-watcher-notification-no-recipients", "INFO", "notifications", "api", {
|
||||
jobId,
|
||||
scenarioKey
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const associationsData = await gqlClient.request(GET_NOTIFICATION_ASSOCIATIONS, {
|
||||
emails: recipientEmails,
|
||||
shopid: bodyShopId
|
||||
});
|
||||
|
||||
const eligibleRecipients = (associationsData?.associations || [])
|
||||
.map((association) => {
|
||||
const preferences = normalizeChannelPreferences(
|
||||
association?.notification_settings?.[scenarioKey],
|
||||
defaultChannelPreferences
|
||||
);
|
||||
|
||||
if (!preferences.app && !preferences.email && !preferences.fcm) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const watcher = recipientsByEmail.get(association.useremail);
|
||||
|
||||
return {
|
||||
user: association.useremail,
|
||||
app: preferences.app,
|
||||
email: preferences.email,
|
||||
fcm: preferences.fcm,
|
||||
firstName: watcher?.firstName || null,
|
||||
lastName: watcher?.lastName || null,
|
||||
employeeId: watcher?.employeeId || null,
|
||||
associationId: association.id
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (!eligibleRecipients.length) {
|
||||
logger?.log?.("dispatch-job-watcher-notification-no-eligible-recipients", "INFO", "notifications", "api", {
|
||||
jobId,
|
||||
scenarioKey,
|
||||
bodyShopId
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const payloads = buildNotificationPayloads({
|
||||
jobId,
|
||||
jobRoNumber,
|
||||
bodyShopId,
|
||||
bodyShopName,
|
||||
bodyShopTimezone,
|
||||
scenarioKey,
|
||||
scenarioTable,
|
||||
key,
|
||||
body,
|
||||
variables,
|
||||
recipients: eligibleRecipients
|
||||
});
|
||||
|
||||
const dispatches = [];
|
||||
|
||||
if (payloads.email.recipients.length) {
|
||||
dispatches.push(dispatchEmailsToQueue({ emailsToDispatch: [payloads.email], logger }));
|
||||
}
|
||||
|
||||
if (payloads.app.recipients.length) {
|
||||
dispatches.push(dispatchAppsToQueue({ appsToDispatch: [payloads.app], logger }));
|
||||
}
|
||||
|
||||
if (payloads.fcm.recipients.length) {
|
||||
dispatches.push(dispatchFcmsToQueue({ fcmsToDispatch: [payloads.fcm], logger }));
|
||||
}
|
||||
|
||||
if (!dispatches.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await Promise.all(dispatches);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_CHANNEL_PREFERENCES,
|
||||
dispatchJobWatcherNotification
|
||||
};
|
||||
Reference in New Issue
Block a user