diff --git a/server/notifications/utils/scenarioMapperr.js b/server/notifications/utils/scenarioMapperr.js index caca96341..a2417961f 100644 --- a/server/notifications/utils/scenarioMapperr.js +++ b/server/notifications/utils/scenarioMapperr.js @@ -7,12 +7,13 @@ const tasksUpdatedCreatedBuilder = require("../scenarioBuilders/tasksUpdatedCreatedBuilder"); const jobStatusChangeBuilder = require("../scenarioBuilders/jobStatusChangeBuilder"); const jobAssignedToMeBuilder = require("../scenarioBuilders/jobAssignedToMeBuilder"); + const notificationScenarios = [ { key: "job-assigned-to-me", table: "jobs", fields: ["employee_pre", "employee_body", "employee_csr", "employee_refinish"], - matchEmployee: true, + matchToUserFields: ["employee_pre", "employee_body", "employee_csr", "employee_refinish"], builder: jobAssignedToMeBuilder }, { diff --git a/server/notifications/utils/scenarioParser.js b/server/notifications/utils/scenarioParser.js index feaf9dc58..b93efe8de 100644 --- a/server/notifications/utils/scenarioParser.js +++ b/server/notifications/utils/scenarioParser.js @@ -3,17 +3,22 @@ const { client: gqlClient } = require("../../graphql-client/graphql-client"); const queries = require("../../graphql-client/queries"); const { isEmpty, isFunction } = require("lodash"); const { getMatchingScenarios } = require("./scenarioMapperr"); -const { writeFile } = require("node:fs").promises; +/** + * 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. + * @param {string} jobIdField - The field used to identify the job ID. + */ const scenarioParser = async (req, jobIdField) => { - // Destructure required fields from the request body const { event, trigger, table } = req.body; if (!event?.data || !trigger || !table) { throw new Error("Missing required request fields: event data, trigger, or table."); } - // Step 1: Parse the changes from the event + // Step 1: Parse event data to extract necessary details. const eventData = await eventParser({ newData: event.data.new, oldData: event.data.old, @@ -22,7 +27,7 @@ const scenarioParser = async (req, jobIdField) => { jobIdField }); - // Step 2: Query jobWatchers for this job + // Step 2: Query job watchers for the given job ID. const watcherData = await gqlClient.request(queries.GET_JOB_WATCHERS, { jobid: eventData.jobId }); @@ -38,7 +43,7 @@ const scenarioParser = async (req, jobIdField) => { return; } - // Step 3: Infer bodyshop information from the job and validate + // Step 3: Retrieve body shop information from the job. const bodyShopId = watcherData?.job?.bodyshop?.id; const bodyShopName = watcherData?.job?.bodyshop?.shopname; @@ -46,7 +51,7 @@ const scenarioParser = async (req, jobIdField) => { throw new Error("No bodyshop data found for this job."); } - // Step 4: Get matching scenarios based on eventData and jobWatchers + // Step 4: Determine matching scenarios based on event data. const matchingScenarios = getMatchingScenarios({ ...eventData, jobWatchers, @@ -58,7 +63,6 @@ const scenarioParser = async (req, jobIdField) => { return; } - // Prepare the final scenario data const finalScenarioData = { ...eventData, jobWatchers, @@ -67,8 +71,7 @@ const scenarioParser = async (req, jobIdField) => { matchingScenarios }; - // Step 5: Query associations (notification_settings) for each watcher - // Filter by both useremail and shopid + // Step 5: Query notification settings for job watchers. const associationsData = await gqlClient.request(queries.GET_NOTIFICATION_ASSOCIATIONS, { emails: jobWatchers.map((x) => x.email), shopid: bodyShopId @@ -78,33 +81,24 @@ const scenarioParser = async (req, jobIdField) => { return; } - // Step 6: For each matching scenario, add a scenarioWatchers property - // that includes only the jobWatchers with at least one notification method enabled. - // Each watcher object is formatted as: { user, email, app, fcm } + // Step 6: Filter scenario watchers based on enabled notification methods. finalScenarioData.matchingScenarios = finalScenarioData.matchingScenarios.map((scenario) => ({ ...scenario, scenarioWatchers: associationsData.associations .filter((assoc) => { - // Retrieve the settings object for this scenario (it now contains app, email, and fcm) const settings = assoc.notification_settings && assoc.notification_settings[scenario.key]; - // Only include this association if at least one notification channel is enabled return settings && (settings.app || settings.email || settings.fcm); }) .map((assoc) => { const settings = assoc.notification_settings[scenario.key]; - // Determine the email from the association—either from assoc.user or assoc.useremail const watcherEmail = assoc.user || assoc.useremail; - // Find the matching watcher object from jobWatchers using the email address const matchingWatcher = jobWatchers.find((watcher) => watcher.email === watcherEmail); return { - // This is the common identifier (email in this case) user: watcherEmail, - // Notification settings for this scenario email: settings.email, app: settings.app, fcm: settings.fcm, - // Additional fields from the watcher lookup firstName: matchingWatcher ? matchingWatcher.firstName : undefined, lastName: matchingWatcher ? matchingWatcher.lastName : undefined, employeeId: matchingWatcher ? matchingWatcher.employeeId : undefined @@ -116,21 +110,40 @@ const scenarioParser = async (req, jobIdField) => { return; } - // Step 7: Call builder functions for each matching scenario (fire-and-forget) - // Only invoke a builder if its scenario has at least one watcher + // Step 7: Trigger scenario builders for matching scenarios with eligible watchers. for (const scenario of finalScenarioData.matchingScenarios) { if (isEmpty(scenario.scenarioWatchers) || !isFunction(scenario.builder)) { continue; } + + let eligibleWatchers = scenario.scenarioWatchers; + + // Ensure watchers are only notified if they are assigned to the changed field. + 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) + ) + ); + } + + if (isEmpty(eligibleWatchers)) { + continue; + } + + // Step 8: Filter scenario fields to only include changed fields. + const filteredScenarioFields = + scenario.fields?.filter((field) => eventData.changedFieldNames.includes(field)) || []; + scenario.builder({ trigger: finalScenarioData.trigger.name, bodyShopId: finalScenarioData.bodyShopId, bodyShopName: finalScenarioData.bodyShopName, scenarioKey: scenario.key, scenarioTable: scenario.table, - scenarioFields: scenario.fields, + scenarioFields: filteredScenarioFields, scenarioBuilder: scenario.builder, - scenarioWatchers: scenario.scenarioWatchers, + scenarioWatchers: eligibleWatchers, jobId: finalScenarioData.jobId, isNew: finalScenarioData.isNew, changedFieldNames: finalScenarioData.changedFieldNames,