From 5e308f13d36fdf48acec8012981eec058f60dc68 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Wed, 1 Oct 2025 14:47:32 -0700 Subject: [PATCH 1/3] IO-3386 --- server/data/carfax-rps.js | 412 ++++++++++++++++++++++++ server/data/carfax.js | 28 +- server/data/data.js | 1 + server/graphql-client/graphql-client.js | 17 + server/routes/dataRoutes.js | 3 +- 5 files changed, 449 insertions(+), 12 deletions(-) create mode 100644 server/data/carfax-rps.js diff --git a/server/data/carfax-rps.js b/server/data/carfax-rps.js new file mode 100644 index 000000000..e8c43f7ae --- /dev/null +++ b/server/data/carfax-rps.js @@ -0,0 +1,412 @@ +const queries = require("../graphql-client/queries"); +const Dinero = require("dinero.js"); +const moment = require("moment-timezone"); +const logger = require("../utils/logger"); +const InstanceManager = require("../utils/instanceMgr").default; +const { isString, isEmpty } = require("lodash"); +const fs = require("fs"); +const client = require("../graphql-client/graphql-client").rpsClient; +const { sendServerEmail, sendMexicoBillingEmail } = require("../email/sendemail"); +const { uploadFileToS3 } = require("../utils/s3"); +const crypto = require("crypto"); +const { ftpSetup, uploadToS3 } = require("./carfax") +let Client = require("ssh2-sftp-client"); + +const AHDateFormat = "YYYY-MM-DD"; + +const NON_ASCII_REGEX = /[^\x20-\x7E]/g; + +const carfaxExportRps = async (req, res) => { + const { bodyshops } = await client.request(queries.GET_CARFAX_SHOPS); //Query for the List of Bodyshop Clients. + + // Only process if in production environment. + if (process.env.NODE_ENV !== "production") { + return res.sendStatus(403); + } + // Only process if the appropriate token is provided. + if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) { + return res.sendStatus(401); + } + + // Send immediate response and continue processing. + res.status(202).json({ + success: true, + message: "Processing request ...", + timestamp: new Date().toISOString() + }); + + try { + logger.log("CARFAX-RPS-start", "DEBUG", "api", null, null); + const allXMLResults = []; + const allErrors = []; + + const { bodyshops } = await client.request(queries.GET_CARFAX_SHOPS); //Query for the List of Bodyshop Clients. + const specificShopIds = req.body.bodyshopIds; // ['uuid]; + const { start, end, skipUpload, ignoreDateFilter } = req.body; //YYYY-MM-DD + + const shopsToProcess = + specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops; + logger.log("CARFAX-RPS-shopsToProcess-generated", "DEBUG", "api", null, null); + + if (shopsToProcess.length === 0) { + logger.log("CARFAX-RPS-shopsToProcess-empty", "DEBUG", "api", null, null); + return; + } + + await processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allXMLResults, allErrors); + + await sendServerEmail({ + subject: `Project Mexico Report ${moment().format("MM-DD-YY")}`, + text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify( + allXMLResults.map((x) => ({ + imexshopid: x.imexshopid, + filename: x.filename, + count: x.count, + result: x.result + })), + null, + 2 + )}` + }); + + logger.log("CARFAX-RPS-end", "DEBUG", "api", null, null); + } catch (error) { + logger.log("CARFAX-RPS-error", "ERROR", "api", null, { error: error.message, stack: error.stack }); + } +}; + +async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allXMLResults, allErrors) { + for (const bodyshop of shopsToProcess) { + const shopid = bodyshop.imexshopid?.toLowerCase() || bodyshop.shopname.replace(/[^a-zA-Z0-9]/g, "").toLowerCase(); + const erroredJobs = []; + try { + logger.log("CARFAX-RPS-start-shop-extract", "DEBUG", "api", bodyshop.id, { + shopname: bodyshop.shopname + }); + + const { jobs, bodyshops_by_pk } = await client.request(queries.CARFAX_QUERY, { + bodyshopid: bodyshop.id, + ...(ignoreDateFilter + ? {} + : { + start: start ? moment(start).startOf("day") : moment().subtract(7, "days").startOf("day"), + ...(end && { end: moment(end).endOf("day") }) + }) + }); + + const carfaxObject = { + shopid: shopid, + shop_name: bodyshop.shopname, + job: jobs.map((j) => + CreateRepairOrderTag({ ...j, bodyshop: bodyshops_by_pk }, function ({ job, error }) { + erroredJobs.push({ job: job, error: error.toString() }); + }) + ) + }; + + if (erroredJobs.length > 0) { + logger.log("CARFAX-RPS-failed-jobs", "ERROR", "api", bodyshop.id, { + count: erroredJobs.length, + jobs: JSON.stringify(erroredJobs.map((j) => j.job.ro_number)) + }); + } + + const jsonObj = { + bodyshopid: bodyshop.id, + imexshopid: shopid, + json: JSON.stringify(carfaxObject, null, 2), + filename: `${shopid}_${moment().format("DDMMYYYY_HHMMss")}.json`, + count: carfaxObject.job.length + }; + + if (skipUpload) { + fs.writeFileSync(`./logs/${jsonObj.filename}`, jsonObj.json); + uploadToS3(jsonObj); + } else { + await uploadViaSFTP(jsonObj); + + await sendMexicoBillingEmail({ + subject: `${shopid.replace(/_/g, "").toUpperCase()}_Mexico${InstanceManager({ + imex: "IO", + rome: "RO" + })}_${moment().format("MMDDYYYY")} ROs ${jsonObj.count} Error ${errorCode(jsonObj)}`, + text: `Errors:\n${JSON.stringify( + erroredJobs.map((ej) => ({ + ro_number: ej.job?.ro_number, + jobid: ej.job?.id, + error: ej.error + })), + null, + 2 + )}\n\nUploaded:\n${JSON.stringify( + { + bodyshopid: bodyshop.id, + imexshopid: shopid, + count: jsonObj.count, + filename: jsonObj.filename, + result: jsonObj.result + }, + null, + 2 + )}` + }); + } + + allXMLResults.push({ + bodyshopid: bodyshop.id, + imexshopid: shopid, + count: jsonObj.count, + filename: jsonObj.filename, + result: jsonObj.result + }); + + logger.log("CARFAX-RPS-end-shop-extract", "DEBUG", "api", bodyshop.id, { + shopname: bodyshop.shopname + }); + } catch (error) { + //Error at the shop level. + logger.log("CARFAX-RPS-error-shop", "ERROR", "api", bodyshop.id, { error: error.message, stack: error.stack }); + + allErrors.push({ + bodyshopid: bodyshop.id, + imexshopid: shopid, + CARFAXid: bodyshop.CARFAXid, + fatal: true, + errors: [error.toString()] + }); + } finally { + allErrors.push({ + bodyshopid: bodyshop.id, + imexshopid: shopid, + CARFAXid: bodyshop.CARFAXid, + errors: erroredJobs.map((ej) => ({ + ro_number: ej.job?.ro_number, + jobid: ej.job?.id, + error: ej.error + })) + }); + } + } +} + +async function uploadViaSFTP(jsonObj) { + const sftp = new Client(); + sftp.on("error", (errors) => + logger.log("CARFAX-RPS-sftp-connection-error", "ERROR", "api", jsonObj.bodyshopid, { + error: errors.message, + stack: errors.stack + }) + ); + try { + // Upload to S3 first. + uploadToS3(jsonObj); + + //Connect to the FTP and upload all. + await sftp.connect(ftpSetup); + + try { + jsonObj.result = await sftp.put(Buffer.from(jsonObj.json), `${jsonObj.filename}`); + logger.log("CARFAX-RPS-sftp-upload", "DEBUG", "api", jsonObj.bodyshopid, { + imexshopid: jsonObj.imexshopid, + filename: jsonObj.filename, + result: jsonObj.result + }); + } catch (error) { + logger.log("CARFAX-RPS-sftp-upload-error", "ERROR", "api", jsonObj.bodyshopid, { + filename: jsonObj.filename, + error: error.message, + stack: error.stack + }); + throw error; + } + } catch (error) { + logger.log("CARFAX-RPS-sftp-error", "ERROR", "api", jsonObj.bodyshopid, { error: error.message, stack: error.stack }); + throw error; + } finally { + sftp.end(); + } +} + +const CreateRepairOrderTag = (job, errorCallback) => { + if (!job.job_totals) { + errorCallback({ + jobid: job.id, + job: job, + ro_number: job.ro_number, + error: { toString: () => "No job totals for RO." } + }); + return {}; + } + + try { + const ret = { + ro_number: crypto.createHash("md5").update(job.ro_number, "utf8").digest("hex"), + v_vin: job.v_vin || "", + v_year: job.v_model_yr + ? parseInt(job.v_model_yr.match(/\d/g)) + ? parseInt(job.v_model_yr.match(/\d/g).join(""), 10) + : "" + : "", + v_make: job.v_make_desc || "", + v_model: job.v_model_desc || "", + + date_estimated: [job.date_estimated, job.created_at].find((date) => date) + ? moment([job.date_open, job.created_at].find((date) => date)) + .tz(job.bodyshop.timezone) + .format(AHDateFormat) + : "", + data_opened: [job.date_open, job.created_at].find((date) => date) + ? moment([job.date_open, job.created_at].find((date) => date)) + .tz(job.bodyshop.timezone) + .format(AHDateFormat) + : "", + date_invoiced: [job.date_invoiced, job.actual_delivery, job.actual_completion].find((date) => date) + ? moment([job.date_invoiced, job.actual_delivery, job.actual_completion].find((date) => date)) + .tz(job.bodyshop.timezone) + .format(AHDateFormat) + : "", + loss_date: job.loss_date ? moment(job.loss_date).format(AHDateFormat) : "", + + ins_co_nm: job.ins_co_nm || "", + loss_desc: job.loss_desc || "", + theft_ind: job.theft_ind, + tloss_ind: job.tlos_ind, + subtotal: Dinero(job.job_totals.totals.subtotal).toUnit(), + + areaofdamage: { + impact1: generateAreaOfDamage(job.area_of_damage?.impact1 || ""), + impact2: generateAreaOfDamage(job.area_of_damage?.impact2 || "") + }, + + jobLines: job.joblines.length > 0 ? job.joblines.map((jl) => GenerateDetailLines(jl)) : [generateNullDetailLine()] + }; + return ret; + } catch (error) { + logger.log("CARFAX-RPS-job-data-error", "ERROR", "api", null, { error: error.message, stack: error.stack }); + errorCallback({ jobid: job.id, ro_number: job.ro_number, error }); + } +}; + +const GenerateDetailLines = (line) => { + const ret = { + line_desc: line.line_desc ? line.line_desc.replace(NON_ASCII_REGEX, "") : null, + oem_partno: line.oem_partno ? line.oem_partno.replace(NON_ASCII_REGEX, "") : null, + alt_partno: line.alt_partno ? line.alt_partno.replace(NON_ASCII_REGEX, "") : null, + op_code_desc: line.op_code_desc ? line.op_code_desc.replace(NON_ASCII_REGEX, "") : null, + lbr_ty: generateLaborType(line.mod_lbr_ty), + lbr_hrs: line.mod_lb_hrs || 0, + part_qty: line.part_qty || 0, + part_type: generatePartType(line.part_type), + act_price: line.act_price || 0 + }; + return ret; +}; + +const generateNullDetailLine = () => { + return { + line_desc: null, + oem_partno: null, + alt_partno: null, + lbr_ty: null, + part_qty: 0, + part_type: null, + act_price: 0 + }; +}; + +const generateAreaOfDamage = (loc) => { + const areaMap = { + "01": "Right Front Corner", + "02": "Right Front Side", + "03": "Right Side", + "04": "Right Rear Side", + "05": "Right Rear Corner", + "06": "Rear", + "07": "Left Rear Corner", + "08": "Left Rear Side", + "09": "Left Side", + 10: "Left Front Side", + 11: "Left Front Corner", + 12: "Front", + 13: "Rollover", + 14: "Uknown", + 15: "Total Loss", + 16: "Non-Collision", + 19: "All Over", + 25: "Hood", + 26: "Deck Lid", + 27: "Roof", + 28: "Undercarriage", + 34: "All Over" + }; + return areaMap[loc] || null; +}; + +const generateLaborType = (type) => { + const laborTypeMap = { + laa: "Aluminum", + lab: "Body", + lad: "Diagnostic", + lae: "Electrical", + laf: "Frame", + lag: "Glass", + lam: "Mechanical", + lar: "Refinish", + las: "Structural", + lau: "Other - LAU", + la1: "Other - LA1", + la2: "Other - LA2", + la3: "Other - LA3", + la4: "Other - LA4", + null: "Other", + mapa: "Paint Materials", + mash: "Shop Materials", + rates_subtotal: "Labor Total", + "timetickets.labels.shift": "Shift", + "timetickets.labels.amshift": "Morning Shift", + "timetickets.labels.ambreak": "Morning Break", + "timetickets.labels.pmshift": "Afternoon Shift", + "timetickets.labels.pmbreak": "Afternoon Break", + "timetickets.labels.lunch": "Lunch" + }; + + return laborTypeMap[type?.toLowerCase()] || null; +}; + +const generatePartType = (type) => { + const partTypeMap = { + paa: "Aftermarket", + pae: "Existing", + pag: "Glass", + pal: "LKQ", + pan: "OEM", + pao: "Other", + pas: "Sublet", + pasl: "Sublet", + ccc: "CC Cleaning", + ccd: "CC Damage Waiver", + ccdr: "CC Daily Rate", + ccf: "CC Refuel", + ccm: "CC Mileage", + prt_dsmk_total: "Line Item Adjustment" + }; + + return partTypeMap[type?.toLowerCase()] || null; +}; + +const errorCode = ({ count, filename, results }) => { + if (count === 0) return 1; + if (!filename) return 3; + const sftpErrorCode = results?.sftpError?.code; + if (sftpErrorCode && ["ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT", "ECONNRESET"].includes(sftpErrorCode)) { + return 4; + } + if (sftpErrorCode) return 7; + return 0; +}; + + +module.exports = { + default: carfaxExportRps, + ftpSetup +} \ No newline at end of file diff --git a/server/data/carfax.js b/server/data/carfax.js index 34a145dda..941ee615b 100644 --- a/server/data/carfax.js +++ b/server/data/carfax.js @@ -24,7 +24,7 @@ const ftpSetup = { debug: process.env.NODE_ENV !== "production" ? (message, ...data) => logger.log(message, "DEBUG", "api", null, data) - : () => {}, + : () => { }, algorithms: { serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"] } @@ -61,7 +61,7 @@ const uploadToS3 = (jsonObj) => { }); }; -exports.default = async (req, res) => { +const carfaxExport = async (req, res) => { // Only process if in production environment. if (process.env.NODE_ENV !== "production") { return res.sendStatus(403); @@ -132,9 +132,9 @@ async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDat ...(ignoreDateFilter ? {} : { - start: start ? moment(start).startOf("day") : moment().subtract(7, "days").startOf("day"), - ...(end && { end: moment(end).endOf("day") }) - }) + start: start ? moment(start).startOf("day") : moment().subtract(7, "days").startOf("day"), + ...(end && { end: moment(end).endOf("day") }) + }) }); const carfaxObject = { @@ -295,18 +295,18 @@ const CreateRepairOrderTag = (job, errorCallback) => { date_estimated: [job.date_estimated, job.created_at].find((date) => date) ? moment([job.date_open, job.created_at].find((date) => date)) - .tz(job.bodyshop.timezone) - .format(AHDateFormat) + .tz(job.bodyshop.timezone) + .format(AHDateFormat) : "", data_opened: [job.date_open, job.created_at].find((date) => date) ? moment([job.date_open, job.created_at].find((date) => date)) - .tz(job.bodyshop.timezone) - .format(AHDateFormat) + .tz(job.bodyshop.timezone) + .format(AHDateFormat) : "", date_invoiced: [job.date_invoiced, job.actual_delivery, job.actual_completion].find((date) => date) ? moment([job.date_invoiced, job.actual_delivery, job.actual_completion].find((date) => date)) - .tz(job.bodyshop.timezone) - .format(AHDateFormat) + .tz(job.bodyshop.timezone) + .format(AHDateFormat) : "", loss_date: job.loss_date ? moment(job.loss_date).format(AHDateFormat) : "", @@ -447,3 +447,9 @@ const errorCode = ({ count, filename, results }) => { if (sftpErrorCode) return 7; return 0; }; + + +module.exports = { + default: carfaxExport, + ftpSetup, uploadToS3 +} \ No newline at end of file diff --git a/server/data/data.js b/server/data/data.js index e9b80de5a..0aa7f6e34 100644 --- a/server/data/data.js +++ b/server/data/data.js @@ -7,4 +7,5 @@ exports.usageReport = require("./usageReport").default; exports.podium = require("./podium").default; exports.emsUpload = require("./emsUpload").default; exports.carfax = require("./carfax").default; +exports.carfaxRps = require("./carfax-rps").default; exports.vehicletype = require("./vehicletype/vehicletype").default; \ No newline at end of file diff --git a/server/graphql-client/graphql-client.js b/server/graphql-client/graphql-client.js index 79d86315b..2c42bf361 100644 --- a/server/graphql-client/graphql-client.js +++ b/server/graphql-client/graphql-client.js @@ -1,3 +1,5 @@ +const logger = require("../utils/logger"); + const GraphQLClient = require("graphql-request").GraphQLClient; //New bug introduced with Graphql Request. @@ -11,9 +13,24 @@ const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { } }); +const rpsClient = + process.env.RPS_GRAPHQL_ENDPOINT && process.env.RPS_HASURA_ADMIN_SECRET ? + new GraphQLClient(process.env.RPS_GRAPHQL_ENDPOINT, { + headers: { + "x-hasura-admin-secret": process.env.RPS_HASURA_ADMIN_SECRET + } + }) : null; + +if (!rpsClient) { + //System log to disable RPS functions + logger.log(`RPS secrets are not set. Client is not configured.`, "WARN", "redis", "api", { + }); +} + const unauthorizedClient = new GraphQLClient(process.env.GRAPHQL_ENDPOINT); module.exports = { client, + rpsClient, unauthorizedClient }; diff --git a/server/routes/dataRoutes.js b/server/routes/dataRoutes.js index 8e7bc04fd..c72a2a502 100644 --- a/server/routes/dataRoutes.js +++ b/server/routes/dataRoutes.js @@ -1,6 +1,6 @@ const express = require("express"); const router = express.Router(); -const { autohouse, claimscorp, chatter, kaizen, usageReport, podium, carfax } = require("../data/data"); +const { autohouse, claimscorp, chatter, kaizen, usageReport, podium, carfax, carfaxRps } = require("../data/data"); router.post("/ah", autohouse); router.post("/cc", claimscorp); @@ -9,5 +9,6 @@ router.post("/kaizen", kaizen); router.post("/usagereport", usageReport); router.post("/podium", podium); router.post("/carfax", carfax); +router.post("/carfaxrps", carfaxRps); module.exports = router; From 9e977d9a58dc4cb7f19ae8220231e5ef076434be Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Thu, 2 Oct 2025 18:34:40 -0700 Subject: [PATCH 2/3] IO-3386 CARFAX RPS Signed-off-by: Allan Carr --- docker-compose-cluster.yml | 3 + docker-compose.yml | 2 + hasura/metadata/cron_triggers.yaml | 11 ++- server/data/carfax-rps.js | 144 +++++++++++++++++------------ server/data/carfax.js | 34 +++---- server/email/sendemail.js | 6 +- server/graphql-client/queries.js | 42 +++++++++ 7 files changed, 163 insertions(+), 79 deletions(-) diff --git a/docker-compose-cluster.yml b/docker-compose-cluster.yml index 86defaf50..bbce31dd4 100644 --- a/docker-compose-cluster.yml +++ b/docker-compose-cluster.yml @@ -207,6 +207,9 @@ services: aws --endpoint-url=http://localstack:4566 secretsmanager create-secret --name CHATTER_PRIVATE_KEY --secret-string file:///tmp/certs/io-ftp-test.key aws --endpoint-url=http://localstack:4566 logs create-log-group --log-group-name development --region ca-central-1 aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-large-log --create-bucket-configuration LocationConstraint=ca-central-1 + aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1 + aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket rome-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1 + aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket rps-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1 " networks: diff --git a/docker-compose.yml b/docker-compose.yml index ad805e272..261076522 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -120,6 +120,8 @@ services: aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-job-totals --create-bucket-configuration LocationConstraint=ca-central-1 aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket parts-estimates --create-bucket-configuration LocationConstraint=ca-central-1 aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1 + aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket rome-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1 + aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket rps-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1 " # Node App: The Main IMEX API node-app: diff --git a/hasura/metadata/cron_triggers.yaml b/hasura/metadata/cron_triggers.yaml index a1b4b7f4a..2c8c4c91d 100644 --- a/hasura/metadata/cron_triggers.yaml +++ b/hasura/metadata/cron_triggers.yaml @@ -8,7 +8,16 @@ value_from_env: DATAPUMP_AUTH - name: CARFAX Data Pump webhook: '{{HASURA_API_URL}}/data/carfax' - schedule: 0 7 * * 6 + schedule: 0 7 * * 0 + include_in_metadata: true + payload: {} + headers: + - name: x-imex-auth + value_from_env: DATAPUMP_AUTH + comment: Project Mexico +- name: CARFAX RPS Data Pump + webhook: '{{HASURA_API_URL}}/data/carfaxrps' + schedule: 15 7 * * 0 include_in_metadata: true payload: {} headers: diff --git a/server/data/carfax-rps.js b/server/data/carfax-rps.js index e8c43f7ae..9455ee7fe 100644 --- a/server/data/carfax-rps.js +++ b/server/data/carfax-rps.js @@ -1,24 +1,20 @@ const queries = require("../graphql-client/queries"); -const Dinero = require("dinero.js"); const moment = require("moment-timezone"); const logger = require("../utils/logger"); -const InstanceManager = require("../utils/instanceMgr").default; -const { isString, isEmpty } = require("lodash"); const fs = require("fs"); const client = require("../graphql-client/graphql-client").rpsClient; const { sendServerEmail, sendMexicoBillingEmail } = require("../email/sendemail"); -const { uploadFileToS3 } = require("../utils/s3"); const crypto = require("crypto"); -const { ftpSetup, uploadToS3 } = require("./carfax") +const { ftpSetup, uploadToS3 } = require("./carfax"); let Client = require("ssh2-sftp-client"); const AHDateFormat = "YYYY-MM-DD"; const NON_ASCII_REGEX = /[^\x20-\x7E]/g; -const carfaxExportRps = async (req, res) => { - const { bodyshops } = await client.request(queries.GET_CARFAX_SHOPS); //Query for the List of Bodyshop Clients. +const S3_BUCKET_NAME = "rps-carfax-uploads"; +const carfaxExportRps = async (req, res) => { // Only process if in production environment. if (process.env.NODE_ENV !== "production") { return res.sendStatus(403); @@ -40,7 +36,7 @@ const carfaxExportRps = async (req, res) => { const allXMLResults = []; const allErrors = []; - const { bodyshops } = await client.request(queries.GET_CARFAX_SHOPS); //Query for the List of Bodyshop Clients. + const { bodyshops } = await client.request(queries.GET_CARFAX_RPS_SHOPS); //Query for the List of Bodyshop Clients. const specificShopIds = req.body.bodyshopIds; // ['uuid]; const { start, end, skipUpload, ignoreDateFilter } = req.body; //YYYY-MM-DD @@ -56,7 +52,7 @@ const carfaxExportRps = async (req, res) => { await processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allXMLResults, allErrors); await sendServerEmail({ - subject: `Project Mexico Report ${moment().format("MM-DD-YY")}`, + subject: `Project Mexico RPS Report ${moment().format("MM-DD-YY")}`, text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify( allXMLResults.map((x) => ({ imexshopid: x.imexshopid, @@ -77,21 +73,25 @@ const carfaxExportRps = async (req, res) => { async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allXMLResults, allErrors) { for (const bodyshop of shopsToProcess) { - const shopid = bodyshop.imexshopid?.toLowerCase() || bodyshop.shopname.replace(/[^a-zA-Z0-9]/g, "").toLowerCase(); + const shopid = bodyshop.shopname.replace(/[^a-zA-Z0-9]/g, "").toLowerCase(); const erroredJobs = []; try { logger.log("CARFAX-RPS-start-shop-extract", "DEBUG", "api", bodyshop.id, { shopname: bodyshop.shopname }); - const { jobs, bodyshops_by_pk } = await client.request(queries.CARFAX_QUERY, { + const { jobs, bodyshops_by_pk } = await client.request(queries.CARFAX_RPS_QUERY, { bodyshopid: bodyshop.id, ...(ignoreDateFilter ? {} : { - start: start ? moment(start).startOf("day") : moment().subtract(7, "days").startOf("day"), - ...(end && { end: moment(end).endOf("day") }) - }) + starttz: start ? moment(start).startOf("day") : moment().subtract(7, "days").startOf("day"), + ...(end && { endtz: moment(end).endOf("day") }), + start: start + ? moment(start).startOf("day").format(AHDateFormat) + : moment().subtract(7, "days").startOf("day").format(AHDateFormat), + ...(end && { endtz: moment(end).endOf("day").format(AHDateFormat) }) + }) }); const carfaxObject = { @@ -107,7 +107,7 @@ async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDat if (erroredJobs.length > 0) { logger.log("CARFAX-RPS-failed-jobs", "ERROR", "api", bodyshop.id, { count: erroredJobs.length, - jobs: JSON.stringify(erroredJobs.map((j) => j.job.ro_number)) + jobs: JSON.stringify(erroredJobs.map((j) => j.job.id)) }); } @@ -121,18 +121,14 @@ async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDat if (skipUpload) { fs.writeFileSync(`./logs/${jsonObj.filename}`, jsonObj.json); - uploadToS3(jsonObj); + uploadToS3(jsonObj, S3_BUCKET_NAME); } else { await uploadViaSFTP(jsonObj); await sendMexicoBillingEmail({ - subject: `${shopid.replace(/_/g, "").toUpperCase()}_Mexico${InstanceManager({ - imex: "IO", - rome: "RO" - })}_${moment().format("MMDDYYYY")} ROs ${jsonObj.count} Error ${errorCode(jsonObj)}`, + subject: `${shopid.replace(/_/g, "").toUpperCase()}_MexicoRPS_${moment().format("MMDDYYYY")} ROs ${jsonObj.count} Error ${errorCode(jsonObj)}`, text: `Errors:\n${JSON.stringify( erroredJobs.map((ej) => ({ - ro_number: ej.job?.ro_number, jobid: ej.job?.id, error: ej.error })), @@ -180,7 +176,6 @@ async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDat imexshopid: shopid, CARFAXid: bodyshop.CARFAXid, errors: erroredJobs.map((ej) => ({ - ro_number: ej.job?.ro_number, jobid: ej.job?.id, error: ej.error })) @@ -199,7 +194,7 @@ async function uploadViaSFTP(jsonObj) { ); try { // Upload to S3 first. - uploadToS3(jsonObj); + uploadToS3(jsonObj, S3_BUCKET_NAME); //Connect to the FTP and upload all. await sftp.connect(ftpSetup); @@ -220,7 +215,10 @@ async function uploadViaSFTP(jsonObj) { throw error; } } catch (error) { - logger.log("CARFAX-RPS-sftp-error", "ERROR", "api", jsonObj.bodyshopid, { error: error.message, stack: error.stack }); + logger.log("CARFAX-RPS-sftp-error", "ERROR", "api", jsonObj.bodyshopid, { + error: error.message, + stack: error.stack + }); throw error; } finally { sftp.end(); @@ -228,42 +226,27 @@ async function uploadViaSFTP(jsonObj) { } const CreateRepairOrderTag = (job, errorCallback) => { - if (!job.job_totals) { - errorCallback({ - jobid: job.id, - job: job, - ro_number: job.ro_number, - error: { toString: () => "No job totals for RO." } - }); - return {}; - } - try { + const subtotalEntry = job.totals.find((total) => total.TTL_TYPECD === ""); + const subtotal = subtotalEntry ? subtotalEntry.T_AMT : 0; + const ret = { - ro_number: crypto.createHash("md5").update(job.ro_number, "utf8").digest("hex"), + ro_number: crypto.createHash("md5").update(job.id, "utf8").digest("hex"), v_vin: job.v_vin || "", v_year: job.v_model_yr ? parseInt(job.v_model_yr.match(/\d/g)) ? parseInt(job.v_model_yr.match(/\d/g).join(""), 10) : "" : "", - v_make: job.v_make_desc || "", - v_model: job.v_model_desc || "", + v_make: job.v_makedesc || "", + v_model: job.v_model || "", - date_estimated: [job.date_estimated, job.created_at].find((date) => date) - ? moment([job.date_open, job.created_at].find((date) => date)) - .tz(job.bodyshop.timezone) - .format(AHDateFormat) - : "", - data_opened: [job.date_open, job.created_at].find((date) => date) - ? moment([job.date_open, job.created_at].find((date) => date)) - .tz(job.bodyshop.timezone) - .format(AHDateFormat) - : "", - date_invoiced: [job.date_invoiced, job.actual_delivery, job.actual_completion].find((date) => date) - ? moment([job.date_invoiced, job.actual_delivery, job.actual_completion].find((date) => date)) - .tz(job.bodyshop.timezone) - .format(AHDateFormat) + date_estimated: moment(job.created_at).tz("America/Winnipeg").format(AHDateFormat) || "", + data_opened: moment(job.created_at).tz("America/Winnipeg").format(AHDateFormat) || "", + date_invoiced: [job.close_date, job.created_at].find((date) => date) + ? moment([job.close_date, job.created_at].find((date) => date)) + .tz("America/Winnipeg") + .format(AHDateFormat) : "", loss_date: job.loss_date ? moment(job.loss_date).format(AHDateFormat) : "", @@ -271,11 +254,12 @@ const CreateRepairOrderTag = (job, errorCallback) => { loss_desc: job.loss_desc || "", theft_ind: job.theft_ind, tloss_ind: job.tlos_ind, - subtotal: Dinero(job.job_totals.totals.subtotal).toUnit(), + + subtotal: subtotal, areaofdamage: { - impact1: generateAreaOfDamage(job.area_of_damage?.impact1 || ""), - impact2: generateAreaOfDamage(job.area_of_damage?.impact2 || "") + impact1: generateAreaOfDamage(job.impact_1 || ""), + impact2: generateAreaOfDamage(job.impact_2 || "") }, jobLines: job.joblines.length > 0 ? job.joblines.map((jl) => GenerateDetailLines(jl)) : [generateNullDetailLine()] @@ -283,7 +267,7 @@ const CreateRepairOrderTag = (job, errorCallback) => { return ret; } catch (error) { logger.log("CARFAX-RPS-job-data-error", "ERROR", "api", null, { error: error.message, stack: error.stack }); - errorCallback({ jobid: job.id, ro_number: job.ro_number, error }); + errorCallback({ jobid: job.id, error }); } }; @@ -292,7 +276,7 @@ const GenerateDetailLines = (line) => { line_desc: line.line_desc ? line.line_desc.replace(NON_ASCII_REGEX, "") : null, oem_partno: line.oem_partno ? line.oem_partno.replace(NON_ASCII_REGEX, "") : null, alt_partno: line.alt_partno ? line.alt_partno.replace(NON_ASCII_REGEX, "") : null, - op_code_desc: line.op_code_desc ? line.op_code_desc.replace(NON_ASCII_REGEX, "") : null, + op_code_desc: generateOpCodeDescription(line.lbr_op), lbr_ty: generateLaborType(line.mod_lbr_ty), lbr_hrs: line.mod_lb_hrs || 0, part_qty: line.part_qty || 0, @@ -394,6 +378,51 @@ const generatePartType = (type) => { return partTypeMap[type?.toLowerCase()] || null; }; +const generateOpCodeDescription = (type) => { + const opCodeMap = { + OP0: "REMOVE / REPLACE PARTIAL", + OP1: "REFINISH / REPAIR", + OP10: "REPAIR , PARTIAL", + OP100: "REPLACE PRE-PRICED", + OP101: "REMOVE/REPLACE RECYCLED PART", + OP103: "REMOVE / REPLACE PARTIAL", + OP104: "REMOVE / REPLACE PARTIAL LABOUR", + OP105: "!!ADJUST MANUALLY!!", + OP106: "REPAIR , PARTIAL", + OP107: "CHIPGUARD", + OP108: "MULTI TONE", + OP109: "REPLACE PRE-PRICED", + OP11: "REMOVE / REPLACE", + OP110: "REFINISH / REPAIR", + OP111: "REMOVE / REPLACE", + OP112: "REMOVE / REPLACE", + OP113: "REPLACE PRE-PRICED", + OP114: "REPLACE PRE-PRICED", + OP12: "REMOVE / REPLACE PARTIAL", + OP120: "REPAIR , PARTIAL", + OP13: "ADDITIONAL COSTS", + OP14: "ADDITIONAL OPERATIONS", + OP15: "BLEND", + OP16: "SUBLET", + OP17: "POLICY LIMIT ADJUSTMENT", + OP18: "APPEAR ALLOWANCE", + OP2: "REMOVE / INSTALL", + OP24: "CHIPGUARD", + OP25: "TWO TONE", + OP26: "PAINTLESS DENT REPAIR", + OP260: "SUBLET", + OP3: "ADDITIONAL LABOR", + OP4: "ALIGNMENT", + OP5: "OVERHAUL", + OP6: "REFINISH", + OP7: "INSPECT", + OP8: "CHECK / ADJUST", + OP9: "REPAIR" + }; + + return opCodeMap[type?.toUpperCase()] || null; +}; + const errorCode = ({ count, filename, results }) => { if (count === 0) return 1; if (!filename) return 3; @@ -405,8 +434,7 @@ const errorCode = ({ count, filename, results }) => { return 0; }; - module.exports = { default: carfaxExportRps, ftpSetup -} \ No newline at end of file +}; diff --git a/server/data/carfax.js b/server/data/carfax.js index 941ee615b..dbbcf3464 100644 --- a/server/data/carfax.js +++ b/server/data/carfax.js @@ -24,7 +24,7 @@ const ftpSetup = { debug: process.env.NODE_ENV !== "production" ? (message, ...data) => logger.log(message, "DEBUG", "api", null, data) - : () => { }, + : () => {}, algorithms: { serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"] } @@ -37,12 +37,12 @@ const S3_BUCKET_NAME = InstanceManager({ const region = InstanceManager.InstanceRegion; const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME); -const uploadToS3 = (jsonObj) => { +const uploadToS3 = (jsonObj, bucketName = S3_BUCKET_NAME) => { const webPath = isLocal - ? `https://${S3_BUCKET_NAME}.s3.localhost.localstack.cloud:4566/${jsonObj.filename}` - : `https://${S3_BUCKET_NAME}.s3.${region}.amazonaws.com/${jsonObj.filename}`; + ? `https://${bucketName}.s3.localhost.localstack.cloud:4566/${jsonObj.filename}` + : `https://${bucketName}.s3.${region}.amazonaws.com/${jsonObj.filename}`; - uploadFileToS3({ bucketName: S3_BUCKET_NAME, key: jsonObj.filename, content: jsonObj.json }) + uploadFileToS3({ bucketName: bucketName, key: jsonObj.filename, content: jsonObj.json }) .then(() => { logger.log("CARFAX-s3-upload", "DEBUG", "api", jsonObj.bodyshopid, { imexshopid: jsonObj.imexshopid, @@ -132,9 +132,9 @@ async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDat ...(ignoreDateFilter ? {} : { - start: start ? moment(start).startOf("day") : moment().subtract(7, "days").startOf("day"), - ...(end && { end: moment(end).endOf("day") }) - }) + start: start ? moment(start).startOf("day") : moment().subtract(7, "days").startOf("day"), + ...(end && { end: moment(end).endOf("day") }) + }) }); const carfaxObject = { @@ -295,18 +295,18 @@ const CreateRepairOrderTag = (job, errorCallback) => { date_estimated: [job.date_estimated, job.created_at].find((date) => date) ? moment([job.date_open, job.created_at].find((date) => date)) - .tz(job.bodyshop.timezone) - .format(AHDateFormat) + .tz(job.bodyshop.timezone) + .format(AHDateFormat) : "", data_opened: [job.date_open, job.created_at].find((date) => date) ? moment([job.date_open, job.created_at].find((date) => date)) - .tz(job.bodyshop.timezone) - .format(AHDateFormat) + .tz(job.bodyshop.timezone) + .format(AHDateFormat) : "", date_invoiced: [job.date_invoiced, job.actual_delivery, job.actual_completion].find((date) => date) ? moment([job.date_invoiced, job.actual_delivery, job.actual_completion].find((date) => date)) - .tz(job.bodyshop.timezone) - .format(AHDateFormat) + .tz(job.bodyshop.timezone) + .format(AHDateFormat) : "", loss_date: job.loss_date ? moment(job.loss_date).format(AHDateFormat) : "", @@ -448,8 +448,8 @@ const errorCode = ({ count, filename, results }) => { return 0; }; - module.exports = { default: carfaxExport, - ftpSetup, uploadToS3 -} \ No newline at end of file + ftpSetup, + uploadToS3 +}; diff --git a/server/email/sendemail.js b/server/email/sendemail.js index c9a72c17f..f953f1bf0 100644 --- a/server/email/sendemail.js +++ b/server/email/sendemail.js @@ -68,7 +68,7 @@ const sendServerEmail = async ({ subject, text }) => { }, // eslint-disable-next-line no-unused-vars (err, info) => { - logger.log("server-email-failure", err ? "error" : "debug", null, null, { + logger.log("server-email-send", err ? "error" : "debug", null, null, { message: err?.message, stack: err?.stack }); @@ -103,7 +103,7 @@ const sendMexicoBillingEmail = async ({ subject, text }) => { }, // eslint-disable-next-line no-unused-vars (err, info) => { - logger.log("server-email-failure", err ? "error" : "debug", null, null, { + logger.log("server-email-send", err ? "error" : "debug", null, null, { message: err?.message, stack: err?.stack }); @@ -258,7 +258,7 @@ const sendTaskEmail = async ({ to, subject, type = "text", html, text, attachmen // eslint-disable-next-line no-unused-vars (err, info) => { // (message, type, user, record, meta - logger.log("server-email", err ? "error" : "debug", null, null, { message: err?.message, stack: err?.stack }); + logger.log("server-email-send", err ? "error" : "debug", null, null, { message: err?.message, stack: err?.stack }); } ); } catch (error) { diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index 9a3bb1d6e..d6ebfce64 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -919,6 +919,41 @@ exports.CARFAX_QUERY = `query CARFAX_EXPORT($start: timestamptz, $bodyshopid: uu } }`; +exports.CARFAX_RPS_QUERY = `query CARFAX_RPS_EXPORT($starttz: timestamptz, $endtz: timestamptz,$start: date, $end: date, $bodyshopid: uuid!) { + bodyshops_by_pk(id: $bodyshopid) { + id + shopname + } + jobs(where: {_and: [{_or: [{close_date: {_gt: $start, _lte: $end}}, {created_at: {_gt: $starttz, _lte: $endtz}, close_date: {_is_null: true}}]}, {_not: {_and: [{close_date: {_is_null: true}}, {created_at: {_is_null: true}}]}}, {bodyshopid: {_eq: $bodyshopid}}, {v_vin: {_is_null: false}}]}) { + close_date + created_at + id + ins_co_nm + impact_1 + impact_2 + joblines { + act_price + alt_partno + line_desc + mod_lb_hrs + mod_lbr_ty + oem_partno + lbr_op + part_type + part_qty + } + loss_date + loss_desc + theft_ind + tlos_ind + totals + v_makedesc + v_model + v_model_yr + v_vin + } +}`; + exports.CLAIMSCORP_QUERY = `query CLAIMSCORP_EXPORT($start: timestamptz, $bodyshopid: uuid!, $end: timestamptz) { bodyshops_by_pk(id: $bodyshopid){ id @@ -1865,6 +1900,13 @@ exports.GET_CARFAX_SHOPS = `query GET_CARFAX_SHOPS { } }`; +exports.GET_CARFAX_RPS_SHOPS = `query GET_CARFAX_RPS_SHOPS { + bodyshops(where: {carfax_exclude: {_neq: "true"}}){ + id + shopname + } +}`; + exports.GET_CLAIMSCORP_SHOPS = `query GET_CLAIMSCORP_SHOPS { bodyshops(where: {claimscorpid: {_is_null: false}, _or: {claimscorpid: {_neq: ""}}}){ id From 4899297539dc123304331f98838f80484da36dc1 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 3 Oct 2025 11:24:10 -0700 Subject: [PATCH 3/3] IO-3386 CARFAX RPS Add Brad Rhoades to Report Signed-off-by: Allan Carr --- server/data/carfax-rps.js | 15 ++++++++------- server/data/carfax.js | 15 ++++++++------- server/email/sendemail.js | 15 ++++++++++++--- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/server/data/carfax-rps.js b/server/data/carfax-rps.js index 9455ee7fe..d6065df57 100644 --- a/server/data/carfax-rps.js +++ b/server/data/carfax-rps.js @@ -33,7 +33,7 @@ const carfaxExportRps = async (req, res) => { try { logger.log("CARFAX-RPS-start", "DEBUG", "api", null, null); - const allXMLResults = []; + const allJSONResults = []; const allErrors = []; const { bodyshops } = await client.request(queries.GET_CARFAX_RPS_SHOPS); //Query for the List of Bodyshop Clients. @@ -49,12 +49,12 @@ const carfaxExportRps = async (req, res) => { return; } - await processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allXMLResults, allErrors); + await processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allJSONResults, allErrors); await sendServerEmail({ subject: `Project Mexico RPS Report ${moment().format("MM-DD-YY")}`, - text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify( - allXMLResults.map((x) => ({ + text: `Total Count: ${allJSONResults.reduce((a, v) => a + v.count, 0)}\nErrors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify( + allJSONResults.map((x) => ({ imexshopid: x.imexshopid, filename: x.filename, count: x.count, @@ -62,7 +62,8 @@ const carfaxExportRps = async (req, res) => { })), null, 2 - )}` + )}`, + to: ["bradley.rhoades@convenient-brands.com"] }); logger.log("CARFAX-RPS-end", "DEBUG", "api", null, null); @@ -71,7 +72,7 @@ const carfaxExportRps = async (req, res) => { } }; -async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allXMLResults, allErrors) { +async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allJSONResults, allErrors) { for (const bodyshop of shopsToProcess) { const shopid = bodyshop.shopname.replace(/[^a-zA-Z0-9]/g, "").toLowerCase(); const erroredJobs = []; @@ -148,7 +149,7 @@ async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDat }); } - allXMLResults.push({ + allJSONResults.push({ bodyshopid: bodyshop.id, imexshopid: shopid, count: jsonObj.count, diff --git a/server/data/carfax.js b/server/data/carfax.js index dbbcf3464..f2ff0bac2 100644 --- a/server/data/carfax.js +++ b/server/data/carfax.js @@ -80,7 +80,7 @@ const carfaxExport = async (req, res) => { try { logger.log("CARFAX-start", "DEBUG", "api", null, null); - const allXMLResults = []; + const allJSONResults = []; const allErrors = []; const { bodyshops } = await client.request(queries.GET_CARFAX_SHOPS); //Query for the List of Bodyshop Clients. @@ -96,12 +96,12 @@ const carfaxExport = async (req, res) => { return; } - await processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allXMLResults, allErrors); + await processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allJSONResults, allErrors); await sendServerEmail({ subject: `Project Mexico Report ${moment().format("MM-DD-YY")}`, - text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify( - allXMLResults.map((x) => ({ + text: `Total Count: ${allJSONResults.reduce((a, v) => a + v.count, 0)}\nErrors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify( + allJSONResults.map((x) => ({ imexshopid: x.imexshopid, filename: x.filename, count: x.count, @@ -109,7 +109,8 @@ const carfaxExport = async (req, res) => { })), null, 2 - )}` + )}`, + to: ["bradley.rhoades@convenient-brands.com"] }); logger.log("CARFAX-end", "DEBUG", "api", null, null); @@ -118,7 +119,7 @@ const carfaxExport = async (req, res) => { } }; -async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allXMLResults, allErrors) { +async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allJSONResults, allErrors) { for (const bodyshop of shopsToProcess) { const shopid = bodyshop.imexshopid?.toLowerCase() || bodyshop.shopname.replace(/[^a-zA-Z0-9]/g, "").toLowerCase(); const erroredJobs = []; @@ -195,7 +196,7 @@ async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDat }); } - allXMLResults.push({ + allJSONResults.push({ bodyshopid: bodyshop.id, imexshopid: shopid, count: jsonObj.count, diff --git a/server/email/sendemail.js b/server/email/sendemail.js index f953f1bf0..864031462 100644 --- a/server/email/sendemail.js +++ b/server/email/sendemail.js @@ -44,8 +44,14 @@ const logEmail = async (req, email) => { } }; -const sendServerEmail = async ({ subject, text }) => { +const sendServerEmail = async ({ subject, text, to = [] }) => { if (process.env.NODE_ENV === undefined) return; + + let sentTo = ["support@imexsystems.ca"]; + if (to?.length) { + sentTo = [...sentTo, ...to]; + } + try { mailer.sendMail( { @@ -53,7 +59,7 @@ const sendServerEmail = async ({ subject, text }) => { imex: `ImEX Online API - ${process.env.NODE_ENV} `, rome: `Rome Online API - ${process.env.NODE_ENV} ` }), - to: ["support@thinkimex.com"], + to: sentTo, subject: subject, text: text, ses: { @@ -258,7 +264,10 @@ const sendTaskEmail = async ({ to, subject, type = "text", html, text, attachmen // eslint-disable-next-line no-unused-vars (err, info) => { // (message, type, user, record, meta - logger.log("server-email-send", err ? "error" : "debug", null, null, { message: err?.message, stack: err?.stack }); + logger.log("server-email-send", err ? "error" : "debug", null, null, { + message: err?.message, + stack: err?.stack + }); } ); } catch (error) {