269 lines
7.5 KiB
JavaScript
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
|
|
};
|