const { getJobAssignmentType, formatTaskPriority } = require("./stringHelpers"); const moment = require("moment-timezone"); const { startCase } = require("lodash"); const Dinero = require("dinero.js"); Dinero.globalRoundingMode = "HALF_EVEN"; /** * 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. */ const alternateTransportChangedBuilder = (data) => { const body = `The alternate transportation has been changed 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. */ const billPostedHandler = (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(); 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. */ // const criticalPartsStatusChangedBuilder = (data) => { const body = `The status on a critical part line (${data?.data?.line_desc}) has changed to ${data?.data?.status || "unset"}.`; const result = { app: { jobId: data.jobId, bodyShopId: data.bodyShopId, jobRoNumber: data.jobRoNumber, key: "notifications.job.criticalPartsStatusChanged", body, variables: { joblineId: data?.data?.id, // If we want to deeplink to the jobline status: data?.data?.status, line_desc: data?.data?.line_desc }, 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. */ 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. */ 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. */ const jobsAddedToProductionBuilder = (data) => { const body = `Job is now in 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. */ 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. */ 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. */ 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. */ 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(); const result = { app: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopId: data.bodyShopId, key: "notifications.job.newTimeTicketPosted", body, variables: { type, date: data?.data?.date }, 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 ${data?.data?.line_desc} has been marked as back-ordered.`; const result = { app: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopId: data.bodyShopId, key: "notifications.job.partBackOrdered", body, variables: { line_desc: data?.data?.line_desc }, 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 momentFormat = "MM/DD/YYYY"; const timezone = data.bodyShopTimezone; // Format amount using Dinero.js const amountDinero = Dinero({ amount: Math.round((data.data.amount || 0) * 100) // Convert to cents, default to 0 if missing }); const amountFormatted = amountDinero.toFormat(); const payer = data.data.payer; const paymentType = data.data.type; const paymentDate = moment(data.data.date).tz(timezone).format(momentFormat); const body = `Payment of ${amountFormatted} has been collected from ${payer} via ${paymentType} on ${paymentDate}`; const result = { app: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopId: data.bodyShopId, key: "notifications.job.paymentCollected", body, variables: { amount: data.data.amount, payer: data.data.payer, type: data.data.type, date: data.data.date }, 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 tasks updated or created. */ 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; let variables; if (data.isNew) { // Created case const priority = formatTaskPriority(data?.data?.priority); const createdBy = data?.data?.created_by || "Unknown"; // Fallback for undefined 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); const oldTitle = changedFields.title ? `"${changedFields.title.old || "Unnamed Task"}"` : taskTitle; // Special case: Only 'completed' changed 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: data?.changedFields?.completed?.new }; } else { // General update case 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] // If only title changed, use it standalone : `Task ${oldTitle} updated: ${fieldMessages.join(", ")}` : `Task ${oldTitle} has been updated.`; variables = { isNew: data.isNew, roNumber: data.jobRoNumber, title: data?.data?.title, changedTitleOld: data?.changedFields?.title?.old, changedTitleNew: data?.changedFields?.title?.new, 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; }; /** * Builds notification data for supplement imported events. * TODO: This is an advanced case and will be done later */ const supplementImportedBuilder = (data) => { const body = `A supplement has been imported.`; const result = { app: { jobId: data.jobId, jobRoNumber: data.jobRoNumber, bodyShopId: data.bodyShopId, key: "notifications.job.supplementImported", 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 };