const { getJobAssignmentType, formatTaskPriority } = require("./stringHelpers"); const moment = require("moment-timezone"); const { startCase } = require("lodash"); const Dinero = require("dinero.js"); Dinero.globalRoundingMode = "HALF_EVEN"; /** * Creates a standard notification object with app, email, and FCM properties and populates recipients. * @param {Object} data - Input data containing jobId, jobRoNumber, bodyShopId, bodyShopName, and scenarioWatchers * @param {string} key - Notification key for the app * @param {string} body - Notification body text * @param {Object} [variables={}] - Variables for the app notification * @returns {Object} Notification object with populated recipients */ const buildNotification = (data, key, body, variables = {}) => { const result = { app: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopId: data.bodyShopId, key, body, variables, recipients: [] }, email: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopName: data.bodyShopName, bodyShopTimezone: data.bodyShopTimezone, body, recipients: [] }, fcm: { recipients: [] } }; // Populate recipients from scenarioWatchers 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 }); }); return result; }; /** * Creates a notification for when the alternate transport is changed. * @param data * @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}} */ const alternateTransportChangedBuilder = (data) => { const oldTransport = data?.changedFields?.alt_transport?.old; const newTransport = data?.changedFields?.alt_transport?.new; let body; if (oldTransport && newTransport) body = `The alternate transportation has been changed from ${oldTransport} to ${newTransport}.`; else if (!oldTransport && newTransport) body = `The alternate transportation has been set to ${newTransport}.`; else if (oldTransport && !newTransport) body = `The alternate transportation has been canceled (previously ${oldTransport}).`; else body = `The alternate transportation has been updated.`; return buildNotification(data, "notifications.job.alternateTransportChanged", body, { alternateTransport: newTransport, oldAlternateTransport: oldTransport }); }; /** * Creates a notification for when a bill is posted. * @param data * @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}} */ const billPostedBuilder = (data) => { const facing = data?.data?.isinhouse ? "in-house" : "vendor"; const body = `An ${facing} ${data?.data?.is_credit_memo ? "credit memo" : "bill"} has been posted.`.trim(); return buildNotification(data, "notifications.job.billPosted", body, { isInHouse: data?.data?.isinhouse, isCreditMemo: data?.data?.is_credit_memo }); }; /** * Creates a notification for when the status of critical parts changes. * @param data * @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}} */ const criticalPartsStatusChangedBuilder = (data) => { const lineDesc = data?.data?.line_desc; const status = data?.data?.status; const body = status ? `The status on a critical part line (${lineDesc}) has been set to ${status}.` : `The status on a critical part line (${lineDesc}) has been cleared.`; return buildNotification(data, "notifications.job.criticalPartsStatusChanged", body, { joblineId: data?.data?.id, status: data?.data?.status, line_desc: lineDesc }); }; /** * Creates a notification for when the intake or delivery checklist is completed. * @param data * @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}} */ const intakeDeliveryChecklistCompletedBuilder = (data) => { const checklistType = data?.changedFields?.intakechecklist ? "intake" : "delivery"; const body = `The ${checklistType.charAt(0).toUpperCase() + checklistType.slice(1)} checklist has been completed.`; return buildNotification(data, "notifications.job.checklistCompleted", body, { checklistType, completed: true }); }; /** * Creates a notification for when a job is assigned to the user. * @param data * @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}} */ const jobAssignedToMeBuilder = (data) => { const body = `You have been assigned to ${getJobAssignmentType(data.scenarioFields?.[0])}.`; return buildNotification(data, "notifications.job.assigned", body, { type: data.scenarioFields?.[0] }); }; /** * Creates a notification for when jobs are added to production. * @param data * @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}} */ const jobsAddedToProductionBuilder = (data) => { const body = `Job is now in production.`; return buildNotification(data, "notifications.job.addedToProduction", body); }; /** * Creates a notification for when the job status changes. * @param data * @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}} */ const jobStatusChangeBuilder = (data) => { const oldStatus = data?.changedFields?.status?.old; const newStatus = data?.changedFields?.status?.new; let body; if (oldStatus && newStatus) body = `The status has been changed from ${oldStatus} to ${newStatus}.`; else if (!oldStatus && newStatus) body = `The status has been set to ${newStatus}.`; else if (oldStatus && !newStatus) body = `The status has been cleared (previously ${oldStatus}).`; else body = `The status has been updated.`; return buildNotification(data, "notifications.job.statusChanged", body, { status: newStatus, oldStatus: oldStatus }); }; /** * Creates a notification for when new media is added or reassigned. * @param data * @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}} */ const newMediaAddedReassignedBuilder = (data) => { const mediaType = data?.data?.type?.startsWith("image") ? "Image" : "Document"; const action = data?.data?._documentMoved ? "moved to another job" : data.isNew ? "added" : data.changedFields?.jobid && data.changedFields.jobid.old !== data.changedFields.jobid.new ? "moved to this job" : "updated"; const body = `A ${mediaType} has been ${action}.`; return buildNotification(data, "notifications.job.newMediaAdded", body, { mediaType, action, movedToJob: data?.data?._movedToJob }); }; /** * Creates a notification for when a new note is added. * @param data * @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}} */ 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(" "); return buildNotification(data, "notifications.job.newNoteAdded", body, { createdBy: data?.data?.created_by, critical: data?.data?.critical, type: data?.data?.type, private: data?.data?.private }); }; /** * Creates a notification for when a new time ticket is posted. * @param data * @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}} */ const newTimeTicketPostedBuilder = (data) => { const type = data?.data?.cost_center; const body = `A ${startCase(type.toLowerCase())} time ticket for ${data?.data?.date} has been posted.`.trim(); return buildNotification(data, "notifications.job.newTimeTicketPosted", body, { type, date: data?.data?.date }); }; /** * Creates a notification for when a part is marked as back-ordered. * @param data * @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}} */ const partMarkedBackOrderedBuilder = (data) => { const body = `A part ${data?.data?.line_desc} has been marked as back-ordered.`; return buildNotification(data, "notifications.job.partBackOrdered", body, { line_desc: data?.data?.line_desc }); }; /** * Creates a notification for when payment is collected or completed. * @param data * @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}} */ const paymentCollectedCompletedBuilder = (data) => { const momentFormat = "MM/DD/YYYY"; const amountDinero = Dinero({ amount: Math.round((data.data.amount || 0) * 100) }); const amountFormatted = amountDinero.toFormat(); const payer = data.data.payer; const paymentType = data.data.type; const paymentDate = moment(data.data.date).format(momentFormat); const body = `Payment of ${amountFormatted} has been collected from ${payer} via ${paymentType} on ${paymentDate}`; return buildNotification(data, "notifications.job.paymentCollected", body, { amount: data.data.amount, payer: data.data.payer, type: data.data.type, date: data.data.date }); }; /** * Creates a notification for when scheduled dates are changed. * @param data * @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}} */ const scheduledDatesChangedBuilder = (data) => { const changedFields = data.changedFields; const fieldConfigs = { scheduled_in: "Scheduled In", scheduled_completion: "Scheduled Completion", scheduled_delivery: "Scheduled Delivery" }; const formatDateTime = (date) => { if (!date) return "(no date set)"; const formatted = moment(date).tz(data.bodyShopTimezone); return `${formatted.format("MM/DD/YYYY")} at ${formatted.format("hh:mm a")}`; }; const fieldMessages = Object.entries(fieldConfigs) .filter(([field]) => changedFields[field]) .map(([field, label]) => { const { old, new: newValue } = changedFields[field]; if (old && !newValue) return `${label} was cancelled (previously ${formatDateTime(old)}).`; else if (!old && newValue) return `${label} was set to ${formatDateTime(newValue)}.`; else if (old && newValue) return `${label} changed from ${formatDateTime(old)} to ${formatDateTime(newValue)}.`; return ""; }) .filter(Boolean); const body = fieldMessages.length > 0 ? fieldMessages.join(" ") : "Scheduled dates have been updated."; return buildNotification(data, "notifications.job.scheduledDatesChanged", body, { 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 }); }; /** * Creates a notification for when tasks are updated or created. * @param data * @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}} */ const tasksUpdatedCreatedBuilder = (data) => { const momentFormat = "MM/DD/YYYY hh:mm a"; const timezone = data.bodyShopTimezone; const taskTitle = data?.data?.title ? `"${data.data.title}"` : "Unnamed Task"; let body, variables; if (data.isNew) { const priority = formatTaskPriority(data?.data?.priority); const createdBy = data?.data?.created_by || "Unknown"; 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 }; } else { const changedFields = data.changedFields; const fieldNames = Object.keys(changedFields); const oldTitle = changedFields.title ? `"${changedFields.title.old || "Unnamed Task"}"` : taskTitle; if (fieldNames.length === 1 && changedFields.completed) { body = `Task ${oldTitle} was marked ${changedFields.completed.new ? "complete" : "incomplete"}`; variables = { isNew: data.isNew, roNumber: data.jobRoNumber, title: data?.data?.title, changedCompleted: changedFields.completed.new }; } else { const fieldMessages = []; if (changedFields.title) fieldMessages.push(`Task ${oldTitle} changed title to "${changedFields.title.new || "unnamed task"}".`); 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 ? fieldMessages.length === 1 && changedFields.title ? fieldMessages[0] : `Task ${oldTitle} updated: ${fieldMessages.join(", ")}` : `Task ${oldTitle} has been updated.`; variables = { isNew: data.isNew, roNumber: data.jobRoNumber, title: data?.data?.title, changedTitleOld: changedFields.title?.old, changedTitleNew: changedFields.title?.new, changedPriority: changedFields.priority?.new, changedDueDate: changedFields.due_date?.new, changedCompleted: changedFields.completed?.new }; } } return buildNotification( data, data.isNew ? "notifications.job.taskCreated" : "notifications.job.taskUpdated", body, variables ); }; /** * Creates a notification for when a supplement is imported. * @param data * @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}} */ const supplementImportedBuilder = (data) => { const body = `A supplement has been imported.`; return buildNotification(data, "notifications.job.supplementImported", body); }; module.exports = { alternateTransportChangedBuilder, billPostedBuilder, criticalPartsStatusChangedBuilder, intakeDeliveryChecklistCompletedBuilder, jobAssignedToMeBuilder, jobsAddedToProductionBuilder, jobStatusChangeBuilder, newMediaAddedReassignedBuilder, newNoteAddedBuilder, newTimeTicketPostedBuilder, partMarkedBackOrderedBuilder, paymentCollectedCompletedBuilder, scheduledDatesChangedBuilder, supplementImportedBuilder, tasksUpdatedCreatedBuilder };