Files
bodyshop/server/notifications/scenarioParser.js

242 lines
8.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @module scenarioParser
* @description
* This module exports a function that parses an event and triggers notification scenarios based on the event data.
* It integrates with event parsing utilities, GraphQL queries, and notification queues to manage the dispatching
* of notifications via email and app channels. The function processes event data, identifies relevant scenarios,
* queries user notification preferences, and dispatches notifications accordingly.
*/
const eventParser = require("./eventParser");
const { client: gqlClient } = require("../graphql-client/graphql-client");
const queries = require("../graphql-client/queries");
const { isEmpty, isFunction } = require("lodash");
const { getMatchingScenarios } = require("./scenarioMapper");
const { dispatchEmailsToQueue } = require("./queues/emailQueue");
const { dispatchAppsToQueue } = require("./queues/appQueue");
// If true, the user who commits the action will NOT receive notifications; if false, they will.
const FILTER_SELF_FROM_WATCHERS = process.env?.FILTER_SELF_FROM_WATCHERS === "true";
/**
* Parses an event and determines matching scenarios for notifications.
* Queries job watchers and notification settings before triggering scenario builders.
*
* @param {Object} req - The request object containing event data, trigger, table, and logger.
* @param {string} jobIdField - The field name used to extract the job ID from the event data.
* @returns {Promise<void>} Resolves when the parsing and notification dispatching process is complete.
* @throws {Error} If required request fields (event data, trigger, or table) or body shop data are missing.
*/
const scenarioParser = async (req, jobIdField) => {
const { event, trigger, table } = req.body;
const { logger } = req;
// Validate we know what user committed the action that fired the parser
const hasuraUserId = event?.session_variables?.["x-hasura-user-id"];
// Bail if we don't know
if (!hasuraUserId) {
return;
}
// Validate that required fields are present in the request body
if (!event?.data || !trigger || !table) {
throw new Error("Missing required request fields: event data, trigger, or table.");
}
// Step 1: Parse the event data to extract details like job ID and changed fields
const eventData = await eventParser({
newData: event.data.new,
oldData: event.data.old,
trigger,
table,
jobIdField
});
// Step 2: Query job watchers associated with the job ID using GraphQL
const watcherData = await gqlClient.request(queries.GET_JOB_WATCHERS, {
jobid: eventData.jobId
});
// Transform watcher data into a simplified format with email and employee details
let jobWatchers = watcherData?.job_watchers?.map((watcher) => ({
email: watcher.user_email,
firstName: watcher?.user?.employee?.first_name,
lastName: watcher?.user?.employee?.last_name,
employeeId: watcher?.user?.employee?.id,
authId: watcher?.user?.authid
}));
if (FILTER_SELF_FROM_WATCHERS) {
jobWatchers = jobWatchers.filter((watcher) => watcher.authId !== hasuraUserId);
}
// Exit early if no job watchers are found for this job
if (isEmpty(jobWatchers)) {
return;
}
// Step 3: Extract body shop information from the job data
const bodyShopId = watcherData?.job?.bodyshop?.id;
const bodyShopName = watcherData?.job?.bodyshop?.shopname;
const jobRoNumber = watcherData?.job?.ro_number;
const jobClaimNumber = watcherData?.job?.clm_no;
// Validate that body shop data exists, as its required for notifications
if (!bodyShopId || !bodyShopName) {
throw new Error("No bodyshop data found for this job.");
}
// Step 4: Identify scenarios that match the event data and job context
const matchingScenarios = getMatchingScenarios({
...eventData,
jobWatchers,
bodyShopId,
bodyShopName
});
// Exit early if no matching scenarios are identified
if (isEmpty(matchingScenarios)) {
return;
}
// Combine event data with additional context for scenario processing
const finalScenarioData = {
...eventData,
jobWatchers,
bodyShopId,
bodyShopName,
matchingScenarios
};
// Step 5: Query notification settings for the job watchers
const associationsData = await gqlClient.request(queries.GET_NOTIFICATION_ASSOCIATIONS, {
emails: jobWatchers.map((x) => x.email),
shopid: bodyShopId
});
// Exit early if no notification associations are found
if (isEmpty(associationsData?.associations)) {
return;
}
// Step 6: Filter scenario watchers based on their enabled notification methods
finalScenarioData.matchingScenarios = finalScenarioData.matchingScenarios.map((scenario) => ({
...scenario,
scenarioWatchers: associationsData.associations
.filter((assoc) => {
const settings = assoc.notification_settings && assoc.notification_settings[scenario.key];
// Include only watchers with at least one enabled notification method (app, email, or FCM)
return settings && (settings.app || settings.email || settings.fcm);
})
.map((assoc) => {
const settings = assoc.notification_settings[scenario.key];
const watcherEmail = assoc.useremail;
const matchingWatcher = jobWatchers.find((watcher) => watcher.email === watcherEmail);
// Build watcher object with notification preferences and personal details
return {
user: watcherEmail,
email: settings.email,
app: settings.app,
fcm: settings.fcm,
firstName: matchingWatcher?.firstName,
lastName: matchingWatcher?.lastName,
employeeId: matchingWatcher?.employeeId,
associationId: assoc.id
};
})
}));
// Exit early if no scenarios have eligible watchers after filtering
if (isEmpty(finalScenarioData?.matchingScenarios)) {
return;
}
// Step 7: Build and collect scenarios to dispatch notifications for
const scenariosToDispatch = [];
for (const scenario of finalScenarioData.matchingScenarios) {
// Skip if no watchers or no builder function is defined for the scenario
if (isEmpty(scenario.scenarioWatchers) || !isFunction(scenario.builder)) {
continue;
}
let eligibleWatchers = scenario.scenarioWatchers;
// Filter watchers to only those assigned to changed fields, if specified
if (scenario.matchToUserFields && scenario.matchToUserFields.length > 0) {
eligibleWatchers = scenario.scenarioWatchers.filter((watcher) =>
scenario.matchToUserFields.some(
(field) => eventData.changedFieldNames.includes(field) && eventData.data[field]?.includes(watcher.employeeId)
)
);
}
// Skip if no watchers remain after filtering
if (isEmpty(eligibleWatchers)) {
continue;
}
// Step 8: Filter scenario fields to include only those that changed
const filteredScenarioFields =
scenario.fields?.filter((field) => eventData.changedFieldNames.includes(field)) || [];
// Use the scenarios builder to construct the notification data
scenariosToDispatch.push(
scenario.builder({
trigger: finalScenarioData.trigger.name,
bodyShopId: finalScenarioData.bodyShopId,
bodyShopName: finalScenarioData.bodyShopName,
scenarioKey: scenario.key,
scenarioTable: scenario.table,
scenarioFields: filteredScenarioFields,
scenarioBuilder: scenario.builder,
scenarioWatchers: eligibleWatchers,
jobId: finalScenarioData.jobId,
jobRoNumber: jobRoNumber,
jobClaimNumber: jobClaimNumber,
isNew: finalScenarioData.isNew,
changedFieldNames: finalScenarioData.changedFieldNames,
changedFields: finalScenarioData.changedFields,
data: finalScenarioData.data
})
);
}
// Exit early if no scenarios are ready to dispatch
if (isEmpty(scenariosToDispatch)) {
return;
}
// Step 9: Dispatch email notifications to the email queue
const emailsToDispatch = scenariosToDispatch.map((scenario) => scenario?.email);
if (!isEmpty(emailsToDispatch)) {
dispatchEmailsToQueue({
emailsToDispatch,
logger
}).catch((e) =>
// Log any errors encountered during email dispatching
logger.log("Something went wrong dispatching emails to the Email Notification Queue", "error", "queue", null, {
message: e?.message
})
);
}
// Step 10: Dispatch app notifications to the app queue
const appsToDispatch = scenariosToDispatch.map((scenario) => scenario?.app);
if (!isEmpty(appsToDispatch)) {
dispatchAppsToQueue({
appsToDispatch,
logger
}).catch((e) =>
// Log any errors encountered during app notification dispatching
logger.log("Something went wrong dispatching apps to the App Notification Queue", "error", "queue", null, {
message: e?.message
})
);
}
};
module.exports = scenarioParser;