589 lines
17 KiB
JavaScript
589 lines
17 KiB
JavaScript
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 { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager");
|
|
const { InstanceRegion, InstanceEndpoints } = require("../utils/instanceMgr");
|
|
const { isEmpty, isNumber } = require("lodash");
|
|
|
|
const domain = process.env.NODE_ENV ? "secure" : "test";
|
|
|
|
const client = new SecretsManagerClient({
|
|
region: InstanceRegion()
|
|
});
|
|
|
|
const gqlClient = require("../graphql-client/graphql-client").client;
|
|
|
|
/**
|
|
* @description Get shop credentials from AWS Secrets Manager
|
|
* @param bodyshop
|
|
* @returns {Promise<{error}|{merchantkey: *, apikey: *}|any>}
|
|
*/
|
|
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
|
|
};
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @description Decode the comment from base64
|
|
* @param comment
|
|
* @returns {any|null}
|
|
*/
|
|
const decodeComment = (comment) => {
|
|
try {
|
|
return comment ? JSON.parse(Buffer.from(comment, "base64").toString()) : null;
|
|
} catch (error) {
|
|
return null; // Handle malformed base64 string gracefully
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @description Get lightbox credentials for the shop
|
|
* @param req
|
|
* @param res
|
|
* @returns {Promise<void>}
|
|
*/
|
|
const lightboxCredentials = 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
|
|
});
|
|
|
|
return res.json({
|
|
message: shopCredentials.error?.message,
|
|
type: "intellipay-credentials-error",
|
|
...logMeta
|
|
});
|
|
}
|
|
|
|
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
|
|
});
|
|
|
|
return res.send(response.data);
|
|
} catch (error) {
|
|
logger.log("intellipay-lightbox-error", "ERROR", req.user?.email, null, {
|
|
message: error?.message,
|
|
...logMeta
|
|
});
|
|
|
|
return res.json({
|
|
message: error?.message,
|
|
type: "intellipay-lightbox-error",
|
|
...logMeta
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @description Process payment refund
|
|
* @param req
|
|
* @param res
|
|
* @returns {Promise<void>}
|
|
*/
|
|
const paymentRefund = 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
|
|
});
|
|
|
|
return res.status(400).json({
|
|
credentialsError: shopCredentials.error,
|
|
type: "intellipay-refund-credentials-error",
|
|
...logResponseMeta
|
|
});
|
|
}
|
|
|
|
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
|
|
});
|
|
|
|
return res.send(response.data);
|
|
} catch (error) {
|
|
logger.log("intellipay-refund-error", "ERROR", req.user?.email, null, {
|
|
message: error?.message,
|
|
...logResponseMeta
|
|
});
|
|
|
|
return res.status(500).json({
|
|
message: error?.message,
|
|
type: "intellipay-refund-error",
|
|
...logResponseMeta
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @description Generate payment URL for the shop
|
|
* @param req
|
|
* @param res
|
|
* @returns {Promise<void>}
|
|
*/
|
|
const generatePaymentUrl = 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
|
|
});
|
|
|
|
return res.status(400).json({
|
|
message: shopCredentials.error?.message,
|
|
type: "intellipay-generate-payment-url-credentials-error",
|
|
...logResponseMeta
|
|
});
|
|
}
|
|
|
|
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
|
|
});
|
|
|
|
return res.send(response.data);
|
|
} catch (error) {
|
|
logger.log("intellipay-generate-payment-url-error", "ERROR", req.user?.email, null, {
|
|
message: error?.message,
|
|
...logResponseMeta
|
|
});
|
|
|
|
return res.status(500).json({ message: error?.message, ...logResponseMeta });
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @description Check the fee for a given amount
|
|
* Reference: https://intellipay.com/dist/webapi26.html#operation/fee
|
|
* @param req
|
|
* @param res
|
|
* @returns {Promise<void>}
|
|
*/
|
|
const 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 (!isNumber(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
|
|
});
|
|
|
|
return res.json({ fee: 0 });
|
|
}
|
|
|
|
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
|
|
});
|
|
|
|
return res.status(400).json({ error: shopCredentials.error?.message, ...logResponseMeta });
|
|
}
|
|
|
|
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
|
|
});
|
|
|
|
return res.status(400).json({
|
|
error: response.data?.error,
|
|
type: "intellipay-checkfee-api-error",
|
|
...logResponseMeta
|
|
});
|
|
}
|
|
|
|
if (response.data < 0) {
|
|
logger.log("intellipay-checkfee-negative-fee", "ERROR", req.user?.email, null, {
|
|
message: "Fee amount returned is negative.",
|
|
...logResponseMeta
|
|
});
|
|
|
|
return res.json({
|
|
error: "Fee amount negative. Check API credentials & account configuration.",
|
|
...logResponseMeta,
|
|
type: "intellipay-checkfee-negative-fee"
|
|
});
|
|
}
|
|
|
|
logger.log("intellipay-checkfee-success", "DEBUG", req.user?.email, null, {
|
|
fee: response.data,
|
|
...logResponseMeta
|
|
});
|
|
|
|
return res.json({ fee: response.data, ...logResponseMeta });
|
|
} catch (error) {
|
|
logger.log("intellipay-checkfee-error", "ERROR", req.user?.email, null, {
|
|
message: error?.message,
|
|
...logResponseMeta
|
|
});
|
|
|
|
return res.status(500).json({ error: error?.message, logResponseMeta });
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @description Handle the postback from Intellipay
|
|
* @param req
|
|
* @param res
|
|
* @returns {Promise<void>}
|
|
*/
|
|
const postBack = async (req, res) => {
|
|
const { body: values } = req;
|
|
const decodedComment = decodeComment(values?.comment);
|
|
|
|
const logResponseMeta = {
|
|
iprequest: values,
|
|
decodedComment
|
|
};
|
|
|
|
logger.log("intellipay-postback-received", "DEBUG", "api", null, logResponseMeta);
|
|
|
|
try {
|
|
if (isEmpty(values?.invoice) && !decodedComment) {
|
|
logger.log("intellipay-postback-ignored", "DEBUG", "api", null, {
|
|
message: "No invoice or comment provided",
|
|
...logResponseMeta
|
|
});
|
|
return res.sendStatus(200);
|
|
}
|
|
|
|
if (decodedComment) {
|
|
const parsedComment = decodedComment;
|
|
|
|
logger.log("intellipay-postback-parsed-comment", "DEBUG", "api", null, {
|
|
parsedComment,
|
|
...logResponseMeta
|
|
});
|
|
|
|
const partialPayments = Array.isArray(parsedComment) ? parsedComment : parsedComment.payments;
|
|
|
|
const jobs = await gqlClient.request(queries.GET_JOBS_BY_PKS, {
|
|
ids: partialPayments.map((p) => p.jobid)
|
|
});
|
|
|
|
const bodyshop = await gqlClient.request(queries.GET_BODYSHOP_BY_ID, {
|
|
id: jobs.jobs[0].shopid
|
|
});
|
|
|
|
const ipMapping = bodyshop.bodyshops_by_pk.intellipay_config?.payment_map;
|
|
|
|
logger.log("intellipay-postback-jobs-fetched", "DEBUG", "api", null, {
|
|
jobs,
|
|
parsedComment,
|
|
...logResponseMeta
|
|
});
|
|
|
|
const paymentResult = await gqlClient.request(queries.INSERT_NEW_PAYMENT, {
|
|
paymentInput: partialPayments.map((p) => ({
|
|
amount: p.amount,
|
|
transactionid: values.authcode,
|
|
payer: "Customer",
|
|
type: ipMapping ? ipMapping[(values.cardtype || "").toLowerCase()] || values.cardtype : values.cardtype,
|
|
jobid: p.jobid,
|
|
date: moment(Date.now()),
|
|
payment_responses: {
|
|
data: {
|
|
amount: values.total,
|
|
bodyshopid: bodyshop.bodyshops_by_pk.id,
|
|
jobid: p.jobid,
|
|
declinereason: "Approved",
|
|
ext_paymentid: values.paymentid,
|
|
successful: true,
|
|
response: values
|
|
}
|
|
}
|
|
}))
|
|
});
|
|
|
|
logger.log("intellipay-postback-payment-success", "DEBUG", "api", null, {
|
|
paymentResult,
|
|
jobs,
|
|
parsedComment,
|
|
...logResponseMeta
|
|
});
|
|
|
|
if (values?.origin === "OneLink" && parsedComment?.userEmail) {
|
|
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: <a href="${InstanceEndpoints()}/manage/jobs/${job.id}">${job.ro_number || "N/A"}</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("<br/>")
|
|
})
|
|
}).catch((error) => {
|
|
logger.log("intellipay-postback-email-error", "ERROR", "api", null, {
|
|
message: error.message,
|
|
jobs,
|
|
paymentResult,
|
|
...logResponseMeta
|
|
});
|
|
});
|
|
}
|
|
|
|
return res.sendStatus(200);
|
|
}
|
|
|
|
if (values?.invoice) {
|
|
const job = await gqlClient.request(queries.GET_JOB_BY_PK, {
|
|
id: values.invoice
|
|
});
|
|
|
|
const bodyshop = await gqlClient.request(queries.GET_BODYSHOP_BY_ID, {
|
|
id: job.jobs_by_pk.shopid
|
|
});
|
|
|
|
const ipMapping = bodyshop.bodyshops_by_pk.intellipay_config?.payment_map;
|
|
|
|
logger.log("intellipay-postback-invoice-job-fetched", "DEBUG", "api", null, {
|
|
job,
|
|
bodyshop,
|
|
...logResponseMeta
|
|
});
|
|
|
|
const paymentResult = await gqlClient.request(queries.INSERT_NEW_PAYMENT, {
|
|
paymentInput: {
|
|
amount: values.total,
|
|
transactionid: values.authcode,
|
|
payer: "Customer",
|
|
type: ipMapping ? ipMapping[(values.cardtype || "").toLowerCase()] || values.cardtype : values.cardtype,
|
|
jobid: values.invoice,
|
|
date: moment(Date.now())
|
|
}
|
|
});
|
|
|
|
logger.log("intellipay-postback-invoice-payment-success", "DEBUG", "api", null, {
|
|
paymentResult,
|
|
...logResponseMeta
|
|
});
|
|
|
|
const responseResults = await gqlClient.request(queries.INSERT_PAYMENT_RESPONSE, {
|
|
paymentResponse: {
|
|
amount: values.total,
|
|
bodyshopid: bodyshop.bodyshops_by_pk.id,
|
|
paymentid: paymentResult.id,
|
|
jobid: values.invoice,
|
|
declinereason: "Approved",
|
|
ext_paymentid: values.paymentid,
|
|
successful: true,
|
|
response: values
|
|
}
|
|
});
|
|
|
|
logger.log("intellipay-postback-invoice-response-success", "DEBUG", "api", null, {
|
|
responseResults,
|
|
...logResponseMeta
|
|
});
|
|
|
|
return res.sendStatus(200);
|
|
}
|
|
|
|
// Default case: no valid conditions met
|
|
logger.log("intellipay-postback-invalid", "WARN", "api", null, {
|
|
message: "No valid invoice or comment provided",
|
|
...logResponseMeta
|
|
});
|
|
|
|
return res.status(400).send("Bad Request: No valid invoice or comment provided");
|
|
} catch (error) {
|
|
logger.log("intellipay-postback-error", "ERROR", "api", null, {
|
|
message: error?.message,
|
|
...logResponseMeta
|
|
});
|
|
|
|
return res.status(400).json({ successful: false, error: error.message, ...logResponseMeta });
|
|
}
|
|
};
|
|
|
|
const postBackCallBack = async (req, res) => {};
|
|
|
|
module.exports = {
|
|
lightboxCredentials,
|
|
paymentRefund,
|
|
generatePaymentUrl,
|
|
checkFee,
|
|
postBack,
|
|
postBackCallBack
|
|
};
|