feature/IO-2885-IntelliPay-App-Postback
- Refactor / Add Tests
This commit is contained in:
@@ -1,98 +1,14 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* Generates a properly formatted Cpteller API URL
|
||||
* @param {Object} options - URL configuration options
|
||||
* @param {string} options.apiType - 'webapi' or 'custapi'
|
||||
* @param {string} [options.version] - API version (e.g., '26' for webapi)
|
||||
* @param {Object} [options.params] - URL query parameters
|
||||
* @returns {string} - The formatted Cpteller URL
|
||||
*/
|
||||
const getCptellerUrl = (options) => {
|
||||
const { apiType = "webapi", version, params = {} } = options;
|
||||
|
||||
// Base URL construction
|
||||
let url = `https://${domain}.cpteller.com/api/`;
|
||||
|
||||
// Add version if specified for webapi
|
||||
if (apiType === "webapi" && version) {
|
||||
url += `${version}/`;
|
||||
}
|
||||
|
||||
// Add the API endpoint
|
||||
url += `${apiType}.cfc`;
|
||||
|
||||
// Add query parameters if any exist
|
||||
const queryParams = new URLSearchParams(params).toString();
|
||||
if (queryParams) {
|
||||
url += `?${queryParams}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Get shop credentials from AWS Secrets Manager
|
||||
* @param bodyshop
|
||||
* @returns {Promise<{error}|{merchantkey: *, apikey: *}|any>}
|
||||
*/
|
||||
const getShopCredentials = async (bodyshop) => {
|
||||
// In Dev/Testing we will use the environment variables
|
||||
if (process.env?.NODE_ENV !== "production") {
|
||||
return {
|
||||
merchantkey: process.env.INTELLIPAY_MERCHANTKEY,
|
||||
apikey: process.env.INTELLIPAY_APIKEY
|
||||
};
|
||||
}
|
||||
|
||||
// In Production we will use the AWS Secrets Manager
|
||||
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
|
||||
}
|
||||
};
|
||||
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
|
||||
@@ -443,207 +359,42 @@ const checkFee = async (req, res) => {
|
||||
* @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 };
|
||||
|
||||
const logResponseMeta = {
|
||||
iprequest: values,
|
||||
decodedComment
|
||||
};
|
||||
|
||||
logger.log("intellipay-postback-received", "DEBUG", "api", null, logResponseMeta);
|
||||
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",
|
||||
...logResponseMeta
|
||||
...logMeta
|
||||
});
|
||||
return res.sendStatus(200);
|
||||
}
|
||||
|
||||
// Process payment based on data type
|
||||
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);
|
||||
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");
|
||||
}
|
||||
|
||||
if (values?.invoice) {
|
||||
// Early Bail on Merchant ID
|
||||
if (!values?.merchantid) {
|
||||
logger.log("intellipay-postback-no-merchantid", "ERROR", "api", null, {
|
||||
message: "Merchant ID is missing",
|
||||
...logResponseMeta
|
||||
});
|
||||
|
||||
return res.status(400).send("Bad Request: Merchant ID is missing");
|
||||
}
|
||||
|
||||
const result = await gqlClient.request(queries.GET_JOBID_BY_MERCHANTID_RONUMBER, {
|
||||
merchantID: values.merchantid,
|
||||
roNumber: values.invoice
|
||||
});
|
||||
|
||||
// Early Bail on No Jobs Found
|
||||
if (!result?.jobs?.length) {
|
||||
logger.log("intellipay-postback-job-not-found", "ERROR", "api", null, {
|
||||
message: "Job not found",
|
||||
...logResponseMeta
|
||||
});
|
||||
|
||||
return res.status(400).send("Bad Request: Job not found");
|
||||
}
|
||||
|
||||
const job = result?.jobs?.[0];
|
||||
|
||||
const bodyshop = job?.bodyshop;
|
||||
|
||||
// Early Bail on no Bodyshop Found
|
||||
if (!bodyshop) {
|
||||
logger.log("intellipay-postback-bodyshop-not-found", "ERROR", "api", null, {
|
||||
message: "Bodyshop not found",
|
||||
...logResponseMeta
|
||||
});
|
||||
|
||||
return res.status(400).send("Bad Request: Bodyshop not found");
|
||||
}
|
||||
|
||||
const ipMapping = bodyshop?.intellipay_config?.payment_map;
|
||||
|
||||
logger.log("intellipay-postback-invoice-job-fetched", "DEBUG", "api", null, {
|
||||
job,
|
||||
...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: job.id,
|
||||
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.id,
|
||||
paymentid: paymentResult.id,
|
||||
jobid: job.id,
|
||||
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
|
||||
...logMeta
|
||||
});
|
||||
|
||||
return res.status(400).json({ successful: false, error: error.message, ...logResponseMeta });
|
||||
return res.status(400).json({ successful: false, error: error.message, ...logMeta });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user