Files
bodyshop/server/notifications/dispatchJobWatcherNotification.js

269 lines
7.5 KiB
JavaScript

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
};