const path = require("path"); require("dotenv").config({ path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) }); const queries = require("../graphql-client/queries"); const Dinero = require("dinero.js"); const qs = require("query-string"); const axios = require("axios"); 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 client = new SecretsManagerClient({ region: InstanceRegion() }); const gqlClient = require("../graphql-client/graphql-client").client; const getShopCredentials = async (bodyshop) => { // Development only if (process.env.NODE_ENV === undefined) { return { merchantkey: process.env.INTELLIPAY_MERCHANTKEY, apikey: process.env.INTELLIPAY_APIKEY }; } // Production code if (bodyshop?.imexshopid) { try { const secret = await client.send( new GetSecretValueCommand({ SecretId: `intellipay-credentials-${bodyshop.imexshopid}`, VersionStage: "AWSCURRENT" // VersionStage defaults to AWSCURRENT if unspecified }) ); return JSON.parse(secret.SecretString); } catch (error) { return { error: error.message }; } } }; const decodeComment = (comment) => { try { return comment ? JSON.parse(Buffer.from(comment, "base64").toString()) : null; } catch (error) { return null; // Handle malformed base64 string gracefully } }; exports.lightbox_credentials = async (req, res) => { const decodedComment = decodeComment(req.body?.comment); const logMeta = { iPayData: req.body?.iPayData, decodedComment, bodyshop: { id: req.body?.bodyshop?.id, imexshopid: req.body?.bodyshop?.imexshopid, name: req.body?.bodyshop?.shopname } }; logger.log("intellipay-lightbox-credentials", "DEBUG", req.user?.email, null, logMeta); const shopCredentials = await getShopCredentials(req.body.bodyshop); if (shopCredentials.error) { logger.log("intellipay-credentials-error", "ERROR", req.user?.email, null, { message: shopCredentials.error?.message, ...logMeta }); res.json({ message: shopCredentials.error?.message, type: "intellipay-credentials-error", ...logMeta }); return; } try { const options = { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, data: qs.stringify({ ...shopCredentials, operatingenv: "businessattended" }), url: `https://${domain}.cpteller.com/api/custapi.cfc?method=autoterminal${req.body.refresh ? "_refresh" : ""}` //autoterminal_refresh }; const response = await axios(options); logger.log("intellipay-lightbox-success", "DEBUG", req.user?.email, null, { requestOptions: options, ...logMeta }); res.send(response.data); } catch (error) { logger.log("intellipay-lightbox-error", "ERROR", req.user?.email, null, { message: error?.message, ...logMeta }); res.json({ message: error?.message, type: "intellipay-lightbox-error", ...logMeta }); } }; exports.payment_refund = async (req, res) => { const decodedComment = decodeComment(req.body.iPayData?.comment); const logResponseMeta = { iPayData: req.body?.iPayData, bodyshop: { id: req.body.bodyshop?.id, imexshopid: req.body.bodyshop?.imexshopid, name: req.body.bodyshop?.shopname }, paymentid: req.body?.paymentid, amount: req.body?.amount, decodedComment }; logger.log("intellipay-refund-request-received", "DEBUG", req.user?.email, null, logResponseMeta); const shopCredentials = await getShopCredentials(req.body.bodyshop); if (shopCredentials.error) { logger.log("intellipay-refund-credentials-error", "ERROR", req.user?.email, null, { credentialsError: shopCredentials.error, ...logResponseMeta }); res.status(400).json({ credentialsError: shopCredentials.error, type: "intellipay-refund-credentials-error", ...logResponseMeta }); return; } try { const options = { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, data: qs.stringify({ method: "payment_refund", ...shopCredentials, paymentid: req.body.paymentid, amount: req.body.amount }), url: `https://${domain}.cpteller.com/api/26/webapi.cfc?method=payment_refund` }; logger.log("intellipay-refund-options-prepared", "DEBUG", req.user?.email, null, { requestOptions: options, ...logResponseMeta }); const response = await axios(options); logger.log("intellipay-refund-success", "DEBUG", req.user?.email, null, { requestOptions: options, ...logResponseMeta }); res.send(response.data); } catch (error) { logger.log("intellipay-refund-error", "ERROR", req.user?.email, null, { message: error?.message, ...logResponseMeta }); res.status(500).json({ message: error?.message, type: "intellipay-refund-error", ...logResponseMeta }); } }; exports.generate_payment_url = async (req, res) => { const decodedComment = decodeComment(req.body.comment); const logResponseMeta = { iPayData: req.body?.iPayData, bodyshop: { id: req.body.bodyshop?.id, imexshopid: req.body.bodyshop?.imexshopid, name: req.body.bodyshop?.shopname }, amount: req.body?.amount, account: req.body?.account, comment: req.body?.comment, invoice: req.body?.invoice, decodedComment }; logger.log("intellipay-generate-payment-url-received", "DEBUG", req.user?.email, null, logResponseMeta); const shopCredentials = await getShopCredentials(req.body.bodyshop); if (shopCredentials.error) { logger.log("intellipay-generate-payment-url-credentials-error", "ERROR", req.user?.email, null, { message: shopCredentials.error?.message, ...logResponseMeta }); res.status(400).json({ message: shopCredentials.error?.message, type: "intellipay-generate-payment-url-credentials-error", ...logResponseMeta }); return; } try { const options = { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, data: qs.stringify({ ...shopCredentials, amount: Dinero({ amount: Math.round(req.body.amount * 100) }).toFormat("0.00"), account: req.body.account, comment: req.body.comment, invoice: req.body.invoice, createshorturl: true }), url: `https://${domain}.cpteller.com/api/custapi.cfc?method=generate_lightbox_url` }; logger.log("intellipay-generate-payment-url-options-prepared", "DEBUG", req.user?.email, null, { requestOptions: options, ...logResponseMeta }); const response = await axios(options); logger.log("intellipay-generate-payment-url-success", "DEBUG", req.user?.email, null, { requestOptions: options, shortUrl: response.data?.shorturl, ...logResponseMeta }); res.send(response.data); } catch (error) { logger.log("intellipay-generate-payment-url-error", "ERROR", req.user?.email, null, { message: error?.message, ...logResponseMeta }); res.status(500).json({ message: error?.message, ...logResponseMeta }); } }; //Reference: https://intellipay.com/dist/webapi26.html#operation/fee exports.checkfee = async (req, res) => { const logResponseMeta = { bodyshop: { id: req.body?.bodyshop?.id, imexshopid: req.body?.bodyshop?.imexshopid, name: req.body?.bodyshop?.shopname, state: req.body?.bodyshop?.state }, amount: req.body?.amount }; logger.log("intellipay-checkfee-request-received", "DEBUG", req.user?.email, null, logResponseMeta); if (!req.body.amount || req.body.amount <= 0) { logger.log("intellipay-checkfee-skip", "DEBUG", req.user?.email, null, { message: "Amount is zero or undefined, skipping fee check.", ...logResponseMeta }); res.json({ fee: 0 }); return; } const shopCredentials = await getShopCredentials(req.body.bodyshop); if (shopCredentials.error) { logger.log("intellipay-checkfee-credentials-error", "ERROR", req.user?.email, null, { message: shopCredentials.error?.message, ...logResponseMeta }); res.status(400).json({ error: shopCredentials.error?.message, ...logResponseMeta }); return; } try { const options = { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, data: qs.stringify( { method: "fee", ...shopCredentials, amount: req.body.amount, paymenttype: `CC`, cardnum: "4111111111111111", // Required for compatibility with API state: req.body.bodyshop?.state && req.body.bodyshop.state.length === 2 ? req.body.bodyshop.state.toUpperCase() : "ZZ" }, { sort: false } // Ensure query string order is preserved ), url: `https://${domain}.cpteller.com/api/26/webapi.cfc` }; logger.log("intellipay-checkfee-options-prepared", "DEBUG", req.user?.email, null, { requestOptions: options, ...logResponseMeta }); const response = await axios(options); if (response.data?.error) { logger.log("intellipay-checkfee-api-error", "ERROR", req.user?.email, null, { message: response.data?.error, ...logResponseMeta }); res.status(400).json({ error: response.data?.error, type: "intellipay-checkfee-api-error", ...logResponseMeta }); } else if (response.data < 0) { logger.log("intellipay-checkfee-negative-fee", "ERROR", req.user?.email, null, { message: "Fee amount returned is negative.", ...logResponseMeta }); res.json({ error: "Fee amount negative. Check API credentials & account configuration.", ...logResponseMeta, type: "intellipay-checkfee-negative-fee" }); } else { logger.log("intellipay-checkfee-success", "DEBUG", req.user?.email, null, { fee: response.data, ...logResponseMeta }); res.json({ fee: response.data, ...logResponseMeta }); } } catch (error) { logger.log("intellipay-checkfee-error", "ERROR", req.user?.email, null, { message: error?.message, ...logResponseMeta }); res.status(500).json({ error: error?.message, logResponseMeta }); } }; exports.postback = async (req, res) => { const { body: values } = req; const decodedComment = decodeComment(values?.comment); const logResponseMeta = { bodyshop: { id: req.body?.bodyshop?.id, imexshopid: req.body?.bodyshop?.imexshopid, name: req.body?.bodyshop?.shopname, state: req.body?.bodyshop?.state }, iprequest: values, decodedComment }; logger.log("intellipay-postback-received", "DEBUG", req.user?.email, null, logResponseMeta); try { if ((!values.invoice || values.invoice === "") && !decodedComment) { //invoice is specified through the pay link. Comment by IO. logger.log("intellipay-postback-ignored", "DEBUG", req.user?.email, null, { message: "No invoice or comment provided", ...logResponseMeta }); res.sendStatus(200); return; } if (decodedComment) { //Shifted the order to have this first to retain backwards compatibility for the old style of short link. //This has been triggered by IO and may have multiple jobs. const parsedComment = decodedComment; logger.log("intellipay-postback-parsed-comment", "DEBUG", req.user?.email, null, { parsedComment, ...logResponseMeta }); //Adding in the user email to the short pay email. //Need to check this to ensure backwards compatibility for clients that don't update. const partialPayments = Array.isArray(parsedComment) ? parsedComment : parsedComment.payments; // Fetch jobs by job IDs const jobs = await gqlClient.request(queries.GET_JOBS_BY_PKS, { ids: partialPayments.map((p) => p.jobid) }); logger.log("intellipay-postback-jobs-fetched", "DEBUG", req.user?.email, null, { jobs, parsedComment, ...logResponseMeta }); // Insert new payments const paymentResult = await gqlClient.request(queries.INSERT_NEW_PAYMENT, { paymentInput: partialPayments.map((p) => ({ amount: p.amount, transactionid: values.authcode, payer: "Customer", type: values.cardtype, jobid: p.jobid, date: moment(Date.now()), payment_responses: { data: { amount: values.total, bodyshopid: jobs.jobs[0].shopid, jobid: p.jobid, declinereason: "Approved", ext_paymentid: values.paymentid, successful: true, response: values } } })) }); logger.log("intellipay-postback-payment-success", "DEBUG", req.user?.email, null, { paymentResult, jobs, parsedComment, ...logResponseMeta }); 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) { logger.log("intellipay-postback-email-error", "ERROR", req.user?.email, null, { message: error.message, jobs, paymentResult, ...logResponseMeta }); } } res.sendStatus(200); } else if (values.invoice) { const job = await gqlClient.request(queries.GET_JOB_BY_PK, { id: values.invoice }); logger.log("intellipay-postback-invoice-job-fetched", "DEBUG", req.user?.email, null, { job, ...logResponseMeta }); const paymentResult = await gqlClient.request(queries.INSERT_NEW_PAYMENT, { paymentInput: { amount: values.total, transactionid: values.authcode, payer: "Customer", type: values.cardtype, jobid: values.invoice, date: moment(Date.now()) } }); logger.log("intellipay-postback-invoice-payment-success", "DEBUG", req.user?.email, null, { paymentResult, ...logResponseMeta }); const responseResults = await gqlClient.request(queries.INSERT_PAYMENT_RESPONSE, { paymentResponse: { amount: values.total, bodyshopid: job.jobs_by_pk.shopid, paymentid: paymentResult.id, jobid: values.invoice, declinereason: "Approved", ext_paymentid: values.paymentid, successful: true, response: values } }); logger.log("intellipay-postback-invoice-response-success", "DEBUG", req.user?.email, null, { responseResults, ...logResponseMeta }); res.sendStatus(200); } } catch (error) { logger.log("intellipay-postback-error", "ERROR", req.user?.email, null, { message: error?.message, ...logResponseMeta }); res.status(400).json({ successful: false, error: error.message, ...logResponseMeta }); } };