Files
bodyshop/server/intellipay/intellipay.js
Dave Richer 9bf6ba9cf0 feature/IO-2885-IntelliPay-App-Postback
- Refactor / Add Tests
2025-04-02 11:09:03 -04:00

408 lines
12 KiB
JavaScript

const Dinero = require("dinero.js");
const qs = require("query-string");
const axios = require("axios");
const logger = require("../utils/logger");
const { isEmpty, isNumber } = require("lodash");
const handleCommentBasedPayment = require("./lib/handleCommentBasedPayment");
const handleInvoiceBasedPayment = require("./lib/handleInvoiceBasedPayment");
const logValidationError = require("./lib/handlePaymentValidationError");
const getCptellerUrl = require("./lib/getCptellerUrl");
const getShopCredentials = require("./lib/getShopCredentials");
const decodeComment = require("./lib/decodeComment");
/**
* @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: getCptellerUrl({
apiType: "custapi",
params: { method: `autoterminal${req.body.refresh ? "_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: getCptellerUrl({
apiType: "webapi",
version: "26",
params: { 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: getCptellerUrl({
apiType: "custapi",
params: { 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: getCptellerUrl({ apiType: "webapi", version: "26" })
};
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>}
*/
/**
* Handle the postback from Intellipay payment system
*/
const postBack = async (req, res) => {
const { body: values } = req;
const decodedComment = decodeComment(values?.comment);
const logMeta = { iprequest: values, decodedComment };
logger.log("intellipay-postback-received", "DEBUG", "api", null, logMeta);
try {
// Handle empty/invalid requests
if (isEmpty(values?.invoice) && !decodedComment) {
logger.log("intellipay-postback-ignored", "DEBUG", "api", null, {
message: "No invoice or comment provided",
...logMeta
});
return res.sendStatus(200);
}
// Process payment based on data type
if (decodedComment) {
return await handleCommentBasedPayment(values, decodedComment, logger, logMeta, res);
} else if (values?.invoice) {
return await handleInvoiceBasedPayment(values, logger, logMeta, res);
} else {
// This should be caught by first validation, but as a safeguard
logValidationError("intellipay-postback-invalid", "No valid invoice or comment provided", logMeta);
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,
...logMeta
});
return res.status(400).json({ successful: false, error: error.message, ...logMeta });
}
};
module.exports = {
lightboxCredentials,
paymentRefund,
generatePaymentUrl,
checkFee,
postBack
};