diff --git a/client/src/components/card-payment-modal/card-payment-modal.component..jsx b/client/src/components/card-payment-modal/card-payment-modal.component..jsx index fcdbd417c..67fa15e4d 100644 --- a/client/src/components/card-payment-modal/card-payment-modal.component..jsx +++ b/client/src/components/card-payment-modal/card-payment-modal.component..jsx @@ -1,6 +1,6 @@ -import { DeleteFilled, CopyFilled } from "@ant-design/icons"; +import { CopyFilled, DeleteFilled } from "@ant-design/icons"; import { useLazyQuery, useMutation } from "@apollo/client"; -import { Button, Card, Col, Form, Input, Row, Space, Spin, Statistic, message, notification } from "antd"; +import { Button, Card, Col, Form, Input, message, notification, Row, Space, Spin, Statistic } from "antd"; import axios from "axios"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -23,7 +23,14 @@ const mapStateToProps = createStructuredSelector({ }); const mapDispatchToProps = (dispatch) => ({ - insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })), + insertAuditTrail: ({ jobid, operation, type }) => + dispatch( + insertAuditTrail({ + jobid, + operation, + type + }) + ), toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")) }); @@ -39,7 +46,6 @@ const CardPaymentModalComponent = ({ const [form] = Form.useForm(); const [paymentLink, setPaymentLink] = useState(); const [loading, setLoading] = useState(false); - // const [insertPayment] = useMutation(INSERT_NEW_PAYMENT); const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE); const { t } = useTranslation(); @@ -48,24 +54,33 @@ const CardPaymentModalComponent = ({ skip: !context?.jobid }); - //Initialize the intellipay window. + const collectIPayFields = () => { + const iPayFields = document.querySelectorAll(".ipayfield"); + const iPayData = {}; + iPayFields.forEach((field) => { + iPayData[field.dataset.ipayname] = field.value; + }); + return iPayData; + }; + const SetIntellipayCallbackFunctions = () => { console.log("*** Set IntelliPay callback functions."); + window.intellipay.runOnClose(() => { //window.intellipay.initialize(); }); - window.intellipay.runOnApproval(async function (response) { + window.intellipay.runOnApproval(() => { //2024-04-25: Nothing is going to happen here anymore. We'll completely rely on the callback. //Add a slight delay to allow the refetch to properly get the data. setTimeout(() => { - if (actions && actions.refetch && typeof actions.refetch === "function") actions.refetch(); + if (actions?.refetch) actions.refetch(); setLoading(false); toggleModalVisible(); }, 750); }); - window.intellipay.runOnNonApproval(async function (response) { + window.intellipay.runOnNonApproval(async (response) => { // Mutate unsuccessful payment const { payments } = form.getFieldsValue(); @@ -98,16 +113,19 @@ const CardPaymentModalComponent = ({ //Validate try { await form.validateFields(); - } catch (error) { + } catch { setLoading(false); return; } + const iPayData = collectIPayFields(); + try { const response = await axios.post("/intellipay/lightbox_credentials", { bodyshop, refresh: !!window.intellipay, - paymentSplitMeta: form.getFieldsValue() + paymentSplitMeta: form.getFieldsValue(), + iPayData: iPayData }); if (window.intellipay) { @@ -116,8 +134,8 @@ const CardPaymentModalComponent = ({ SetIntellipayCallbackFunctions(); window.intellipay.autoOpen(); } else { - var rg = document.createRange(); - let node = rg.createContextualFragment(response.data); + const rg = document.createRange(); + const node = rg.createContextualFragment(response.data); document.documentElement.appendChild(node); SetIntellipayCallbackFunctions(); window.intellipay.isAutoOpen = true; @@ -137,25 +155,27 @@ const CardPaymentModalComponent = ({ //Validate try { await form.validateFields(); - } catch (error) { + } catch { setLoading(false); return; } + const iPayData = collectIPayFields(); + try { const { payments } = form.getFieldsValue(); const response = await axios.post("/intellipay/generate_payment_url", { bodyshop, - amount: payments?.reduce((acc, val) => { - return acc + (val?.amount || 0); - }, 0), - account: payments && data && data.jobs.length > 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null, + amount: payments.reduce((acc, val) => acc + (val?.amount || 0), 0), + account: payments && data?.jobs?.length > 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null, comment: btoa(JSON.stringify({ payments, userEmail: currentUser.email })), - paymentSplitMeta: form.getFieldsValue() + paymentSplitMeta: form.getFieldsValue(), + iPayData: iPayData }); - if (response.data) { - setPaymentLink(response.data?.shorUrl); - navigator.clipboard.writeText(response.data?.shorUrl); + + if (response?.data?.shorUrl) { + setPaymentLink(response.data.shorUrl); + await navigator.clipboard.writeText(response.data.shorUrl); message.success(t("general.actions.copied")); } setLoading(false); @@ -179,67 +199,44 @@ const CardPaymentModalComponent = ({ }} > - {(fields, { add, remove, move }) => { - return ( -
- {fields.map((field, index) => ( - - - - - - - - - - - - - - { - remove(field.name); - }} - /> - - - - ))} - - + {(fields, { add, remove }) => ( +
+ {fields.map((field, index) => ( + + + + + + + + + + + + + + remove(field.name)} /> + + -
- ); - }} + ))} + + + +
+ )}
{() => { const { payments } = form.getFieldsValue(); - const totalAmountToCharge = payments?.reduce((acc, val) => { - return acc + (val?.amount || 0); - }, 0); + const totalAmountToCharge = payments?.reduce((acc, val) => acc + (val?.amount || 0), 0); return ( diff --git a/server/intellipay/intellipay.js b/server/intellipay/intellipay.js index 4b2f89dcd..c839cb29d 100644 --- a/server/intellipay/intellipay.js +++ b/server/intellipay/intellipay.js @@ -1,4 +1,3 @@ -const GraphQLClient = require("graphql-request").GraphQLClient; const path = require("path"); const queries = require("../graphql-client/queries"); const Dinero = require("dinero.js"); @@ -6,7 +5,6 @@ const qs = require("query-string"); const axios = require("axios"); const moment = require("moment"); const logger = require("../utils/logger"); -const InstanceManager = require("../utils/instanceMgr").default; const { sendTaskEmail } = require("../email/sendemail"); const generateEmailTemplate = require("../email/generateTemplate"); const { getEndpoints } = require("../email/tasksEmails"); @@ -53,14 +51,19 @@ const getShopCredentials = async (bodyshop) => { }; exports.lightbox_credentials = async (req, res) => { - logger.log("intellipay-lightbox-credentials", "DEBUG", req.user?.email, null, null); + logger.log("intellipay-lightbox-credentials", "DEBUG", req.user?.email, null, { + iPayData: req.body.iPayData, + bodyshop: req.body.bodyshop + }); const shopCredentials = await getShopCredentials(req.body.bodyshop); if (shopCredentials.error) { + logger.log("intellipay-credentials-error", "ERROR", req.user?.email, null, { message: shopCredentials.error }); res.json(shopCredentials); return; } + try { const options = { method: "POST", @@ -74,26 +77,39 @@ exports.lightbox_credentials = async (req, res) => { const response = await axios(options); + logger.log("intellipay-lightbox-success", "DEBUG", req.user?.email, null, { + responseData: response.data, + requestOptions: options + }); + res.send(response.data); } catch (error) { - //console.log(error); - logger.log("intellipay-lightbox-credentials-error", "ERROR", req.user?.email, null, { - error: JSON.stringify(error) - }); + logger.log("intellipay-lightbox-error", "ERROR", req.user?.email, null, { message: error.message }); res.json({ error }); } }; exports.payment_refund = async (req, res) => { - logger.log("intellipay-refund", "DEBUG", req.user?.email, null, null); + logger.log("intellipay-refund-request-received", "DEBUG", req.user?.email, null, { + bodyshop: req.body.bodyshop, + paymentid: req.body.paymentid, + amount: req.body.amount + }); const shopCredentials = await getShopCredentials(req.body.bodyshop); + if (shopCredentials.error) { + logger.log("intellipay-refund-credentials-error", "ERROR", req.user?.email, null, { + credentialsError: shopCredentials.error + }); + res.status(400).json({ error: shopCredentials.error }); + return; + } + try { const options = { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, - data: qs.stringify({ method: "payment_refund", ...shopCredentials, @@ -103,132 +119,219 @@ exports.payment_refund = async (req, res) => { url: `https://${domain}.cpteller.com/api/26/webapi.cfc?method=payment_refund` }; + logger.log("intellipay-refund-options-prepared", "DEBUG", req.user?.email, null, { + options + }); + const response = await axios(options); + logger.log("intellipay-refund-success", "DEBUG", req.user?.email, null, { + responseData: response.data + }); + res.send(response.data); } catch (error) { - //console.log(error); logger.log("intellipay-refund-error", "ERROR", req.user?.email, null, { - error: JSON.stringify(error) + error: error.message, + stack: error.stack, + requestOptions: { + paymentid: req.body.paymentid, + amount: req.body.amount + } }); - res.json({ error }); + res.status(500).json({ error: error.message }); } }; exports.generate_payment_url = async (req, res) => { - logger.log("intellipay-payment-url", "DEBUG", req.user?.email, null, null); + logger.log("intellipay-generate-payment-url-received", "DEBUG", req.user?.email, null, { + bodyshop: req.body.bodyshop, + amount: req.body.amount, + account: req.body.account, + comment: req.body.comment, + invoice: req.body.invoice + }); + const shopCredentials = await getShopCredentials(req.body.bodyshop); + if (shopCredentials.error) { + logger.log("intellipay-generate-payment-url-credentials-error", "ERROR", req.user?.email, null, { + credentialsError: shopCredentials.error + }); + res.status(400).json({ error: shopCredentials.error }); + return; + } + try { const options = { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, - //TODO: Move these to environment variables/database. data: qs.stringify({ ...shopCredentials, - //...req.body, 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 - //The postback URL is set at the CP teller global terminal settings page. }), 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, { + options + }); + const response = await axios(options); + logger.log("intellipay-generate-payment-url-success", "DEBUG", req.user?.email, null, { + responseData: response.data + }); + res.send(response.data); } catch (error) { - //console.log(error); - logger.log("intellipay-payment-url-error", "ERROR", req.user?.email, null, { - error: JSON.stringify(error) + logger.log("intellipay-generate-payment-url-error", "ERROR", req.user?.email, null, { + error: error.message, + stack: error.stack, + requestOptions: { + amount: req.body.amount, + account: req.body.account, + comment: req.body.comment, + invoice: req.body.invoice + } }); - res.json({ error }); + res.status(500).json({ error: error.message }); } }; //Reference: https://intellipay.com/dist/webapi26.html#operation/fee exports.checkfee = async (req, res) => { - // Requires amount, bodyshop.imexshopid, and state? to get data. - logger.log("intellipay-fee-check", "DEBUG", req.user?.email, null, null); + logger.log("intellipay-checkfee-request-received", "DEBUG", req.user?.email, null, { + bodyshop: req.body.bodyshop, + amount: req.body.amount + }); - //If there's no amount, there can't be a fee. Skip the call. if (!req.body.amount || req.body.amount <= 0) { + logger.log( + "intellipay-checkfee-skip", + "DEBUG", + req.user?.email, + "Amount is zero or undefined, skipping fee check." + ); 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, { + credentialsError: shopCredentials.error + }); + res.status(400).json({ error: shopCredentials.error }); + return; + } + try { const options = { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, - //TODO: Move these to environment variables/database. data: qs.stringify( { method: "fee", ...shopCredentials, amount: req.body.amount, paymenttype: `CC`, - cardnum: "4111111111111111", //Not needed per documentation, but incorrect values come back without it. + cardnum: "4111111111111111", // Required for compatibility with API state: - req.body.bodyshop?.state && req.body.bodyshop.state?.length === 2 + req.body.bodyshop?.state && req.body.bodyshop.state.length === 2 ? req.body.bodyshop.state.toUpperCase() - : "ZZ" //Same as above + : "ZZ" }, - { sort: false } //ColdFusion Query Strings depend on order. This preserves it. + { 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, { + options + }); + const response = await axios(options); + if (response.data?.error) { + logger.log("intellipay-checkfee-api-error", "ERROR", req.user?.email, null, { + apiError: response.data.error + }); res.status(400).json({ error: response.data.error }); } else if (response.data < 0) { + logger.log("intellipay-checkfee-negative-fee", "ERROR", req.user?.email, "Fee amount returned is negative."); res.json({ error: "Fee amount negative. Check API credentials & account configuration." }); } else { + logger.log("intellipay-checkfee-success", "DEBUG", req.user?.email, null, { + fee: response.data + }); res.json({ fee: response.data }); } } catch (error) { - //console.log(error); - logger.log("intellipay-fee-check-error", "ERROR", req.user?.email, null, { - error: error.message + logger.log("intellipay-checkfee-error", "ERROR", req.user?.email, null, { + error: error.message, + stack: error.stack, + amount: req.body.amount }); - res.status(400).json({ error }); + res.status(500).json({ error: error.message }); } }; exports.postback = async (req, res) => { try { - logger.log("intellipay-postback", "DEBUG", req.user?.email, null, req.body); const { body: values } = req; - const comment = Buffer.from(values?.comment, "base64").toString(); + logger.log("intellipay-postback-received", "DEBUG", req.user?.email, null, { + iprequest: values, + base64Comment: values?.comment || null + }); - if ((!values.invoice || values.invoice === "") && !comment) { + // Decode the base64 comment, if it exists + const decodedComment = values?.comment ? Buffer.from(values.comment, "base64").toString() : null; + + logger.log("intellipay-postback-decoded-comment", "DEBUG", req.user?.email, null, { + decodedComment + }); + + 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, req.body); + logger.log("intellipay-postback-ignored", "DEBUG", req.user?.email, null, { + reason: "No invoice or comment provided", + iprequest: values + }); res.sendStatus(200); return; } - if (comment) { + 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 = JSON.parse(comment); + const parsedComment = JSON.parse(decodedComment); + + logger.log("intellipay-postback-parsed-comment", "DEBUG", req.user?.email, null, { + parsedComment + }); //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 + }); + + // Insert new payments const paymentResult = await gqlClient.request(queries.INSERT_NEW_PAYMENT, { paymentInput: partialPayments.map((p) => ({ amount: p.amount, @@ -250,13 +353,12 @@ exports.postback = async (req, res) => { } })) }); - logger.log("intellipay-postback-app-success", "DEBUG", req.user?.email, JSON.stringify(jobs), { - iprequest: values, + + logger.log("intellipay-postback-payment-success", "DEBUG", req.user?.email, null, { paymentResult }); if (values.origin === "OneLink" && parsedComment.userEmail) { - //Send an email, it was a text to pay link. try { const endPoints = getEndpoints(); sendTaskEmail({ @@ -275,20 +377,23 @@ exports.postback = async (req, res) => { }) }); } catch (error) { - logger.log("intellipay-postback-app-email-error", "DEBUG", req.user?.email, JSON.stringify(jobs), { - iprequest: values, - paymentResult, - error: error.message + logger.log("intellipay-postback-email-error", "ERROR", req.user?.email, null, { + error: error.message, + jobs, + paymentResult }); } } res.sendStatus(200); } else if (values.invoice) { - //This is a link email that's been sent out. 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 + }); + const paymentResult = await gqlClient.request(queries.INSERT_NEW_PAYMENT, { paymentInput: { amount: values.total, @@ -300,6 +405,10 @@ exports.postback = async (req, res) => { } }); + logger.log("intellipay-postback-invoice-payment-success", "DEBUG", req.user?.email, null, { + paymentResult + }); + const responseResults = await gqlClient.request(queries.INSERT_PAYMENT_RESPONSE, { paymentResponse: { amount: values.total, @@ -313,18 +422,17 @@ exports.postback = async (req, res) => { } }); - logger.log("intellipay-postback-link-success", "DEBUG", req.user?.email, values.invoice, { - iprequest: values, - responseResults, - paymentResult + logger.log("intellipay-postback-invoice-response-success", "DEBUG", req.user?.email, null, { + responseResults }); res.sendStatus(200); } } catch (error) { - logger.log("intellipay-postback-total-error", "ERROR", req.user?.email, null, { - error: JSON.stringify(error), - body: req.body + logger.log("intellipay-postback-error", "ERROR", req.user?.email, null, { + error: error.message, + stack: error.stack, + iprequest: req.body }); - res.status(400).json({ succesful: false, error: error.message }); + res.status(400).json({ successful: false, error: error.message }); } };