Files
bodyshop/server/notifications/scenarioMapper.js

258 lines
8.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const {
jobAssignedToMeBuilder,
billPostedBuilder,
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<string>}: Fields to check for changes.
* - matchToUserFields {Array<string>}: Fields used to match scenarios to user data.
* - onNew {boolean|Array<boolean>}: Indicates whether the scenario should be triggered on new data.
* - builder {Function}: A function to handle the scenario.
* - onlyTruthyValues {boolean|Array<string>}: 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).
* - enabled {boolean}: If true, the scenario is active; if false or omitted, the scenario is skipped.
*/
const notificationScenarios = [
{
key: "job-assigned-to-me",
enabled: true,
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",
enabled: true,
table: "bills",
builder: billPostedBuilder,
onNew: true
},
{
key: "new-note-added",
enabled: true,
table: "notes",
builder: newNoteAddedBuilder,
onNew: true
},
{
key: "schedule-dates-changed",
enabled: true,
table: "jobs",
fields: ["scheduled_in", "scheduled_completion", "scheduled_delivery"],
builder: scheduledDatesChangedBuilder
},
{
key: "tasks-updated-created",
enabled: true,
table: "tasks",
fields: ["updated_at"],
// onNew: true,
builder: tasksUpdatedCreatedBuilder
},
{
key: "job-status-change",
enabled: true,
table: "jobs",
fields: ["status"],
builder: jobStatusChangeBuilder
},
{
key: "job-added-to-production",
enabled: true,
table: "jobs",
fields: ["inproduction"],
onlyTruthyValues: ["inproduction"],
builder: jobsAddedToProductionBuilder
},
{
key: "alternate-transport-changed",
enabled: true,
table: "jobs",
fields: ["alt_transport"],
builder: alternateTransportChangedBuilder
},
{
key: "new-time-ticket-posted",
enabled: true,
table: "timetickets",
builder: newTimeTicketPostedBuilder
},
{
key: "intake-delivery-checklist-completed",
enabled: true,
table: "jobs",
fields: ["intakechecklist", "deliverchecklist"],
builder: intakeDeliveryChecklistCompletedBuilder
},
{
key: "payment-collected-completed",
enabled: true,
table: "payments",
onNew: true,
builder: paymentCollectedCompletedBuilder
},
{
// Only works on a non LMS ENV
key: "new-media-added-reassigned",
enabled: true,
table: "documents",
fields: ["jobid"],
builder: newMediaAddedReassignedBuilder
},
{
key: "critical-parts-status-changed",
enabled: true,
table: "joblines",
fields: ["status"],
onlyTruthyValues: ["status"],
builder: criticalPartsStatusChangedBuilder,
filterCallback: ({ eventData }) => !eventData?.data?.critical
},
{
key: "part-marked-back-ordered",
enabled: true,
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;
}
}
},
// Holding off on this one for now, spans multiple tables
{
key: "supplement-imported",
enabled: false,
builder: supplementImportedBuilder
}
];
/**
* 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<Array<Object>>} A promise resolving to an array of matching scenario objects.
*/
const getMatchingScenarios = async (eventData, getBodyshopFromRedis) => {
const matches = [];
for (const scenario of notificationScenarios) {
// Check if the scenario is enabled; skip if not explicitly true
if (scenario.enabled !== true) {
continue;
}
// 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 scenarios 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
};