const { getJobAssignmentType, formatTaskPriority } = require("./stringHelpers"); const moment = require("moment-timezone"); const { startCase } = require("lodash"); /** * Populates the recipients for app, email, and FCM notifications based on scenario watchers. * * @param {Object} data - The data object containing scenarioWatchers and bodyShopId. * @param {Object} result - The result object to populate with recipients for app, email, and FCM notifications. */ const populateWatchers = (data, result) => { data.scenarioWatchers.forEach((recipients) => { const { user, app, fcm, email, firstName, lastName, employeeId, associationId } = recipients; if (app === true) result.app.recipients.push({ user, bodyShopId: data.bodyShopId, employeeId, associationId }); if (fcm === true) result.fcm.recipients.push(user); if (email === true) result.email.recipients.push({ user, firstName, lastName }); }); }; /** * Builds notification data for changes to alternate transport. */ // Verified const alternateTransportChangedBuilder = (data) => { const body = `The Alternate Transport status has been updated from ${data.changedFields.alt_transport?.old || "unset"} to ${data?.changedFields?.alt_transport?.new || "unset"}.`; const result = { app: { jobId: data.jobId, bodyShopId: data.bodyShopId, jobRoNumber: data.jobRoNumber, key: "notifications.job.alternateTransportChanged", body, variables: { alternateTransport: data?.changedFields?.alt_transport?.new, oldAlternateTransport: data?.changedFields?.alt_transport?.old }, recipients: [] }, email: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopName: data.bodyShopName, body, recipients: [] }, fcm: { recipients: [] } }; populateWatchers(data, result); return result; }; /** * Builds notification data for bill posted events. */ //verified const billPostedHandler = (data) => { const facing = data?.data?.isinhouse ? "In-House" : "External"; const body = `An ${facing} Bill has been posted${data?.data?.is_credit_memo ? " (Credit Memo)" : ""}.`.trim(); const result = { app: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopId: data.bodyShopId, key: "notifications.job.billPosted", body, variables: { isInHouse: data?.data?.isinhouse, isCreditMemo: data?.data?.is_credit_memo }, recipients: [] }, email: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopName: data.bodyShopName, body, recipients: [] }, fcm: { recipients: [] } }; populateWatchers(data, result); return result; }; /** * Builds notification data for changes to critical parts status. */ // TODO: Needs change const criticalPartsStatusChangedBuilder = (data) => { const body = `The critical parts status has changed to ${data?.data?.queued_for_parts ? "queued" : "not queued"}.`; const result = { app: { jobId: data.jobId, bodyShopId: data.bodyShopId, jobRoNumber: data.jobRoNumber, key: "notifications.job.criticalPartsStatusChanged", body, variables: { queuedForParts: data?.data?.queued_for_parts, oldQueuedForParts: data?.changedFields?.queued_for_parts?.old }, recipients: [] }, email: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopName: data.bodyShopName, body, recipients: [] }, fcm: { recipients: [] } }; populateWatchers(data, result); return result; }; /** * Builds notification data for completed intake or delivery checklists. */ // Verified const intakeDeliveryChecklistCompletedBuilder = (data) => { const checklistType = data?.changedFields?.intakechecklist ? "Intake" : "Delivery"; const body = `The ${checklistType.charAt(0).toUpperCase() + checklistType.slice(1)} checklist has been completed.`; const result = { app: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopId: data.bodyShopId, key: "notifications.job.checklistCompleted", body, variables: { checklistType, completed: true }, recipients: [] }, email: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopName: data.bodyShopName, body, recipients: [] }, fcm: { recipients: [] } }; populateWatchers(data, result); return result; }; /** * Builds notification data for job assignment events. */ // Verified const jobAssignedToMeBuilder = (data) => { const body = `You have been assigned to ${getJobAssignmentType(data.scenarioFields?.[0])}.`; const result = { app: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopId: data.bodyShopId, key: "notifications.job.assigned", body, variables: { type: data.scenarioFields?.[0] }, recipients: [] }, email: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopName: data.bodyShopName, body, recipients: [] }, fcm: { recipients: [] } }; populateWatchers(data, result); return result; }; /** * Builds notification data for jobs added to production. */ // Verified const jobsAddedToProductionBuilder = (data) => { const body = `Has been added to Production.`; const result = { app: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopId: data.bodyShopId, key: "notifications.job.addedToProduction", body, variables: {}, recipients: [] }, email: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopName: data.bodyShopName, body, recipients: [] }, fcm: { recipients: [] } }; populateWatchers(data, result); return result; }; /** * Builds notification data for job status changes. */ // Verified const jobStatusChangeBuilder = (data) => { const body = `The status has changed from ${data?.changedFields?.status?.old || "unset"} to ${data?.changedFields?.status?.new || "unset"}`; const result = { app: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopId: data.bodyShopId, key: "notifications.job.statusChanged", body, variables: { status: data.changedFields.status.new, oldStatus: data.changedFields.status.old }, recipients: [] }, email: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopName: data.bodyShopName, body, recipients: [] }, fcm: { recipients: [] } }; populateWatchers(data, result); return result; }; /** * Builds notification data for new media added or reassigned events. */ // Verified const newMediaAddedReassignedBuilder = (data) => { // Determine if it's an image or document const mediaType = data?.data?.type?.startsWith("image") ? "Image" : "Document"; // Determine if it's added or updated const action = data.isNew ? "added" : "updated"; // Construct the body string const body = `An ${mediaType} has been ${action}.`; const result = { app: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopId: data.bodyShopId, key: "notifications.job.newMediaAdded", body, variables: { mediaType, action }, recipients: [] }, email: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopName: data.bodyShopName, body, recipients: [] }, fcm: { recipients: [] } }; populateWatchers(data, result); return result; }; /** * Builds notification data for new notes added to a job. */ // verified const newNoteAddedBuilder = (data) => { const body = [ "A", data?.data?.critical && "critical", data?.data?.private && "private", data?.data?.type, "Note has been added by", `${data.data.created_by}` ] .filter(Boolean) .join(" "); const result = { app: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopId: data.bodyShopId, key: "notifications.job.newNoteAdded", body, variables: { createdBy: data?.data?.created_by, critical: data?.data?.critical, type: data?.data?.type, private: data?.data?.private }, recipients: [] }, email: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopName: data.bodyShopName, body, recipients: [] }, fcm: { recipients: [] } }; populateWatchers(data, result); return result; }; /** * Builds notification data for new time tickets posted. */ // Verified const newTimeTicketPostedBuilder = (data) => { const type = data?.data?.cost_center; const body = `A ${startCase(type.toLowerCase())} Time Ticket for ${data?.data?.date} has been posted${data?.data?.flat_rate ? " (Flat Rate)" : ""}.`.trim(); const result = { app: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopId: data.bodyShopId, key: "notifications.job.newTimeTicketPosted", body, variables: { type, date: data?.data?.date, flatRate: data?.data?.flat_rate }, recipients: [] }, email: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopName: data.bodyShopName, body, recipients: [] }, fcm: { recipients: [] } }; populateWatchers(data, result); return result; }; /** * Builds notification data for parts marked as back-ordered. */ const partMarkedBackOrderedBuilder = (data) => { const body = `A part has been marked as back-ordered.`; const result = { app: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopId: data.bodyShopId, key: "notifications.job.partBackOrdered", body, variables: {}, recipients: [] }, email: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopName: data.bodyShopName, body, recipients: [] }, fcm: { recipients: [] } }; populateWatchers(data, result); return result; }; /** * Builds notification data for payment collection events. */ const paymentCollectedCompletedBuilder = (data) => { const body = `Payment of $${data.data.clm_total} has been collected.`; const result = { app: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopId: data.bodyShopId, key: "notifications.job.paymentCollected", body, variables: { clmTotal: data.data.clm_total }, recipients: [] }, email: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopName: data.bodyShopName, body, recipients: [] }, fcm: { recipients: [] } }; populateWatchers(data, result); return result; }; /** * Builds notification data for changes to scheduled dates. */ const scheduledDatesChangedBuilder = (data) => { const momentFormat = "MM/DD/YYYY hh:mm a"; const changedFields = data.changedFields; // Define field configurations const fieldConfigs = { scheduled_in: "Scheduled In", scheduled_completion: "Scheduled Completion", scheduled_delivery: "Scheduled Delivery" }; // Build field messages dynamically const fieldMessages = Object.entries(fieldConfigs) .filter(([field]) => changedFields[field]) // Only include changed fields .map(([field, label]) => { const { old, new: newValue } = changedFields[field]; const formatDate = (date) => (date ? moment(date).tz(data.bodyShopTimezone).format(momentFormat) : "unset"); return `${label} changed from ${formatDate(old)} to ${formatDate(newValue)}`; }); const body = fieldMessages.length > 0 ? fieldMessages.join(", ") + "." : "Scheduled dates have been updated."; const result = { app: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopId: data.bodyShopId, key: "notifications.job.scheduledDatesChanged", body, variables: { scheduledIn: changedFields.scheduled_in?.new, oldScheduledIn: changedFields.scheduled_in?.old, scheduledCompletion: changedFields.scheduled_completion?.new, oldScheduledCompletion: changedFields.scheduled_completion?.old, scheduledDelivery: changedFields.scheduled_delivery?.new, oldScheduledDelivery: changedFields.scheduled_delivery?.old }, recipients: [] }, email: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopName: data.bodyShopName, body, recipients: [] }, fcm: { recipients: [] } }; populateWatchers(data, result); return result; }; /** * Builds notification data for supplement imported events. */ const supplementImportedBuilder = (data) => { const body = `A supplement of $${data.data.cieca_ttl?.data?.supp_amt || 0} has been imported.`; const result = { app: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopId: data.bodyShopId, key: "notifications.job.supplementImported", body, variables: { suppAmt: data.data.cieca_ttl?.data?.supp_amt }, recipients: [] }, email: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopName: data.bodyShopName, body, recipients: [] }, fcm: { recipients: [] } }; populateWatchers(data, result); return result; }; /** * Builds notification data for tasks updated or created. */ const tasksUpdatedCreatedBuilder = (data) => { const momentFormat = "MM/DD/YYYY hh:mm a"; const timezone = data.bodyShopTimezone; const taskTitle = data?.data?.title; let body; let variables; if (data.isNew) { // Created case const priority = formatTaskPriority(data?.data?.priority); const createdBy = data?.data?.created_by; const dueDate = data.data.due_date ? ` due on ${moment(data.data.due_date).tz(timezone).format(momentFormat)}` : ""; const completedOnCreation = data.data.completed === true; body = `A ${priority} Task ${taskTitle} has been created${completedOnCreation ? " and marked completed" : ""} by ${createdBy}${dueDate}`; variables = { isNew: data.isNew, roNumber: data.jobRoNumber, title: data?.data?.title, priority: data?.data?.priority, createdBy: data?.data?.created_by, dueDate: data?.data?.due_date, completed: completedOnCreation ? data?.data?.completed : undefined // Only include if true }; } else { // Updated case const changedFields = data.changedFields; const fieldNames = Object.keys(changedFields); // Special case: Only 'completed' changed if (fieldNames.length === 1 && changedFields.completed) { body = `Task ${taskTitle} was marked ${changedFields.completed.new ? "complete" : "incomplete"}`; variables = { isNew: data.isNew, roNumber: data.jobRoNumber, title: data?.data?.title, changedCompleted: data?.changedFields?.completed?.new }; } else { // General update case const fieldMessages = []; if (changedFields.description) { fieldMessages.push("Description Updated"); } if (changedFields.priority) { fieldMessages.push(`Priority changed to ${formatTaskPriority(changedFields.priority.new)}`); } if (changedFields.due_date) { fieldMessages.push(`Due date set to ${moment(changedFields.due_date.new).tz(timezone).format(momentFormat)}`); } if (changedFields.completed) { fieldMessages.push(`Status changed to ${changedFields.completed.new ? "complete" : "incomplete"}`); } body = fieldMessages.length > 0 ? `Task ${taskTitle} updated: ${fieldMessages.join(", ")}` : `Task ${taskTitle} has been updated.`; variables = { isNew: data.isNew, roNumber: data.jobRoNumber, title: data?.data?.title, changedPriority: data?.changedFields?.priority?.new, changedDueDate: data?.changedFields?.due_date?.new, changedCompleted: data?.changedFields?.completed?.new }; } } const result = { app: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopId: data.bodyShopId, key: data.isNew ? "notifications.job.taskCreated" : "notifications.job.taskUpdated", body, variables, recipients: [] }, email: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopName: data.bodyShopName, body, recipients: [] }, fcm: { recipients: [] } }; populateWatchers(data, result); return result; }; module.exports = { alternateTransportChangedBuilder, billPostedHandler, criticalPartsStatusChangedBuilder, intakeDeliveryChecklistCompletedBuilder, jobAssignedToMeBuilder, jobsAddedToProductionBuilder, jobStatusChangeBuilder, newMediaAddedReassignedBuilder, newNoteAddedBuilder, newTimeTicketPostedBuilder, partMarkedBackOrderedBuilder, paymentCollectedCompletedBuilder, scheduledDatesChangedBuilder, supplementImportedBuilder, tasksUpdatedCreatedBuilder };