diff --git a/server/notifications/eventParser.js b/server/notifications/eventParser.js index 2357dae6c..654006bbc 100644 --- a/server/notifications/eventParser.js +++ b/server/notifications/eventParser.js @@ -10,8 +10,8 @@ * @param {string} params.table - The name of the table where the event occurred. * @param {string} [params.jobIdField] - The field name or key path (e.g., "req.body.event.new.jobid") used to extract the job ID. * @returns {Promise} An object containing: - * - {@link changedFieldNames}: An array of field names that have changed. - * - {@link changedFields}: An object mapping changed field names to their new values (or `null` if the field was removed). + * - {string[]} changedFieldNames - An array of field names that have changed. + * - {Object} changedFields - An object mapping changed field names to an object with `old` and `new` values. * - {boolean} isNew - Indicates if the event is for new data (i.e., no oldData exists). * - {Object} data - The new data. * - {string} trigger - The event trigger. @@ -24,8 +24,10 @@ const eventParser = async ({ oldData, newData, trigger, table, jobIdField }) => let changedFieldNames = []; if (isNew) { - // If there's no old data, every field in newData is considered changed (new) - changedFields = { ...newData }; + // If there's no old data, every field in newData is considered new + changedFields = Object.fromEntries( + Object.entries(newData).map(([key, value]) => [key, { old: undefined, new: value }]) + ); changedFieldNames = Object.keys(newData); } else { // Compare oldData with newData for changes @@ -36,7 +38,10 @@ const eventParser = async ({ oldData, newData, trigger, table, jobIdField }) => !Object.prototype.hasOwnProperty.call(oldData, key) || JSON.stringify(oldData[key]) !== JSON.stringify(newData[key]) ) { - changedFields[key] = newData[key]; + changedFields[key] = { + old: oldData[key], // Could be undefined if key didn’t exist in oldData + new: newData[key] + }; changedFieldNames.push(key); } } @@ -44,22 +49,23 @@ const eventParser = async ({ oldData, newData, trigger, table, jobIdField }) => // Check for fields that were removed for (const key in oldData) { if (Object.prototype.hasOwnProperty.call(oldData, key) && !Object.prototype.hasOwnProperty.call(newData, key)) { - changedFields[key] = null; // Indicate field was removed + changedFields[key] = { + old: oldData[key], + new: null // Indicate field was removed + }; changedFieldNames.push(key); } } } + // Extract jobId based on jobIdField let jobId = null; if (jobIdField) { - // If the jobIdField is provided as a string like "req.body.event.new.jobid", - // strip the prefix if it exists so we can use the property name. let keyName = jobIdField; const prefix = "req.body.event.new."; if (keyName.startsWith(prefix)) { keyName = keyName.slice(prefix.length); } - // Attempt to retrieve the job id from newData first; if not available, try oldData. jobId = newData[keyName] || (oldData && oldData[keyName]) || null; } diff --git a/server/notifications/scenarioBuilders.js b/server/notifications/scenarioBuilders.js index f35f178b4..bf4d18f6a 100644 --- a/server/notifications/scenarioBuilders.js +++ b/server/notifications/scenarioBuilders.js @@ -1,24 +1,115 @@ -const consoleDir = require("../utils/consoleDir"); const { getJobAssignmentType } = require("./stringHelpers"); +// Helper function to populate watchers for app, fcm, and email channels +const populateWatchers = (data, result) => { + data.scenarioWatchers.forEach((recipients) => { + const { user, app, fcm, email } = recipients; + if (app === true) result.app.recipients.push({ user, bodyShopId: data.bodyShopId }); + if (fcm === true) result.fcm.recipients.push(user); + if (email === true) result.email.recipients.push({ user }); + }); +}; + const alternateTransportChangedBuilder = (data) => { - consoleDir(data); + const result = { + jobId: data.jobId, + bodyShopName: data.bodyShopName, + app: { + key: "notifications.job.alternateTransportChanged", + variables: { + alternateTransport: data.data.alt_transport, + oldAlternateTransport: data.changedFields.alt_transport?.old + }, + recipients: [] + }, + email: { + subject: `Alternate transport for ${data?.jobRoNumber} (${data.bodyShopName}) changed to ${data.data.alt_transport || "None"}`, + body: `The alternate transport status has been updated for job ${data?.jobRoNumber} in ${data.bodyShopName}.`, + recipients: [] + }, + fcm: { recipients: [] } + }; + + populateWatchers(data, result); + return result; }; const billPostedHandler = (data) => { - consoleDir(data); + const result = { + jobId: data.jobId, + bodyShopName: data.bodyShopName, + app: { + key: "notifications.job.billPosted", + variables: { + clmTotal: data.data.clm_total + }, + recipients: [] + }, + email: { + subject: `Bill posted for ${data?.jobRoNumber} (${data.bodyShopName})`, + body: `A bill of $${data.data.clm_total} has been posted for job ${data?.jobRoNumber} in ${data.bodyShopName}.`, + recipients: [] + }, + fcm: { recipients: [] } + }; + + populateWatchers(data, result); + return result; }; const criticalPartsStatusChangedBuilder = (data) => { - consoleDir(data); + const result = { + jobId: data.jobId, + bodyShopName: data.bodyShopName, + app: { + key: "notifications.job.criticalPartsStatusChanged", + variables: { + queuedForParts: data.data.queued_for_parts, + oldQueuedForParts: data.changedFields.queued_for_parts?.old + }, + recipients: [] + }, + email: { + subject: `Critical parts status for ${data?.jobRoNumber} (${data.bodyShopName}) updated`, + body: `The critical parts status for job ${data?.jobRoNumber} in ${data.bodyShopName} has changed to ${data.data.queued_for_parts ? "queued" : "not queued"}.`, + recipients: [] + }, + fcm: { recipients: [] } + }; + + populateWatchers(data, result); + return result; }; const intakeDeliveryChecklistCompletedBuilder = (data) => { - consoleDir(data); + const checklistType = data.changedFields.intakechecklist ? "intake" : "delivery"; + const result = { + jobId: data.jobId, + bodyShopName: data.bodyShopName, + app: { + key: "notifications.job.checklistCompleted", + variables: { + checklistType, + completed: true + }, + recipients: [] + }, + email: { + subject: `${checklistType.charAt(0).toUpperCase() + checklistType.slice(1)} checklist completed for ${data?.jobRoNumber} (${data.bodyShopName})`, + body: `The ${checklistType} checklist for job ${data?.jobRoNumber} in ${data.bodyShopName} has been completed.`, + recipients: [] + }, + fcm: { recipients: [] } + }; + + populateWatchers(data, result); + return result; }; const jobAssignedToMeBuilder = (data) => { - return { + const result = { + jobId: data.jobId, + bodyShopName: data.bodyShopName, app: { key: "notifications.job.assigned", variables: { @@ -26,55 +117,252 @@ const jobAssignedToMeBuilder = (data) => { jobId: data.jobId, bodyShopName: data.bodyShopName }, - recipients: data.scenarioWatchers.map((watcher) => ({ email: watcher.user, employeeId: watcher.employeeId })) + recipients: [] }, email: { subject: `You have been assigned to [${getJobAssignmentType(data.scenarioFields?.[0])}] on ${data?.jobRoNumber} in ${data.bodyShopName}`, body: `Hello, a new job has been assigned to you in ${data.bodyShopName}.`, - recipient: data.scenarioWatchers.map((watcher) => watcher.user) + recipients: [] }, - fcm: {} + fcm: { recipients: [] } }; + + populateWatchers(data, result); + return result; }; const jobsAddedToProductionBuilder = (data) => { - consoleDir(data); + const result = { + jobId: data.jobId, + bodyShopName: data.bodyShopName, + app: { + key: "notifications.job.addedToProduction", + variables: { + inProduction: data.data.inproduction, + oldInProduction: data.changedFields.inproduction?.old + }, + recipients: [] + }, + email: { + subject: `Job ${data?.jobRoNumber} (${data.bodyShopName}) added to production`, + body: `Job ${data?.jobRoNumber} in ${data.bodyShopName} has been added to production.`, + recipients: [] + }, + fcm: { recipients: [] } + }; + + populateWatchers(data, result); + return result; }; +// Verified const jobStatusChangeBuilder = (data) => { - consoleDir(data); + const result = { + jobId: data.jobId, + bodyShopName: data.bodyShopName, + app: { + key: "notifications.job.statusChanged", + variables: { + status: data.data.status, + oldStatus: data.changedFields.status.old + }, + recipients: [] + }, + email: { + subject: `The status of ${data?.jobRoNumber} (${data.bodyShopName}) has changed from ${data.changedFields.status.old} to ${data.data.status}`, + body: `...`, + recipients: [] + }, + fcm: { recipients: [] } + }; + + populateWatchers(data, result); + return result; }; const newMediaAddedReassignedBuilder = (data) => { - consoleDir(data); + const result = { + jobId: data.jobId, + bodyShopName: data.bodyShopName, + app: { + key: "notifications.job.newMediaAdded", + variables: {}, + recipients: [] + }, + email: { + subject: `New media added to ${data?.jobRoNumber} (${data.bodyShopName})`, + body: `New media has been added to job ${data?.jobRoNumber} in ${data.bodyShopName}.`, + recipients: [] + }, + fcm: { recipients: [] } + }; + + populateWatchers(data, result); + return result; }; +// Verified const newNoteAddedBuilder = (data) => { - consoleDir(data); + const result = { + jobId: data.jobId, + bodyShopName: data.bodyShopName, + app: { + key: "notifications.job.newNoteAdded", + variables: { + text: data.data.text + }, + recipients: [] + }, + email: { + subject: `New note added to ${data?.jobRoNumber} (${data.bodyShopName})`, + body: `A new note has been added to job ${data?.jobRoNumber} in ${data.bodyShopName}: "${data.data.text}"`, + recipients: [] + }, + fcm: { recipients: [] } + }; + + populateWatchers(data, result); + return result; }; const newTimeTicketPostedBuilder = (data) => { - consoleDir(data); + const result = { + jobId: data.jobId, + bodyShopName: data.bodyShopName, + app: { + key: "notifications.job.newTimeTicketPosted", + variables: {}, + recipients: [] + }, + email: { + subject: `New time ticket posted for ${data?.jobRoNumber} (${data.bodyShopName})`, + body: `A new time ticket has been posted for job ${data?.jobRoNumber} in ${data.bodyShopName}.`, + recipients: [] + }, + fcm: { recipients: [] } + }; + + populateWatchers(data, result); + return result; }; const partMarkedBackOrderedBuilder = (data) => { - consoleDir(data); + const result = { + jobId: data.jobId, + bodyShopName: data.bodyShopName, + app: { + key: "notifications.job.partBackOrdered", + variables: { + queuedForParts: data.data.queued_for_parts, + oldQueuedForParts: data.changedFields.queued_for_parts?.old + }, + recipients: [] + }, + email: { + subject: `Part marked back-ordered for ${data?.jobRoNumber} (${data.bodyShopName})`, + body: `A part for job ${data?.jobRoNumber} in ${data.bodyShopName} has been marked as back-ordered.`, + recipients: [] + }, + fcm: { recipients: [] } + }; + + populateWatchers(data, result); + return result; }; const paymentCollectedCompletedBuilder = (data) => { - consoleDir(data); + const result = { + jobId: data.jobId, + bodyShopName: data.bodyShopName, + app: { + key: "notifications.job.paymentCollected", + variables: { + clmTotal: data.data.clm_total + }, + recipients: [] + }, + email: { + subject: `Payment collected for ${data?.jobRoNumber} (${data.bodyShopName})`, + body: `Payment of $${data.data.clm_total} has been collected for job ${data?.jobRoNumber} in ${data.bodyShopName}.`, + recipients: [] + }, + fcm: { recipients: [] } + }; + + populateWatchers(data, result); + return result; }; const scheduledDatesChangedBuilder = (data) => { - consoleDir(data); + const result = { + jobId: data.jobId, + bodyShopName: data.bodyShopName, + app: { + key: "notifications.job.scheduledDatesChanged", + variables: { + scheduledIn: data.data.scheduled_in, + oldScheduledIn: data.changedFields.scheduled_in?.old, + scheduledCompletion: data.data.scheduled_completion, + oldScheduledCompletion: data.changedFields.scheduled_completion?.old, + scheduledDelivery: data.data.scheduled_delivery, + oldScheduledDelivery: data.changedFields.scheduled_delivery?.old + }, + recipients: [] + }, + email: { + subject: `Scheduled dates updated for ${data?.jobRoNumber} (${data.bodyShopName})`, + body: `Scheduled dates for job ${data?.jobRoNumber} in ${data.bodyShopName} have been updated.`, + recipients: [] + }, + fcm: { recipients: [] } + }; + + populateWatchers(data, result); + return result; }; const supplementImportedBuilder = (data) => { - consoleDir(data); + const result = { + jobId: data.jobId, + bodyShopName: data.bodyShopName, + app: { + key: "notifications.job.supplementImported", + variables: { + suppAmt: data.data.cieca_ttl?.data?.supp_amt + }, + recipients: [] + }, + email: { + subject: `Supplement imported for ${data?.jobRoNumber} (${data.bodyShopName})`, + body: `A supplement of $${data.data.cieca_ttl?.data?.supp_amt || 0} has been imported for job ${data?.jobRoNumber} in ${data.bodyShopName}.`, + recipients: [] + }, + fcm: { recipients: [] } + }; + + populateWatchers(data, result); + return result; }; -const tasksUpdatedCreatedBuilder = async (data) => { - consoleDir(data); +const tasksUpdatedCreatedBuilder = (data) => { + const result = { + jobId: data.jobId, + bodyShopName: data.bodyShopName, + app: { + key: "notifications.job.tasksUpdated", + variables: {}, + recipients: [] + }, + email: { + subject: `Tasks ${data.isNew ? "created" : "updated"} for ${data?.jobRoNumber} (${data.bodyShopName})`, + body: `Tasks for job ${data?.jobRoNumber} in ${data.bodyShopName} have been ${data.isNew ? "created" : "updated"}.`, + recipients: [] + }, + fcm: { recipients: [] } + }; + + populateWatchers(data, result); + return result; }; module.exports = { diff --git a/server/notifications/scenarioParser.js b/server/notifications/scenarioParser.js index ba269d872..7ed7eb0ea 100644 --- a/server/notifications/scenarioParser.js +++ b/server/notifications/scenarioParser.js @@ -10,6 +10,7 @@ const queries = require("../graphql-client/queries"); const { isEmpty, isFunction } = require("lodash"); const { getMatchingScenarios } = require("./scenarioMapperr"); const emailQueue = require("./queues/emailQueue"); +const consoleDir = require("../utils/consoleDir"); /** * Parses an event and determines matching scenarios for notifications. @@ -209,11 +210,12 @@ const scenarioParser = async (req, jobIdField) => { // Step 9: Dispatch Email Notifications to the Email Notification Queue // console.log(`8`); - const emailsToDispatch = scenariosToDispatch.map((scenario) => scenario.email); + const emailsToDispatch = scenariosToDispatch.map((scenario) => scenario?.email); // Step 10: Dispatch App Notifications to the App Notification Queue - const appsToDispatch = scenariosToDispatch.map((scenario) => scenario.app); + const appsToDispatch = scenariosToDispatch.map((scenario) => scenario?.app); + consoleDir({ emailsToDispatch, appsToDispatch }); // TODO: Test Code for Queues // emailQueue().add("test", { data: "test" }); };