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