303 lines
10 KiB
JavaScript
303 lines
10 KiB
JavaScript
/**
|
||
* @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 !== "false";
|
||
|
||
/**
|
||
* 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 path (e.g., "req.body.event.new.id") to extract the job ID.
|
||
* @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,
|
||
sessionUtils: { getBodyshopFromRedis }
|
||
} = req;
|
||
|
||
// Step 1: Validate we know what user committed the action that fired the parser
|
||
// console.log("Step 1");
|
||
|
||
const hasuraUserId = event?.session_variables?.["x-hasura-user-id"];
|
||
|
||
// Bail if we don't know who started the scenario
|
||
if (!hasuraUserId) {
|
||
logger.log("No Hasura user ID found, skipping notification parsing", "info", "notifications");
|
||
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 2: Extract just the jobId using the provided jobIdField
|
||
// console.log("Step 2");
|
||
|
||
let jobId = null;
|
||
if (jobIdField) {
|
||
let keyName = jobIdField;
|
||
const prefix = "req.body.event.new.";
|
||
if (keyName.startsWith(prefix)) {
|
||
keyName = keyName.slice(prefix.length);
|
||
}
|
||
jobId = event.data.new[keyName] || (event.data.old && event.data.old[keyName]) || null;
|
||
}
|
||
|
||
if (!jobId) {
|
||
logger.log(`No jobId found using path "${jobIdField}", skipping notification parsing`, "info", "notifications");
|
||
return;
|
||
}
|
||
|
||
// Step 3: Query job watchers associated with the job ID using GraphQL
|
||
// console.log("Step 3");
|
||
|
||
const watcherData = await gqlClient.request(queries.GET_JOB_WATCHERS, {
|
||
jobid: 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)) {
|
||
logger.log(`No watchers found for jobId "${jobId}", skipping notification parsing`, "info", "notifications");
|
||
return;
|
||
}
|
||
|
||
// Step 5: Perform the full event diff now that we know there are watchers
|
||
// console.log("Step 5");
|
||
|
||
const eventData = await eventParser({
|
||
newData: event.data.new,
|
||
oldData: event.data.old,
|
||
trigger,
|
||
table,
|
||
jobId
|
||
});
|
||
|
||
// Step 6: Extract body shop information from the job data
|
||
// console.log("Step 6");
|
||
|
||
const bodyShopId = watcherData?.job?.bodyshop?.id;
|
||
const bodyShopName = watcherData?.job?.bodyshop?.shopname;
|
||
const bodyShopTimezone = watcherData?.job?.bodyshop?.timezone;
|
||
|
||
const jobRoNumber = watcherData?.job?.ro_number;
|
||
const jobClaimNumber = watcherData?.job?.clm_no;
|
||
|
||
// Validate that body shop data exists, as it’s required for notifications
|
||
if (!bodyShopId || !bodyShopName) {
|
||
throw new Error("No bodyshop data found for this job.");
|
||
}
|
||
|
||
// Step 7: Identify scenarios that match the event data and job context
|
||
// console.log("Step 7");
|
||
|
||
const matchingScenarios = await getMatchingScenarios(
|
||
{
|
||
...eventData,
|
||
jobWatchers,
|
||
bodyShopId,
|
||
bodyShopName
|
||
},
|
||
getBodyshopFromRedis
|
||
);
|
||
|
||
// Exit early if no matching scenarios are identified
|
||
if (isEmpty(matchingScenarios)) {
|
||
logger.log(
|
||
`No matching scenarios found for jobId "${jobId}", skipping notification dispatch`,
|
||
"info",
|
||
"notifications"
|
||
);
|
||
return;
|
||
}
|
||
|
||
// Combine event data with additional context for scenario processing
|
||
const finalScenarioData = {
|
||
...eventData,
|
||
jobWatchers,
|
||
bodyShopId,
|
||
bodyShopName,
|
||
bodyShopTimezone,
|
||
matchingScenarios
|
||
};
|
||
|
||
// Step 8: Query notification settings for the job watchers
|
||
// console.log("Step 8");
|
||
|
||
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)) {
|
||
logger.log(
|
||
`No notification associations found for jobId "${jobId}", skipping notification dispatch`,
|
||
"info",
|
||
"notifications"
|
||
);
|
||
return;
|
||
}
|
||
|
||
// Step 9: Filter scenario watchers based on their enabled notification methods
|
||
// console.log("Step 9");
|
||
|
||
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)) {
|
||
logger.log(
|
||
`No eligible watchers after filtering for jobId "${jobId}", skipping notification dispatch`,
|
||
"info",
|
||
"notifications"
|
||
);
|
||
return;
|
||
}
|
||
|
||
// Step 10: Build and collect scenarios to dispatch notifications for
|
||
// console.log("Step 10");
|
||
|
||
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 11: Filter scenario fields to include only those that changed
|
||
// console.log("Step 11");
|
||
|
||
const filteredScenarioFields =
|
||
scenario.fields?.filter((field) => eventData.changedFieldNames.includes(field)) || [];
|
||
|
||
// Use the scenario’s builder to construct the notification data
|
||
scenariosToDispatch.push(
|
||
scenario.builder({
|
||
trigger: finalScenarioData.trigger.name,
|
||
bodyShopId: finalScenarioData.bodyShopId,
|
||
bodyShopName: finalScenarioData.bodyShopName,
|
||
bodyShopTimezone: finalScenarioData.bodyShopTimezone,
|
||
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
|
||
})
|
||
);
|
||
}
|
||
|
||
if (isEmpty(scenariosToDispatch)) {
|
||
logger.log(`No scenarios to dispatch for jobId "${jobId}" after building`, "info", "notifications");
|
||
return;
|
||
}
|
||
|
||
// Step 12: Dispatch email notifications to the email queue
|
||
// console.log("Step 12");
|
||
|
||
const emailsToDispatch = scenariosToDispatch.map((scenario) => scenario?.email);
|
||
if (!isEmpty(emailsToDispatch)) {
|
||
dispatchEmailsToQueue({ emailsToDispatch, logger }).catch((e) =>
|
||
logger.log("Something went wrong dispatching emails to the Email Notification Queue", "error", "queue", null, {
|
||
message: e?.message,
|
||
stack: e?.stack
|
||
})
|
||
);
|
||
}
|
||
|
||
// Step 13: Dispatch app notifications to the app queue
|
||
// console.log("Step 13");
|
||
|
||
const appsToDispatch = scenariosToDispatch.map((scenario) => scenario?.app);
|
||
if (!isEmpty(appsToDispatch)) {
|
||
dispatchAppsToQueue({ appsToDispatch, logger }).catch((e) =>
|
||
logger.log("Something went wrong dispatching apps to the App Notification Queue", "error", "queue", null, {
|
||
message: e?.message,
|
||
stack: e?.stack
|
||
})
|
||
);
|
||
}
|
||
};
|
||
|
||
module.exports = scenarioParser;
|