From c82cfb3ec22f9544e658ba9a69c95994f56bbcff Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 20 Feb 2025 13:13:09 -0500 Subject: [PATCH] feature/IO-3096-GlobalNotifications - Checkpoint, fixed some email bugs in other files, consolidated the GetEndpoints on the backend, moved the consolidation delays for queues to ENV vars --- server/accounting/qbo/qbo-callback.js | 14 ++------ server/email/sendemail.js | 10 +++--- server/email/tasksEmails.js | 27 ++++----------- server/intellipay/intellipay.js | 38 ++++++++++----------- server/notifications/queues/appQueue.js | 23 ++++++++----- server/notifications/queues/emailQueue.js | 40 +++++++++++++---------- server/utils/instanceMgr.js | 16 +++++++++ 7 files changed, 85 insertions(+), 83 deletions(-) diff --git a/server/accounting/qbo/qbo-callback.js b/server/accounting/qbo/qbo-callback.js index af6d244d7..f1de7d551 100644 --- a/server/accounting/qbo/qbo-callback.js +++ b/server/accounting/qbo/qbo-callback.js @@ -7,7 +7,7 @@ const OAuthClient = require("intuit-oauth"); const client = require("../../graphql-client/graphql-client").client; const queries = require("../../graphql-client/queries"); const { parse, stringify } = require("querystring"); -const InstanceManager = require("../../utils/instanceMgr").default; +const { InstanceEndpoints } = require("../../utils/instanceMgr"); const oauthClient = new OAuthClient({ clientId: process.env.QBO_CLIENT_ID, @@ -17,16 +17,8 @@ const oauthClient = new OAuthClient({ logging: true }); -let url; - -if (process.env.NODE_ENV === "production") { - //TODO:AIO Add in QBO callbacks. - url = InstanceManager({ imex: `https://imex.online`, rome: `https://romeonline.io` }); -} else if (process.env.NODE_ENV === "test") { - url = InstanceManager({ imex: `https://test.imex.online`, rome: `https://test.romeonline.io` }); -} else { - url = `http://localhost:3000`; -} +//TODO:AIO Add in QBO callbacks. +const url = InstanceEndpoints(); exports.default = async (req, res) => { const queryString = req.url.split("?").reverse()[0]; diff --git a/server/email/sendemail.js b/server/email/sendemail.js index 97637fd2d..cc5e4a79f 100644 --- a/server/email/sendemail.js +++ b/server/email/sendemail.js @@ -239,24 +239,24 @@ const emailBounce = async (req, res) => { return; } //If it's bounced, log it as bounced in audit log. Send an email to the user. - const result = await client.request(queries.UPDATE_EMAIL_AUDIT, { + await client.request(queries.UPDATE_EMAIL_AUDIT, { sesid: messageId, status: "Bounced", context: message.bounce?.bouncedRecipients }); mailer.sendMail( { - from: InstanceMgr({ + from: InstanceManager({ imex: `ImEX Online `, rome: `Rome Online ` }), to: replyTo, //bcc: "patrick@snapt.ca", - subject: `${InstanceMgr({ + subject: `${InstanceManager({ imex: "ImEX Online", rome: "Rome Online" })} Bounced Email - RE: ${subject}`, - text: `${InstanceMgr({ + text: `${InstanceManager({ imex: "ImEX Online", rome: "Rome Online" })} has tried to deliver an email with the subject: ${subject} to the intended recipients but encountered an error. @@ -270,7 +270,7 @@ ${body.bounce?.bouncedRecipients.map( }, (err, info) => { logger.log("sns-error", err ? "error" : "debug", "api", null, { - message: err ? JSON.stringify(error) : info + message: err?.message ?? info }); } ); diff --git a/server/email/tasksEmails.js b/server/email/tasksEmails.js index 6d787811a..05811e5e2 100644 --- a/server/email/tasksEmails.js +++ b/server/email/tasksEmails.js @@ -10,6 +10,7 @@ const generateEmailTemplate = require("./generateTemplate"); const moment = require("moment-timezone"); const { taskEmailQueue } = require("./tasksEmailsQueue"); const mailer = require("./mailer"); +const { InstanceEndpoints } = require("../utils/instanceMgr"); // Initialize the Tasks Email Queue const tasksEmailQueue = taskEmailQueue(); @@ -83,15 +84,8 @@ const formatPriority = (priority) => { * @param taskId * @returns {{header, body: string, subHeader: string}} */ - -const getEndpoints = (bodyshop) => - InstanceManager({ - imex: process.env?.NODE_ENV === "test" ? "https://test.imex.online" : "https://imex.online", - rome: process.env?.NODE_ENV === "test" ? "https//test.romeonline.io" : "https://romeonline.io" - }); - const generateTemplateArgs = (title, priority, description, dueDate, bodyshop, job, taskId, dateLine, createdBy) => { - const endPoints = getEndpoints(bodyshop); + const endPoints = InstanceEndpoints(); return { header: title, subHeader: `Body Shop: ${bodyshop.shopname} | Priority: ${formatPriority(priority)} ${formatDate(dueDate)} | Created By: ${createdBy || "N/A"}`, @@ -108,9 +102,8 @@ const generateTemplateArgs = (title, priority, description, dueDate, bodyshop, j * @param html * @param taskIds * @param successCallback - * @param requestInstance */ -const sendMail = (type, to, subject, html, taskIds, successCallback, requestInstance) => { +const sendMail = (type, to, subject, html, taskIds, successCallback) => { const fromEmails = InstanceManager({ imex: "ImEX Online ", rome: "Rome Online " @@ -136,7 +129,7 @@ const sendMail = (type, to, subject, html, taskIds, successCallback, requestInst }; /** - * Send an email to the assigned user. + * Email the assigned user. * @param req * @param res * @returns {Promise<*>} @@ -186,7 +179,7 @@ const taskAssignedEmail = async (req, res) => { }; /** - * Send an email to remind the user of their tasks. + * Email remind the user of their tasks. * @param req * @param res * @returns {Promise<*>} @@ -264,11 +257,6 @@ const tasksRemindEmail = async (req, res) => { } // There are multiple emails to send to this author. else { - const endPoints = InstanceManager({ - imex: process.env?.NODE_ENV === "test" ? "https://test.imex.online" : "https://imex.online", - rome: process.env?.NODE_ENV === "test" ? "https//test.romeonline.io" : "https://romeonline.io" - }); - const allTasks = groupedTasks[recipient.email]; emailData.subject = `New Tasks Reminder - ${allTasks.length} Tasks require your attention`; emailData.html = generateEmailTemplate({ @@ -278,7 +266,7 @@ const tasksRemindEmail = async (req, res) => { body: `` @@ -338,6 +326,5 @@ const tasksRemindEmail = async (req, res) => { module.exports = { taskAssignedEmail, - tasksRemindEmail, - getEndpoints + tasksRemindEmail }; diff --git a/server/intellipay/intellipay.js b/server/intellipay/intellipay.js index 71adce9a3..0106cb6da 100644 --- a/server/intellipay/intellipay.js +++ b/server/intellipay/intellipay.js @@ -10,12 +10,11 @@ const moment = require("moment"); const logger = require("../utils/logger"); const { sendTaskEmail } = require("../email/sendemail"); const generateEmailTemplate = require("../email/generateTemplate"); -const { getEndpoints } = require("../email/tasksEmails"); const domain = process.env.NODE_ENV ? "secure" : "test"; const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager"); -const { InstanceRegion } = require("../utils/instanceMgr"); +const { InstanceRegion, InstanceEndpoints } = require("../utils/instanceMgr"); const client = new SecretsManagerClient({ region: InstanceRegion() @@ -443,31 +442,28 @@ exports.postback = async (req, res) => { }); if (values.origin === "OneLink" && parsedComment.userEmail) { - try { - const endPoints = getEndpoints(); - sendTaskEmail({ - to: parsedComment.userEmail, - subject: `New Payment(s) Received - RO ${jobs.jobs.map((j) => j.ro_number).join(", ")}`, - type: "html", - html: generateEmailTemplate({ - header: "New Payment(s) Received", - subHeader: "", - body: jobs.jobs - .map( - (job) => - `Reference: ${job.ro_number || "N/A"} | ${job.ownr_co_nm ? job.ownr_co_nm : `${job.ownr_fn || ""} ${job.ownr_ln || ""}`.trim()} | ${`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""}`.trim()} | $${partialPayments.find((p) => p.jobid === job.id).amount}` - ) - .join("
") - }) - }); - } catch (error) { + sendTaskEmail({ + to: parsedComment.userEmail, + subject: `New Payment(s) Received - RO ${jobs.jobs.map((j) => j.ro_number).join(", ")}`, + type: "html", + html: generateEmailTemplate({ + header: "New Payment(s) Received", + subHeader: "", + body: jobs.jobs + .map( + (job) => + `Reference: ${job.ro_number || "N/A"} | ${job.ownr_co_nm ? job.ownr_co_nm : `${job.ownr_fn || ""} ${job.ownr_ln || ""}`.trim()} | ${`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""}`.trim()} | $${partialPayments.find((p) => p.jobid === job.id).amount}` + ) + .join("
") + }) + }).catch((error) => { logger.log("intellipay-postback-email-error", "ERROR", req.user?.email, null, { message: error.message, jobs, paymentResult, ...logResponseMeta }); - } + }); } res.sendStatus(200); } else if (values.invoice) { diff --git a/server/notifications/queues/appQueue.js b/server/notifications/queues/appQueue.js index 04c6e5fcd..837eb52fc 100644 --- a/server/notifications/queues/appQueue.js +++ b/server/notifications/queues/appQueue.js @@ -1,13 +1,20 @@ const { Queue, Worker } = require("bullmq"); -// Base time-related constant (in milliseconds) -const CONSOLIDATION_DELAY = 60000; // 1 minute (base timeout) +// Base time-related constant in minutes, sourced from environment variable or defaulting to 1 +const APP_CONSOLIDATION_DELAY_IN_MINS = (() => { + const envValue = process.env?.APP_CONSOLIDATION_DELAY_IN_MINS; + const parsedValue = envValue ? parseInt(envValue, 10) : NaN; + return isNaN(parsedValue) ? 1 : Math.max(1, parsedValue); // Default to 1, ensure at least 1 +})(); -// Derived time-related constants based on CONSOLIDATION_DELAY / DO NOT TOUCH, these are pegged to CONSOLIDATION_DELAY -const NOTIFICATION_STORAGE_EXPIRATION = CONSOLIDATION_DELAY * 1.5; // 1.5 minutes (90s, for notification storage) -const CONSOLIDATION_FLAG_EXPIRATION = CONSOLIDATION_DELAY * 1.5; // 1.5 minutes (90s, buffer for consolidation flag) -const LOCK_EXPIRATION = CONSOLIDATION_DELAY * 0.25; // 15 seconds (quarter of base, for lock duration) -const RATE_LIMITER_DURATION = CONSOLIDATION_DELAY * 0.1; // 6 seconds (tenth of base, for rate limiting) +// Base time-related constant (in milliseconds) +const APP_CONSOLIDATION_DELAY = APP_CONSOLIDATION_DELAY_IN_MINS * 60000; // 1 minute (base timeout) + +// Derived time-related constants based on APP_CONSOLIDATION_DELAY / DO NOT TOUCH, these are pegged to APP_CONSOLIDATION_DELAY +const NOTIFICATION_STORAGE_EXPIRATION = APP_CONSOLIDATION_DELAY * 1.5; // 1.5 minutes (90s, for notification storage) +const CONSOLIDATION_FLAG_EXPIRATION = APP_CONSOLIDATION_DELAY * 1.5; // 1.5 minutes (90s, buffer for consolidation flag) +const LOCK_EXPIRATION = APP_CONSOLIDATION_DELAY * 0.25; // 15 seconds (quarter of base, for lock duration) +const RATE_LIMITER_DURATION = APP_CONSOLIDATION_DELAY * 0.1; // 6 seconds (tenth of base, for rate limiting) let addQueue; let consolidateQueue; @@ -74,7 +81,7 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => { { jobId, recipients }, { jobId: `consolidate:${jobId}`, - delay: CONSOLIDATION_DELAY, + delay: APP_CONSOLIDATION_DELAY, attempts: 3, // Retry up to 3 times backoff: LOCK_EXPIRATION // Retry delay matches lock expiration (15s) } diff --git a/server/notifications/queues/emailQueue.js b/server/notifications/queues/emailQueue.js index ba850a6fc..302c3665a 100644 --- a/server/notifications/queues/emailQueue.js +++ b/server/notifications/queues/emailQueue.js @@ -1,8 +1,16 @@ const { Queue, Worker } = require("bullmq"); const { sendTaskEmail } = require("../../email/sendemail"); +const generateEmailTemplate = require("../../email/generateTemplate"); +const { InstanceEndpoints } = require("../../utils/instanceMgr"); + +const EMAIL_CONSOLIDATION_DELAY_IN_MINS = (() => { + const envValue = process.env?.APP_CONSOLIDATION_DELAY_IN_MINS; + const parsedValue = envValue ? parseInt(envValue, 10) : NaN; + return isNaN(parsedValue) ? 1 : Math.max(1, parsedValue); // Default to 1, ensure at least 1 +})(); // Base time-related constant (in milliseconds) -const EMAIL_CONSOLIDATION_DELAY = 60000; // 1 minute (base timeout) +const EMAIL_CONSOLIDATION_DELAY = EMAIL_CONSOLIDATION_DELAY_IN_MINS * 60000; // 1 minute (base timeout) // Derived time-related constants based on EMAIL_CONSOLIDATION_DELAY / DO NOT TOUCH, these are pegged to EMAIL_CONSOLIDATION_DELAY const CONSOLIDATION_KEY_EXPIRATION = EMAIL_CONSOLIDATION_DELAY * 1.5; // 1.5 minutes (90s, buffer for consolidation) @@ -109,27 +117,23 @@ const loadEmailQueue = async ({ pubClient, logger }) => { const details = await pubClient.hgetall(detailsKey); const firstName = details.firstName || "User"; const subject = `Updates for job ${jobRoNumber} at ${bodyShopName}`; - const body = [ - '', - "Dear " + firstName + ",", - "", - "There have been updates to job " + jobRoNumber + ":", - "", - "
    ", - ...messages.map((msg) => "
  • " + msg + "
  • "), - "
", - "", - "Please check the job for more details.", - "", - "Best regards,", - bodyShopName, - "" - ].join("\n"); + // Use the template instead of inline HTML + const emailBody = generateEmailTemplate({ + header: `Updates for Job ${jobRoNumber}`, + subHeader: `Dear ${firstName},`, + body: ` +

There have been updates to job ${jobRoNumber} at ${bodyShopName}:


+
    + ${messages.map((msg) => `
  • ${msg}
  • `).join("")} +


+

Please check the job for more details.

+ ` + }); await sendTaskEmail({ to: recipient, subject, type: "html", - html: body + html: emailBody }); logger.logger.info( `Sent consolidated email to ${recipient} for jobId ${jobId} with ${messages.length} updates` diff --git a/server/utils/instanceMgr.js b/server/utils/instanceMgr.js index b94408fe6..07759ac32 100644 --- a/server/utils/instanceMgr.js +++ b/server/utils/instanceMgr.js @@ -58,4 +58,20 @@ exports.InstanceRegion = () => rome: "us-east-2" }); +exports.InstanceEndpoints = () => + InstanceManager({ + imex: + process.env?.NODE_ENV === "development" + ? "https://localhost:3000" + : process.env?.NODE_ENV === "test" + ? "https://test.imex.online" + : "https://imex.online", + rome: + process.env?.NODE_ENV === "development" + ? "https://localhost:3000" + : process.env?.NODE_ENV === "test" + ? "https://test.romeonline.io" + : "https://romeonline.io" + }); + exports.default = InstanceManager;