diff --git a/client/src/components/profile-my/notification-settings.component.jsx b/client/src/components/profile-my/notification-settings.component.jsx index 659b951a7..e1b2f92d1 100644 --- a/client/src/components/profile-my/notification-settings.component.jsx +++ b/client/src/components/profile-my/notification-settings.component.jsx @@ -128,18 +128,19 @@ function NotificationSettingsForm({ currentUser }) { ) - }, - { - title: setIsDirty(true)} />, - dataIndex: "fcm", - key: "fcm", - align: "center", - render: (_, record) => ( - - - - ) } + // TODO: Disabled for now until FCM is implemented. + // { + // title: setIsDirty(true)} />, + // dataIndex: "fcm", + // key: "fcm", + // align: "center", + // render: (_, record) => ( + // + // + // + // ) + // } ]; const dataSource = notificationScenarios.map((scenario) => ({ key: scenario })); diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index 974f651f5..d8b30cc80 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -1952,6 +1952,27 @@ _eq: X-Hasura-User-Id - active: _eq: true + event_triggers: + - name: notifications_docuemtns + definition: + enable_manual: false + update: + columns: + - jobid + retry_conf: + interval_sec: 10 + num_retries: 0 + timeout_sec: 60 + webhook_from_env: HASURA_API_URL + headers: + - name: event-secret + value_from_env: EVENT_SECRET + request_transform: + method: POST + query_params: {} + template_engine: Kriti + url: '{{$base_url}}/notifications/events/handleDocumentsChange' + version: 2 - table: name: email_audit_trail schema: public @@ -3213,6 +3234,28 @@ _eq: X-Hasura-User-Id - active: _eq: true + event_triggers: + - name: notifications_joblines + definition: + enable_manual: false + insert: + columns: '*' + update: + columns: + - critical + retry_conf: + interval_sec: 10 + num_retries: 0 + timeout_sec: 60 + webhook_from_env: HASURA_API_URL + headers: + - name: event-secret + value_from_env: EVENT_SECRET + request_transform: + method: POST + query_params: {} + template_engine: Kriti + url: '{{$base_url}}/notifications/events/handleJobLinesChange' - table: name: joblines_status schema: public @@ -5662,6 +5705,25 @@ - active: _eq: true event_triggers: + - name: notifications_payments + definition: + enable_manual: false + insert: + columns: '*' + retry_conf: + interval_sec: 10 + num_retries: 0 + timeout_sec: 60 + webhook_from_env: HASURA_API_URL + headers: + - name: event-secret + value_from_env: EVENT_SECRET + request_transform: + method: POST + query_params: {} + template_engine: Kriti + url: '{{$base_url}}/notifications/events/handlePaymentsChange' + version: 2 - name: os_payments definition: delete: diff --git a/server/notifications/eventHandlers.js b/server/notifications/eventHandlers.js new file mode 100644 index 000000000..cad02a7b5 --- /dev/null +++ b/server/notifications/eventHandlers.js @@ -0,0 +1,140 @@ +/** + * @fileoverview Notification event handlers. + * This module exports functions to handle various notification events. + * Each handler optionally calls the scenarioParser and logs errors if they occur, + * then returns a JSON response with a success message. + */ + +const scenarioParser = require("./scenarioParser"); + +/** + * Processes a notification event by invoking the scenario parser. + * The scenarioParser is intentionally not awaited so that the response is sent immediately. + * + * @param {Object} req - Express request object. + * @param {Object} res - Express response object. + * @param {string} parserPath - The key path to be passed to scenarioParser. + * @param {string} successMessage - The message to return on success. + * @returns {Promise} A promise that resolves to an Express JSON response. + */ +async function processNotificationEvent(req, res, parserPath, successMessage) { + const { logger } = req; + + // Call scenarioParser but don't await it; log any error that occurs. + scenarioParser(req, parserPath).catch((error) => { + logger.log("notifications-error", "error", "notifications", null, { error: error?.message }); + }); + + return res.status(200).json({ message: successMessage }); +} + +/** + * Handle job change notifications. + * + * @param {Object} req - Express request object. + * @param {Object} res - Express response object. + * @returns {Promise} JSON response with a success message. + */ +const handleJobsChange = async (req, res) => + processNotificationEvent(req, res, "req.body.event.new.id", "Job Notifications Event Handled."); + +/** + * Handle bills change notifications. + * + * @param {Object} req - Express request object. + * @param {Object} res - Express response object. + * @returns {Promise} JSON response with a success message. + */ +const handleBillsChange = async (req, res) => + processNotificationEvent(req, res, "req.body.event.new.jobid", "Bills Changed Notification Event Handled."); + +/** + * Handle documents change notifications. + * + * @param {Object} req - Express request object. + * @param {Object} res - Express response object. + * @returns {Promise} JSON response with a success message. + */ +const handleDocumentsChange = async (req, res) => + processNotificationEvent(req, res, "req.body.event.new.jobid", "Documents Change Notifications Event Handled."); + +/** + * Handle job lines change notifications. + * + * @param {Object} req - Express request object. + * @param {Object} res - Express response object. + * @returns {Promise} JSON response with a success message. + */ +const handleJobLinesChange = async (req, res) => + processNotificationEvent(req, res, "req.body.event.new.jobid", "JobLines Change Notifications Event Handled."); + +/** + * Handle notes change notifications. + * + * @param {Object} req - Express request object. + * @param {Object} res - Express response object. + * @returns {Promise} JSON response with a success message. + */ +const handleNotesChange = async (req, res) => + processNotificationEvent(req, res, "req.body.event.new.jobid", "Notes Changed Notification Event Handled."); + +/** + * Handle parts dispatch change notifications. + * + * @param {Object} req - Express request object. + * @param {Object} res - Express response object. + * @returns {Object} JSON response with a success message. + */ +const handlePartsDispatchChange = (req, res) => res.status(200).json({ message: "Parts Dispatch change handled." }); + +/** + * Handle parts order change notifications. + * + * @param {Object} req - Express request object. + * @param {Object} res - Express response object. + * @returns {Object} JSON response with a success message. + */ +const handlePartsOrderChange = (req, res) => res.status(200).json({ message: "Parts Order change handled." }); + +/** + * Handle payments change notifications. + * + * @param {Object} req - Express request object. + * @param {Object} res - Express response object. + * @returns {Promise} JSON response with a success message. + */ +const handlePaymentsChange = async (req, res) => + processNotificationEvent(req, res, "req.body.event.new.jobid", "Payments Changed Notification Event Handled."); + +/** + * Handle tasks change notifications. + * + * @param {Object} req - Express request object. + * @param {Object} res - Express response object. + * @returns {Promise} JSON response with a success message. + */ +const handleTasksChange = async (req, res) => + processNotificationEvent(req, res, "req.body.event.new.jobid", "Tasks Notifications Event Handled."); + +/** + * Handle time tickets change notifications. + * + * @param {Object} req - Express request object. + * @param {Object} res - Express response object. + * @returns {Promise} JSON response with a success message. + */ +const handleTimeTicketsChange = async (req, res) => + processNotificationEvent(req, res, "req.body.event.new.jobid", "Time Tickets Changed Notification Event Handled."); + +module.exports = { + handleJobsChange, + handleBillsChange, + handleDocumentsChange, + handleJobLinesChange, + handleNotesChange, + handlePartsDispatchChange, + handlePartsOrderChange, + handlePaymentsChange, + handleTasksChange, + handleTimeTicketsChange +}; diff --git a/server/notifications/eventHandlers/handeJobsChange.js b/server/notifications/eventHandlers/handeJobsChange.js deleted file mode 100644 index f038d8781..000000000 --- a/server/notifications/eventHandlers/handeJobsChange.js +++ /dev/null @@ -1,12 +0,0 @@ -const scenarioParser = require("../utils/scenarioParser"); - -const handleJobsChange = async (req, res) => { - const { logger } = req; - scenarioParser(req, `req.body.event.new.id`).catch((e) => { - console.dir(e); - logger.log("notifications-error", "error", "notifications", null, { error: e?.message }); - }); - return res.status(200).json({ message: "Job Notifications Event Handled." }); -}; -// -module.exports = handleJobsChange; diff --git a/server/notifications/eventHandlers/handleBillsChange.js b/server/notifications/eventHandlers/handleBillsChange.js deleted file mode 100644 index a2c95eb30..000000000 --- a/server/notifications/eventHandlers/handleBillsChange.js +++ /dev/null @@ -1,52 +0,0 @@ -const scenarioParser = require("../utils/scenarioParser"); - -const handleBillsChange = async (req, res) => { - const { logger } = req; - scenarioParser(req, `req.body.event.new.jobid`).catch((e) => { - logger.log("notifications-error", "error", "notifications", null, { error: e?.message }); - }); - return res.status(200).json({ message: "Bills Changed Notification Event Handled." }); -}; -// -module.exports = handleBillsChange; - -//node-app | { -// node-app | created_at: '2025-02-12T16:23:45.397685', -// node-app | delivery_info: { current_retry: 0, max_retries: 0 }, -// node-app | event: { -// node-app | data: { -// node-app | new: { -// node-app | created_at: '2025-02-12T16:23:45.397685+00:00', -// node-app | date: '2025-02-13', -// node-app | due_date: null, -// node-app | exported: false, -// node-app | exported_at: null, -// node-app | federal_tax_rate: 0, -// node-app | id: '873bd1cc-0196-4920-8f2f-4b1bc06f631b', -// node-app | invoice_number: 'sadasdasd', -// node-app | is_credit_memo: false, -// node-app | isinhouse: false, -// node-app | jobid: 'f66534c6-4e1e-462d-bf4f-aca9cf2f03bc', -// node-app | local_tax_rate: 0, -// node-app | state_tax_rate: 7, -// node-app | total: 1, -// node-app | updated_at: '2025-02-12T16:23:45.397685+00:00', -// node-app | vendorid: '4c2ff2c4-af2b-4a5f-970e-3e026f0bbf9f' -// node-app | }, -// node-app | old: null -// node-app | }, -// node-app | op: 'INSERT', -// node-app | session_variables: { -// node-app | 'x-hasura-role': 'user', -// node-app | 'x-hasura-user-id': 'cULlDduYGDgs2oTWSZ1otJIWbfo1' -// node-app | }, -// node-app | trace_context: { -// node-app | sampling_state: '1', -// node-app | span_id: 'b1bfc69e31438823', -// node-app | trace_id: '92d91c363b9a891aa41e5574dbd391d3' -// node-app | } -// node-app | }, -// node-app | id: '2530b665-8421-40b6-bcf1-6d7b39fa020d', -// node-app | table: { name: 'bills', schema: 'public' }, -// node-app | trigger: { name: 'notifications_bills' } -// node-app | } diff --git a/server/notifications/eventHandlers/handleNotesChange.js b/server/notifications/eventHandlers/handleNotesChange.js deleted file mode 100644 index 787267b45..000000000 --- a/server/notifications/eventHandlers/handleNotesChange.js +++ /dev/null @@ -1,13 +0,0 @@ -const scenarioParser = require("../utils/scenarioParser"); - -const handleNotesChange = async (req, res) => { - const { logger } = req; - - scenarioParser(req, `req.body.event.new.jobid`).catch((e) => { - logger.log("notifications-error", "error", "notifications", null, { error: e?.message }); - }); - - return res.status(200).json({ message: "Notes Changed Notification Event Handled." }); -}; - -module.exports = handleNotesChange; diff --git a/server/notifications/eventHandlers/handlePartsDispatchChange.js b/server/notifications/eventHandlers/handlePartsDispatchChange.js deleted file mode 100644 index fd3f78b97..000000000 --- a/server/notifications/eventHandlers/handlePartsDispatchChange.js +++ /dev/null @@ -1,5 +0,0 @@ -const handlePartsDispatchChange = (req, res) => { - return res.status(200).json({ message: "Parts Dispatch change handled." }); -}; - -module.exports = handlePartsDispatchChange; diff --git a/server/notifications/eventHandlers/handlePartsOrderChange.js b/server/notifications/eventHandlers/handlePartsOrderChange.js deleted file mode 100644 index 16c09ef2f..000000000 --- a/server/notifications/eventHandlers/handlePartsOrderChange.js +++ /dev/null @@ -1,5 +0,0 @@ -const handlePartsOrderChange = (req, res) => { - return res.status(200).json({ message: "Parts Order change handled." }); -}; - -module.exports = handlePartsOrderChange; diff --git a/server/notifications/eventHandlers/handleTasksChange.js b/server/notifications/eventHandlers/handleTasksChange.js deleted file mode 100644 index 795f21944..000000000 --- a/server/notifications/eventHandlers/handleTasksChange.js +++ /dev/null @@ -1,11 +0,0 @@ -const scenarioParser = require("../utils/scenarioParser"); - -const handleTasksChange = async (req, res) => { - const { logger } = req; - scenarioParser(req, "req.body.event.new.jobid").catch((e) => - logger.log("notifications-error", "error", "notifications", null, { error: e?.message }) - ); - return res.status(200).json({ message: "Tasks Notifications Event Handled." }); -}; -// -module.exports = handleTasksChange; diff --git a/server/notifications/eventHandlers/handleTimeTicketsChange.js b/server/notifications/eventHandlers/handleTimeTicketsChange.js deleted file mode 100644 index e48782412..000000000 --- a/server/notifications/eventHandlers/handleTimeTicketsChange.js +++ /dev/null @@ -1,13 +0,0 @@ -const scenarioParser = require("../utils/scenarioParser"); - -const handleTimeTicketsChange = async (req, res) => { - const { logger } = req; - - scenarioParser(req, `req.body.event.new.jobid`).catch((e) => { - logger.log("notifications-error", "error", "notifications", null, { error: e?.message }); - }); - - return res.status(200).json({ message: "Time Tickets Changed Notification Event Handled." }); -}; - -module.exports = handleTimeTicketsChange; diff --git a/server/notifications/utils/eventParser.js b/server/notifications/eventParser.js similarity index 60% rename from server/notifications/utils/eventParser.js rename to server/notifications/eventParser.js index 906ed4073..2357dae6c 100644 --- a/server/notifications/utils/eventParser.js +++ b/server/notifications/eventParser.js @@ -1,3 +1,23 @@ +/** + * Parses an event by comparing old and new data to determine which fields have changed. + * + * @async + * @function eventParser + * @param {Object} params - The parameters for parsing the event. + * @param {Object} params.oldData - The previous state of the data. If not provided, the data is considered new. + * @param {Object} params.newData - The new state of the data. + * @param {string} params.trigger - The trigger that caused the event. + * @param {string} params.table - The name of the table where the event occurred. + * @param {string} [params.jobIdField] - The field name or key path (e.g., "req.body.event.new.jobid") used to extract the job ID. + * @returns {Promise} An object containing: + * - {@link changedFieldNames}: An array of field names that have changed. + * - {@link changedFields}: An object mapping changed field names to their new values (or `null` if the field was removed). + * - {boolean} isNew - Indicates if the event is for new data (i.e., no oldData exists). + * - {Object} data - The new data. + * - {string} trigger - The event trigger. + * - {string} table - The table name. + * - {string|null} jobId - The extracted job ID, if available. + */ const eventParser = async ({ oldData, newData, trigger, table, jobIdField }) => { const isNew = !oldData; let changedFields = {}; diff --git a/server/notifications/utils/notificationEmailQueue.js b/server/notifications/notificationEmailQueue.js similarity index 92% rename from server/notifications/utils/notificationEmailQueue.js rename to server/notifications/notificationEmailQueue.js index 8da63d500..209422566 100644 --- a/server/notifications/utils/notificationEmailQueue.js +++ b/server/notifications/notificationEmailQueue.js @@ -4,7 +4,7 @@ require("dotenv").config({ }); const Queue = require("better-queue"); -const logger = require("../../utils/logger"); +const logger = require("../utils/logger"); const notificationsEmailQueue = () => new Queue( diff --git a/server/notifications/scenarioBuilders.js b/server/notifications/scenarioBuilders.js new file mode 100644 index 000000000..c715a3a17 --- /dev/null +++ b/server/notifications/scenarioBuilders.js @@ -0,0 +1,79 @@ +const consoleDir = require("../utils/consoleDir"); + +const alternateTransportChangedBuilder = (data) => { + consoleDir(data); +}; + +const billPostedHandler = (data) => { + consoleDir(data); +}; + +const criticalPartsStatusChangedBuilder = (data) => { + consoleDir(data); +}; + +const intakeDeliveryChecklistCompletedBuilder = (data) => { + consoleDir(data); +}; + +const jobAssignedToMeBuilder = (data) => { + consoleDir(data); +}; + +const jobsAddedToProductionBuilder = (data) => { + consoleDir(data); +}; + +const jobStatusChangeBuilder = (data) => { + consoleDir(data); +}; + +const newMediaAddedReassignedBuilder = (data) => { + consoleDir(data); +}; + +const newNoteAddedBuilder = (data) => { + consoleDir(data); +}; + +const newTimeTicketPostedBuilder = (data) => { + consoleDir(data); +}; + +const partMarkedBackOrderedBuilder = (data) => { + consoleDir(data); +}; + +const paymentCollectedCompletedBuilder = (data) => { + consoleDir(data); +}; + +const scheduledDatesChangedBuilder = (data) => { + consoleDir(data); +}; + +const supplementImportedBuilder = (data) => { + consoleDir(data); +}; + +const tasksUpdatedCreatedBuilder = async (data) => { + consoleDir(data); +}; + +module.exports = { + alternateTransportChangedBuilder, + billPostedHandler, + criticalPartsStatusChangedBuilder, + intakeDeliveryChecklistCompletedBuilder, + jobAssignedToMeBuilder, + jobsAddedToProductionBuilder, + jobStatusChangeBuilder, + newMediaAddedReassignedBuilder, + newNoteAddedBuilder, + newTimeTicketPostedBuilder, + partMarkedBackOrderedBuilder, + paymentCollectedCompletedBuilder, + scheduledDatesChangedBuilder, + supplementImportedBuilder, + tasksUpdatedCreatedBuilder +}; diff --git a/server/notifications/scenarioBuilders/alternateTransportChangedBuilder.js b/server/notifications/scenarioBuilders/alternateTransportChangedBuilder.js deleted file mode 100644 index 55e44a56d..000000000 --- a/server/notifications/scenarioBuilders/alternateTransportChangedBuilder.js +++ /dev/null @@ -1,7 +0,0 @@ -const consoleDir = require("../../utils/consoleDir"); - -const alternateTransportChangedBuilder = (data) => { - consoleDir(data); -}; - -module.exports = alternateTransportChangedBuilder; diff --git a/server/notifications/scenarioBuilders/billPostedHandler.js b/server/notifications/scenarioBuilders/billPostedHandler.js deleted file mode 100644 index 2405027a1..000000000 --- a/server/notifications/scenarioBuilders/billPostedHandler.js +++ /dev/null @@ -1,7 +0,0 @@ -const consoleDir = require("../../utils/consoleDir"); - -const billPostedHandler = (data) => { - consoleDir(data); -}; - -module.exports = billPostedHandler; diff --git a/server/notifications/scenarioBuilders/criticalPartsStatusChangedBuilder.js b/server/notifications/scenarioBuilders/criticalPartsStatusChangedBuilder.js deleted file mode 100644 index 9ee4aac39..000000000 --- a/server/notifications/scenarioBuilders/criticalPartsStatusChangedBuilder.js +++ /dev/null @@ -1,7 +0,0 @@ -const consoleDir = require("../../utils/consoleDir"); - -const criticalPartsStatusChangedBuilder = (data) => { - consoleDir(data); -}; - -module.exports = criticalPartsStatusChangedBuilder; diff --git a/server/notifications/scenarioBuilders/intakeDeliveryChecklistCompletedBuilder.js b/server/notifications/scenarioBuilders/intakeDeliveryChecklistCompletedBuilder.js deleted file mode 100644 index 620fc7751..000000000 --- a/server/notifications/scenarioBuilders/intakeDeliveryChecklistCompletedBuilder.js +++ /dev/null @@ -1,7 +0,0 @@ -const consoleDir = require("../../utils/consoleDir"); - -const intakeDeliveryChecklistCompletedBuilder = (data) => { - consoleDir(data); -}; - -module.exports = intakeDeliveryChecklistCompletedBuilder; diff --git a/server/notifications/scenarioBuilders/jobAssignedToMeBuilder.js b/server/notifications/scenarioBuilders/jobAssignedToMeBuilder.js deleted file mode 100644 index 3f2a31830..000000000 --- a/server/notifications/scenarioBuilders/jobAssignedToMeBuilder.js +++ /dev/null @@ -1,7 +0,0 @@ -const consoleDir = require("../../utils/consoleDir"); - -const jobAssignedToMeBuilder = (data) => { - consoleDir(data); -}; - -module.exports = jobAssignedToMeBuilder; diff --git a/server/notifications/scenarioBuilders/jobStatusChangeBuilder.js b/server/notifications/scenarioBuilders/jobStatusChangeBuilder.js deleted file mode 100644 index 10a7404e5..000000000 --- a/server/notifications/scenarioBuilders/jobStatusChangeBuilder.js +++ /dev/null @@ -1,6 +0,0 @@ -const consoleDir = require("../../utils/consoleDir"); -const jobStatusChangeBuilder = (data) => { - consoleDir(data); -}; - -module.exports = jobStatusChangeBuilder; diff --git a/server/notifications/scenarioBuilders/jobsAddedToProductionBuilder.js b/server/notifications/scenarioBuilders/jobsAddedToProductionBuilder.js deleted file mode 100644 index e6ddfb4e9..000000000 --- a/server/notifications/scenarioBuilders/jobsAddedToProductionBuilder.js +++ /dev/null @@ -1,7 +0,0 @@ -const consoleDir = require("../../utils/consoleDir"); - -const jobsAddedToProductionBuilder = (data) => { - consoleDir(data); -}; - -module.exports = jobsAddedToProductionBuilder; diff --git a/server/notifications/scenarioBuilders/newMediaAddedReassignedBuilder.js b/server/notifications/scenarioBuilders/newMediaAddedReassignedBuilder.js deleted file mode 100644 index 72c9ac61e..000000000 --- a/server/notifications/scenarioBuilders/newMediaAddedReassignedBuilder.js +++ /dev/null @@ -1,6 +0,0 @@ -const consoleDir = require("../../utils/consoleDir"); -const newMediaAddedReassignedBuilder = (data) => { - consoleDir(data); -}; - -module.exports = newMediaAddedReassignedBuilder; diff --git a/server/notifications/scenarioBuilders/newNoteAddedBuilder.js b/server/notifications/scenarioBuilders/newNoteAddedBuilder.js deleted file mode 100644 index 86c7ccd98..000000000 --- a/server/notifications/scenarioBuilders/newNoteAddedBuilder.js +++ /dev/null @@ -1,7 +0,0 @@ -const consoleDir = require("../../utils/consoleDir"); - -const newNoteAddedBuilder = (data) => { - consoleDir(data); -}; - -module.exports = newNoteAddedBuilder; diff --git a/server/notifications/scenarioBuilders/newTimeTicketPostedBuilder.js b/server/notifications/scenarioBuilders/newTimeTicketPostedBuilder.js deleted file mode 100644 index e31598873..000000000 --- a/server/notifications/scenarioBuilders/newTimeTicketPostedBuilder.js +++ /dev/null @@ -1,7 +0,0 @@ -const consoleDir = require("../../utils/consoleDir"); - -const newTimeTicketPostedBuilder = (data) => { - consoleDir(data); -}; - -module.exports = newTimeTicketPostedBuilder; diff --git a/server/notifications/scenarioBuilders/partMarkedBackOrderedBuilder.js b/server/notifications/scenarioBuilders/partMarkedBackOrderedBuilder.js deleted file mode 100644 index d25b462f8..000000000 --- a/server/notifications/scenarioBuilders/partMarkedBackOrderedBuilder.js +++ /dev/null @@ -1,7 +0,0 @@ -const consoleDir = require("../../utils/consoleDir"); - -const partMarkedBackOrderedBuilder = (data) => { - consoleDir(data); -}; - -module.exports = partMarkedBackOrderedBuilder; diff --git a/server/notifications/scenarioBuilders/paymentCollectedCompletedBuilder.js b/server/notifications/scenarioBuilders/paymentCollectedCompletedBuilder.js deleted file mode 100644 index 7a4c01a66..000000000 --- a/server/notifications/scenarioBuilders/paymentCollectedCompletedBuilder.js +++ /dev/null @@ -1,7 +0,0 @@ -const consoleDir = require("../../utils/consoleDir"); - -const paymentCollectedCompletedBuilder = (data) => { - consoleDir(data); -}; - -module.exports = paymentCollectedCompletedBuilder; diff --git a/server/notifications/scenarioBuilders/scheduleDatesChangedBuilder.js b/server/notifications/scenarioBuilders/scheduleDatesChangedBuilder.js deleted file mode 100644 index 9b3fa3a46..000000000 --- a/server/notifications/scenarioBuilders/scheduleDatesChangedBuilder.js +++ /dev/null @@ -1,7 +0,0 @@ -const consoleDir = require("../../utils/consoleDir"); - -const scheduledDatesChangedBuilder = (data) => { - consoleDir(data); -}; - -module.exports = scheduledDatesChangedBuilder; diff --git a/server/notifications/scenarioBuilders/supplementImportedBuilder.js b/server/notifications/scenarioBuilders/supplementImportedBuilder.js deleted file mode 100644 index aa98c55af..000000000 --- a/server/notifications/scenarioBuilders/supplementImportedBuilder.js +++ /dev/null @@ -1,7 +0,0 @@ -const consoleDir = require("../../utils/consoleDir"); - -const supplementImportedBuilder = (data) => { - consoleDir(data); -}; - -module.exports = supplementImportedBuilder; diff --git a/server/notifications/scenarioBuilders/tasksUpdatedCreatedBuilder.js b/server/notifications/scenarioBuilders/tasksUpdatedCreatedBuilder.js deleted file mode 100644 index 365c31e20..000000000 --- a/server/notifications/scenarioBuilders/tasksUpdatedCreatedBuilder.js +++ /dev/null @@ -1,47 +0,0 @@ -const consoleDir = require("../../utils/consoleDir"); - -// node-app | { -// node-app | trigger: 'notifications_tasks', -// node-app | bodyShopId: '71f8494c-89f0-43e0-8eb2-820b52d723bc', -// node-app | bodyShopName: 'Rome Online Collision DEMO', -// node-app | scenarioKey: 'tasks-updated-created', -// node-app | scenarioTable: 'tasks', -// node-app | scenarioFields: [ 'updated_at' ], -// node-app | scenarioBuilder: [AsyncFunction: tasksUpdatedCreatedBuilder], -// node-app | scenarioWatchers: [ { user: 'dave@imex.dev', email: true, app: true, fcm: undefined } ], -// node-app | jobId: 'ec1c26c7-b0ea-493f-9bba-30efc291e0fa', -// node-app | isNew: false, -// node-app | changedFieldNames: [ 'description', 'updated_at' ], -// node-app | changedFields: { -// node-app | description: 'sadasdasdasdsadssdsaddddsdsddddddddddddsdsdddsddddddddddd', -// node-app | updated_at: '2025-02-10T23:02:21.244722+00:00' -// node-app | }, -// node-app | data: { -// node-app | assigned_to: '5e4f78a2-0f23-4e7e-920c-02a4e016b398', -// node-app | billid: null, -// node-app | bodyshopid: '71f8494c-89f0-43e0-8eb2-820b52d723bc', -// node-app | completed: false, -// node-app | completed_at: null, -// node-app | created_at: '2025-02-09T20:02:46.839271+00:00', -// node-app | created_by: 'dave@imex.dev', -// node-app | deleted: false, -// node-app | deleted_at: null, -// node-app | description: 'sadasdasdasdsadssdsaddddsdsddddddddddddsdsdddsddddddddddd', -// node-app | due_date: null, -// node-app | id: 'ca1c49a9-3c26-46cb-bebd-4b93f02cad2a', -// node-app | jobid: 'ec1c26c7-b0ea-493f-9bba-30efc291e0fa', -// node-app | joblineid: '84b5bbf9-ab57-4c77-abb0-8fdd8709c9ff', -// node-app | partsorderid: null, -// node-app | priority: 2, -// node-app | remind_at: null, -// node-app | remind_at_sent: null, -// node-app | title: 'sd', -// node-app | updated_at: '2025-02-10T23:02:21.244722+00:00' -// node-app | } -// node-app | } - -const tasksUpdatedCreatedBuilder = async (data) => { - consoleDir(data); -}; - -module.exports = tasksUpdatedCreatedBuilder; diff --git a/server/notifications/utils/scenarioMapperr.js b/server/notifications/scenarioMapperr.js similarity index 61% rename from server/notifications/utils/scenarioMapperr.js rename to server/notifications/scenarioMapperr.js index 630c233a8..b3a507b7e 100644 --- a/server/notifications/utils/scenarioMapperr.js +++ b/server/notifications/scenarioMapperr.js @@ -1,28 +1,35 @@ -// Key: scenario name -// Table: table name to check for changes -// Fields: fields to check for changes -// OnNew: whether the scenario should be triggered on new data -// Builder: function to handle the scenario - -const tasksUpdatedCreatedBuilder = require("../scenarioBuilders/tasksUpdatedCreatedBuilder"); -const jobStatusChangeBuilder = require("../scenarioBuilders/jobStatusChangeBuilder"); -const jobAssignedToMeBuilder = require("../scenarioBuilders/jobAssignedToMeBuilder"); -const billPostedHandler = require("../scenarioBuilders/billPostedHandler"); -const newNoteAddedBuilder = require("../scenarioBuilders/newNoteAddedBuilder"); -const scheduledDatesChangedBuilder = require("../scenarioBuilders/scheduleDatesChangedBuilder"); -const jobsAddedToProductionBuilder = require("../scenarioBuilders/jobsAddedToProductionBuilder"); -const alternateTransportChangedBuilder = require("../scenarioBuilders/alternateTransportChangedBuilder"); -const paymentCollectedCompletedBuilder = require("../scenarioBuilders/paymentCollectedCompletedBuilder"); -const newMediaAddedReassignedBuilder = require("../scenarioBuilders/newMediaAddedReassignedBuilder"); -const newTimeTicketPostedBuilder = require("../scenarioBuilders/newTimeTicketPostedBuilder"); -const intakeDeliveryChecklistCompletedBuilder = require("../scenarioBuilders/intakeDeliveryChecklistCompletedBuilder"); -const supplementImportedBuilder = require("../scenarioBuilders/supplementImportedBuilder"); -const criticalPartsStatusChangedBuilder = require("../scenarioBuilders/criticalPartsStatusChangedBuilder"); -const partMarkedBackOrderedBuilder = require("../scenarioBuilders/partMarkedBackOrderedBuilder"); +const { + jobAssignedToMeBuilder, + billPostedHandler, + newNoteAddedBuilder, + scheduledDatesChangedBuilder, + tasksUpdatedCreatedBuilder, + jobStatusChangeBuilder, + jobsAddedToProductionBuilder, + alternateTransportChangedBuilder, + newTimeTicketPostedBuilder, + intakeDeliveryChecklistCompletedBuilder, + paymentCollectedCompletedBuilder, + newMediaAddedReassignedBuilder, + criticalPartsStatusChangedBuilder, + supplementImportedBuilder, + partMarkedBackOrderedBuilder +} = require("./scenarioBuilders"); +/** + * 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. + * - onlyTrue {Array}: Specifies fields that must be true for the scenario to match. + * - builder {Function}: A function to handle the scenario. + */ const notificationScenarios = [ { - // Confirmed key: "job-assigned-to-me", table: "jobs", fields: ["employee_pre", "employee_body", "employee_csr", "employee_refinish"], @@ -30,28 +37,24 @@ const notificationScenarios = [ builder: jobAssignedToMeBuilder }, { - // Confirmed key: "bill-posted", table: "bills", builder: billPostedHandler, onNew: true }, { - // Confirmed key: "new-note-added", table: "notes", builder: newNoteAddedBuilder, onNew: true }, { - // Confirmed key: "schedule-dates-changed", table: "jobs", fields: ["scheduled_in", "scheduled_completion", "scheduled_delivery"], builder: scheduledDatesChangedBuilder }, { - // Confirmed key: "tasks-updated-created", table: "tasks", fields: ["updated_at"], @@ -59,26 +62,35 @@ const notificationScenarios = [ builder: tasksUpdatedCreatedBuilder }, { - // Confirmed key: "job-status-change", table: "jobs", fields: ["status"], builder: jobStatusChangeBuilder }, { - // Confirmed key: "job-added-to-production", table: "jobs", fields: ["inproduction"], builder: jobsAddedToProductionBuilder }, { - // Confirmed key: "alternate-transport-changed", table: "jobs", fields: ["alt_transport"], builder: alternateTransportChangedBuilder }, + { + key: "new-time-ticket-posted", + table: "timetickets", + builder: newTimeTicketPostedBuilder + }, + { + // Good test for batching as this will hit multiple scenarios + key: "intake-delivery-checklist-completed", + table: "jobs", + fields: ["intakechecklist"], + builder: intakeDeliveryChecklistCompletedBuilder + }, { key: "payment-collected-completed", table: "payments", @@ -86,37 +98,32 @@ const notificationScenarios = [ builder: paymentCollectedCompletedBuilder }, { - key: "new-time-ticket-posted", - table: "timetickets", - builder: newTimeTicketPostedBuilder - }, - { - // Confirmed, also a good test for batching as this will hit multiple scenarios - key: "intake-delivery-checklist-completed", - table: "jobs", - fields: ["intakechecklist"], - builder: intakeDeliveryChecklistCompletedBuilder - }, - { - key: "supplement-imported", - builder: supplementImportedBuilder + // MAKE SURE YOU ARE NOT ON A LMS ENVIRONMENT + // Potential Callbacks / Save for last + // Not question mark for Non LMS Scenario + key: "new-media-added-reassigned", + table: "documents", + fields: ["jobid"], + builder: newMediaAddedReassignedBuilder }, { key: "critical-parts-status-changed", table: "joblines", + fields: ["critical"], + onlyTrue: ["critical"], builder: criticalPartsStatusChangedBuilder }, + // -------------- Difficult --------------- + // Holding off on this one for now + { + key: "supplement-imported", + builder: supplementImportedBuilder + }, + // This one may be tricky as the jobid is not directly in the event data { key: "part-marked-back-ordered", table: "joblines", builder: partMarkedBackOrderedBuilder - }, - // MAKE SURE YOU ARE NOT ON A LMS ENVIRONMENT - // Potential Callbacks - { - key: "new-media-added-reassigned", - table: "documents", - builder: newMediaAddedReassignedBuilder } ]; @@ -128,9 +135,10 @@ const notificationScenarios = [ * - 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 - * - trigger: the trigger information (if needed for extra filtering) + * - data: the new data object (used to check field values) + * - (other properties may be added such as jobWatchers, bodyShopId, etc.) * - * @returns {Array} An array of matching scenario objects. + * @returns {Array} An array of matching scenario objects. */ function getMatchingScenarios(eventData) { return notificationScenarios.filter((scenario) => { @@ -159,6 +167,18 @@ function getMatchingScenarios(eventData) { } } + // OnlyTrue logic: + // If a scenario defines an onlyTrue array, then at least one of those fields must have changed + // and its new value (from eventData.data) must be non-falsey. + if (scenario.onlyTrue && Array.isArray(scenario.onlyTrue) && scenario.onlyTrue.length > 0) { + const hasTruthyChange = scenario.onlyTrue.some( + (field) => eventData.changedFieldNames.includes(field) && Boolean(eventData.data[field]) + ); + if (!hasTruthyChange) { + return false; + } + } + return true; }); } diff --git a/server/notifications/utils/scenarioParser.js b/server/notifications/scenarioParser.js similarity index 80% rename from server/notifications/utils/scenarioParser.js rename to server/notifications/scenarioParser.js index e9f55201d..ed5d6247d 100644 --- a/server/notifications/utils/scenarioParser.js +++ b/server/notifications/scenarioParser.js @@ -1,6 +1,12 @@ +/** + * @module scenarioParser + * @description + * This module exports a function that parses an event and triggers notification scenarios based on the event data. + */ + const eventParser = require("./eventParser"); -const { client: gqlClient } = require("../../graphql-client/graphql-client"); -const queries = require("../../graphql-client/queries"); +const { client: gqlClient } = require("../graphql-client/graphql-client"); +const queries = require("../graphql-client/queries"); const { isEmpty, isFunction } = require("lodash"); const { getMatchingScenarios } = require("./scenarioMapperr"); @@ -8,8 +14,33 @@ const { getMatchingScenarios } = require("./scenarioMapperr"); * Parses an event and determines matching scenarios for notifications. * Queries job watchers and notification settings before triggering scenario builders. * + * This function performs the following steps: + * + * Parse event data to extract necessary details using {@link eventParser}. + * Query job watchers for the given job ID using a GraphQL client. + * Retrieve body shop information from the job. + * Determine matching scenarios based on event data. + * Query notification settings for job watchers. + * Filter scenario watchers based on enabled notification methods. + * Trigger scenario builders for matching scenarios with eligible watchers. + * + * + * @async + * @function scenarioParser * @param {Object} req - The request object containing event data. + * Expected properties: + * + * { + * body: { + * event: { data: { new: Object, old: Object } }, + * trigger: Object, + * table: string + * } + * } + * * @param {string} jobIdField - The field used to identify the job ID. + * @returns {Promise} A promise that resolves when the scenarios have been processed. + * @throws {Error} Throws an error if required request fields are missing or if body shop data is not found. */ const scenarioParser = async (req, jobIdField) => { const { event, trigger, table } = req.body; @@ -30,7 +61,6 @@ const scenarioParser = async (req, jobIdField) => { // Step 2: Query job watchers for the given job ID. // console.log(`2`); - const watcherData = await gqlClient.request(queries.GET_JOB_WATCHERS, { jobid: eventData.jobId }); @@ -48,7 +78,6 @@ const scenarioParser = async (req, jobIdField) => { // Step 3: Retrieve body shop information from the job. // console.log(`3`); - const bodyShopId = watcherData?.job?.bodyshop?.id; const bodyShopName = watcherData?.job?.bodyshop?.shopname; @@ -58,7 +87,6 @@ const scenarioParser = async (req, jobIdField) => { // Step 4: Determine matching scenarios based on event data. // console.log(`4`); - const matchingScenarios = getMatchingScenarios({ ...eventData, jobWatchers, @@ -80,7 +108,6 @@ const scenarioParser = async (req, jobIdField) => { // Step 5: Query notification settings for job watchers. // console.log(`5`); - const associationsData = await gqlClient.request(queries.GET_NOTIFICATION_ASSOCIATIONS, { emails: jobWatchers.map((x) => x.email), shopid: bodyShopId @@ -92,7 +119,6 @@ const scenarioParser = async (req, jobIdField) => { // Step 6: Filter scenario watchers based on enabled notification methods. // console.log(`6`); - finalScenarioData.matchingScenarios = finalScenarioData.matchingScenarios.map((scenario) => ({ ...scenario, scenarioWatchers: associationsData.associations @@ -123,7 +149,6 @@ const scenarioParser = async (req, jobIdField) => { // Step 7: Trigger scenario builders for matching scenarios with eligible watchers. // console.log(`7`); - for (const scenario of finalScenarioData.matchingScenarios) { if (isEmpty(scenario.scenarioWatchers) || !isFunction(scenario.builder)) { continue; @@ -146,7 +171,6 @@ const scenarioParser = async (req, jobIdField) => { // Step 8: Filter scenario fields to only include changed fields. // console.log(`8`); - const filteredScenarioFields = scenario.fields?.filter((field) => eventData.changedFieldNames.includes(field)) || []; diff --git a/server/routes/notificationsRoutes.js b/server/routes/notificationsRoutes.js index e2e60bdac..0d47882b1 100644 --- a/server/routes/notificationsRoutes.js +++ b/server/routes/notificationsRoutes.js @@ -2,14 +2,18 @@ const express = require("express"); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); const { subscribe, unsubscribe, sendNotification } = require("../firebase/firebase-handler"); const eventAuthorizationMiddleware = require("../middleware/eventAuthorizationMIddleware"); - -const handlePartsOrderChange = require("../notifications/eventHandlers/handlePartsOrderChange"); -const handlePartsDispatchChange = require("../notifications/eventHandlers/handlePartsDispatchChange"); -const handleTasksChange = require("../notifications/eventHandlers/handleTasksChange"); -const handleTimeTicketsChange = require("../notifications/eventHandlers/handleTimeTicketsChange"); -const handleJobsChange = require("../notifications/eventHandlers/handeJobsChange"); -const handleBillsChange = require("../notifications/eventHandlers/handleBillsChange"); -const handleNotesChange = require("../notifications/eventHandlers/handleNotesChange"); +const { + handleJobsChange, + handleBillsChange, + handlePartsOrderChange, + handlePartsDispatchChange, + handleTasksChange, + handleTimeTicketsChange, + handleNotesChange, + handlePaymentsChange, + handleDocumentsChange, + handleJobLinesChange +} = require("../notifications/eventHandlers"); const router = express.Router(); @@ -26,5 +30,8 @@ router.post("/events/handlePartsDispatchChange", eventAuthorizationMiddleware, h router.post("/events/handleTasksChange", eventAuthorizationMiddleware, handleTasksChange); router.post("/events/handleTimeTicketsChange", eventAuthorizationMiddleware, handleTimeTicketsChange); router.post("/events/handleNotesChange", eventAuthorizationMiddleware, handleNotesChange); +router.post("/events/handlePaymentsChange", eventAuthorizationMiddleware, handlePaymentsChange); +router.post("/events/handleDocumentsChange", eventAuthorizationMiddleware, handleDocumentsChange); +router.post("/events/handleJobLinesChange", eventAuthorizationMiddleware, handleJobLinesChange); module.exports = router;
This function performs the following steps: + *
+ * { + * body: { + * event: { data: { new: Object, old: Object } }, + * trigger: Object, + * table: string + * } + * } + *