const { jobAssignedToMeBuilder, billPostedHandler, newNoteAddedBuilder, scheduledDatesChangedBuilder, tasksUpdatedCreatedBuilder, jobStatusChangeBuilder, jobsAddedToProductionBuilder, alternateTransportChangedBuilder, newTimeTicketPostedBuilder, intakeDeliveryChecklistCompletedBuilder, paymentCollectedCompletedBuilder, newMediaAddedReassignedBuilder, criticalPartsStatusChangedBuilder, supplementImportedBuilder, partMarkedBackOrderedBuilder } = require("./scenarioBuilders"); const logger = require("../utils/logger"); const { isFunction } = require("lodash"); /** * An array of notification scenario definitions. * * Each scenario object can include the following properties: * - key {string}: The unique scenario name. * - table {string}: The table name to check for changes. * - fields {Array}: Fields to check for changes. * - matchToUserFields {Array}: Fields used to match scenarios to user data. * - onNew {boolean|Array}: Indicates whether the scenario should be triggered on new data. * - builder {Function}: A function to handle the scenario. * - onlyTruthyValues {boolean|Array}: Specifies fields that must have truthy values for the scenario to match. * - filterCallback {Function}: Optional callback (sync or async) to further filter the scenario based on event data (returns boolean). */ const notificationScenarios = [ { key: "job-assigned-to-me", table: "jobs", fields: ["employee_prep", "employee_body", "employee_csr", "employee_refinish"], matchToUserFields: ["employee_prep", "employee_body", "employee_csr", "employee_refinish"], builder: jobAssignedToMeBuilder }, { key: "bill-posted", table: "bills", builder: billPostedHandler, onNew: true }, { key: "new-note-added", table: "notes", builder: newNoteAddedBuilder, onNew: true }, { key: "schedule-dates-changed", table: "jobs", fields: ["scheduled_in", "scheduled_completion", "scheduled_delivery"], builder: scheduledDatesChangedBuilder }, { key: "tasks-updated-created", table: "tasks", fields: ["updated_at"], // onNew: true, builder: tasksUpdatedCreatedBuilder }, { key: "job-status-change", table: "jobs", fields: ["status"], builder: jobStatusChangeBuilder }, { key: "job-added-to-production", table: "jobs", fields: ["inproduction"], onlyTruthyValues: ["inproduction"], builder: jobsAddedToProductionBuilder }, { key: "alternate-transport-changed", table: "jobs", fields: ["alt_transport"], builder: alternateTransportChangedBuilder }, { key: "new-time-ticket-posted", table: "timetickets", builder: newTimeTicketPostedBuilder }, { key: "intake-delivery-checklist-completed", table: "jobs", fields: ["intakechecklist", "deliverchecklist"], builder: intakeDeliveryChecklistCompletedBuilder }, { key: "payment-added", table: "payments", onNew: true, builder: paymentCollectedCompletedBuilder }, { // MAKE SURE YOU ARE NOT ON A LMS ENVIRONMENT key: "new-media-added-reassigned", table: "documents", fields: ["jobid"], builder: newMediaAddedReassignedBuilder }, { key: "critical-parts-status-changed", table: "joblines", fields: ["critical"], onlyTruthyValues: ["critical"], builder: criticalPartsStatusChangedBuilder }, { key: "part-marked-back-ordered", table: "joblines", fields: ["status"], builder: partMarkedBackOrderedBuilder, filterCallback: async ({ eventData, getBodyshopFromRedis }) => { try { const bodyshop = await getBodyshopFromRedis(eventData.bodyShopId); return eventData?.data?.status !== bodyshop?.md_order_statuses?.default_bo; } catch (err) { logger.log("notifications-error-parts-marked-back-ordered", "error", "notifications", "mapper", { message: err?.message, stack: err?.stack }); return false; } } }, // -------------- Difficult --------------- // Holding off on this one for now { key: "supplement-imported", builder: supplementImportedBuilder // spans multiple tables, } ]; /** * Returns an array of scenarios that match the given event data. * Supports asynchronous callbacks for additional filtering. * * @param {Object} eventData - The parsed event data. * Expected properties: * - table: an object with a `name` property (e.g. { name: "tasks", schema: "public" }) * - changedFieldNames: an array of changed field names (e.g. [ "description", "updated_at" ]) * - isNew: boolean indicating whether the record is new or updated * - data: the new data object (used to check field values) * - (other properties may be added such as jobWatchers, bodyShopId, etc.) * @param {Function} getBodyshopFromRedis - Function to retrieve bodyshop data from Redis. * @returns {Promise>} A promise resolving to an array of matching scenario objects. */ const getMatchingScenarios = async (eventData, getBodyshopFromRedis) => { const matches = []; for (const scenario of notificationScenarios) { // If eventData has a table, then only scenarios with a table property that matches should be considered. if (eventData.table) { if (!scenario.table || eventData.table.name !== scenario.table) { continue; } } // Check the onNew flag. // Allow onNew to be either a boolean or an array of booleans. if (Object.prototype.hasOwnProperty.call(scenario, "onNew")) { if (Array.isArray(scenario.onNew)) { if (!scenario.onNew.includes(eventData.isNew)) continue; } else { if (eventData.isNew !== scenario.onNew) continue; } } // If the scenario defines fields, ensure at least one of them is present in changedFieldNames. if (scenario.fields && scenario.fields.length > 0) { const hasMatchingField = scenario.fields.some((field) => eventData.changedFieldNames.includes(field)); if (!hasMatchingField) { continue; } } // OnlyTruthyValues logic: // If onlyTruthyValues is defined, check that the new values of specified fields (or all changed fields if true) // are truthy. If an array, only check the listed fields, which must be in scenario.fields. if (Object.prototype.hasOwnProperty.call(scenario, "onlyTruthyValues")) { let fieldsToCheck; if (scenario.onlyTruthyValues === true) { // If true, check all fields in the scenario that changed fieldsToCheck = scenario.fields.filter((field) => eventData.changedFieldNames.includes(field)); } else if (Array.isArray(scenario.onlyTruthyValues) && scenario.onlyTruthyValues.length > 0) { // If an array, check only the specified fields, ensuring they are in scenario.fields fieldsToCheck = scenario.onlyTruthyValues.filter( (field) => scenario.fields.includes(field) && eventData.changedFieldNames.includes(field) ); // If no fields in onlyTruthyValues match the scenario’s fields or changed fields, skip this scenario if (fieldsToCheck.length === 0) { continue; } } else { // Invalid onlyTruthyValues (not true or a non-empty array), skip this scenario continue; } // Ensure all fields to check have truthy new values const allTruthy = fieldsToCheck.every((field) => Boolean(eventData.data[field])); if (!allTruthy) { continue; } } // Execute the callback if defined, supporting both sync and async, and filter based on its return value if (isFunction(scenario?.filterCallback)) { const shouldFilter = await Promise.resolve( scenario.filterCallback({ eventData, getBodyshopFromRedis }) ); if (shouldFilter) { continue; } } matches.push(scenario); } return matches; }; module.exports = { notificationScenarios, getMatchingScenarios };