feature/IO-2885-IntelliPay-App-Postback
- Refactor / Add Tests
This commit is contained in:
45
server/intellipay/lib/aws-secrets-manager.js
Normal file
45
server/intellipay/lib/aws-secrets-manager.js
Normal file
@@ -0,0 +1,45 @@
|
||||
"use strict";
|
||||
|
||||
const awsSecretManager = require("@aws-sdk/client-secrets-manager");
|
||||
|
||||
class SecretsManager {
|
||||
/**
|
||||
* Uses AWS Secrets Manager to retrieve a secret
|
||||
*/
|
||||
static async getSecret(secretName, region) {
|
||||
const config = { region: region };
|
||||
let secretsManager = new awsSecretManager.SecretsManager(config);
|
||||
try {
|
||||
let secretValue = await secretsManager.getSecretValue({ SecretId: secretName });
|
||||
if ("SecretString" in secretValue) {
|
||||
return secretValue.SecretString;
|
||||
} else {
|
||||
let buff = new Buffer(secretValue.SecretBinary, "base64");
|
||||
return buff.toString("ascii");
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code === "DecryptionFailureException")
|
||||
// Secrets Manager can't decrypt the protected secret text using the provided KMS key.
|
||||
// Deal with the exception here, and/or rethrow at your discretion.
|
||||
throw err;
|
||||
else if (err.code === "InternalServiceErrorException")
|
||||
// An error occurred on the server side.
|
||||
// Deal with the exception here, and/or rethrow at your discretion.
|
||||
throw err;
|
||||
else if (err.code === "InvalidParameterException")
|
||||
// You provided an invalid value for a parameter.
|
||||
// Deal with the exception here, and/or rethrow at your discretion.
|
||||
throw err;
|
||||
else if (err.code === "InvalidRequestException")
|
||||
// You provided a parameter value that is not valid for the current state of the resource.
|
||||
// Deal with the exception here, and/or rethrow at your discretion.
|
||||
throw err;
|
||||
else if (err.code === "ResourceNotFoundException")
|
||||
// We can't find the resource that you asked for.
|
||||
// Deal with the exception here, and/or rethrow at your discretion.
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SecretsManager;
|
||||
14
server/intellipay/lib/decodeComment.js
Normal file
14
server/intellipay/lib/decodeComment.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* @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
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = decodeComment;
|
||||
34
server/intellipay/lib/getCptellerUrl.js
Normal file
34
server/intellipay/lib/getCptellerUrl.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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 domain = process.env?.NODE_ENV === "production" ? "secure" : "test";
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
module.exports = getCptellerUrl;
|
||||
12
server/intellipay/lib/getPaymentType.js
Normal file
12
server/intellipay/lib/getPaymentType.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* @description Get payment type based on IP mapping
|
||||
* @param ipMapping
|
||||
* @param cardType
|
||||
* @returns {*}
|
||||
*/
|
||||
const getPaymentType = (ipMapping, cardType) => {
|
||||
const normalizedCardType = (cardType || "").toLowerCase();
|
||||
return ipMapping ? ipMapping[normalizedCardType] || cardType : cardType;
|
||||
};
|
||||
|
||||
module.exports = getPaymentType;
|
||||
40
server/intellipay/lib/getShopCredentials.js
Normal file
40
server/intellipay/lib/getShopCredentials.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager");
|
||||
const { InstanceRegion } = require("../../utils/instanceMgr");
|
||||
|
||||
const client = new SecretsManagerClient({
|
||||
region: InstanceRegion()
|
||||
});
|
||||
|
||||
/**
|
||||
* @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
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = getShopCredentials;
|
||||
81
server/intellipay/lib/handleCommentBasedPayment.js
Normal file
81
server/intellipay/lib/handleCommentBasedPayment.js
Normal file
@@ -0,0 +1,81 @@
|
||||
const sendPaymentNotificationEmail = require("./sendPaymentNotificationEmail");
|
||||
const { INSERT_NEW_PAYMENT, GET_BODYSHOP_BY_ID, GET_JOBS_BY_PKS } = require("../../graphql-client/queries");
|
||||
const getPaymentType = require("./getPaymentType");
|
||||
const moment = require("moment");
|
||||
|
||||
const gqlClient = require("../../graphql-client/graphql-client").client;
|
||||
|
||||
/**
|
||||
* @description Handle comment-based payment processing
|
||||
* @param values
|
||||
* @param decodedComment
|
||||
* @param logger
|
||||
* @param logMeta
|
||||
* @param res
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
const handleCommentBasedPayment = async (values, decodedComment, logger, logMeta, res) => {
|
||||
logger.log("intellipay-postback-parsed-comment", "DEBUG", "api", null, {
|
||||
parsedComment: decodedComment,
|
||||
...logMeta
|
||||
});
|
||||
|
||||
const partialPayments = Array.isArray(decodedComment) ? decodedComment : decodedComment.payments;
|
||||
|
||||
// Fetch job data
|
||||
const jobs = await gqlClient.request(GET_JOBS_BY_PKS, {
|
||||
ids: partialPayments.map((p) => p.jobid)
|
||||
});
|
||||
|
||||
// Fetch bodyshop data
|
||||
const bodyshop = await gqlClient.request(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: decodedComment,
|
||||
...logMeta
|
||||
});
|
||||
|
||||
// Create payment records
|
||||
const paymentResult = await gqlClient.request(INSERT_NEW_PAYMENT, {
|
||||
paymentInput: partialPayments.map((p) => ({
|
||||
amount: p.amount,
|
||||
transactionid: values.authcode,
|
||||
payer: "Customer",
|
||||
type: getPaymentType(ipMapping, 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: decodedComment,
|
||||
...logMeta
|
||||
});
|
||||
|
||||
// Send notification email if needed
|
||||
if (values?.origin === "OneLink" && decodedComment?.userEmail) {
|
||||
await sendPaymentNotificationEmail(decodedComment.userEmail, jobs, partialPayments, logger, logMeta);
|
||||
}
|
||||
|
||||
return res.sendStatus(200);
|
||||
};
|
||||
|
||||
module.exports = handleCommentBasedPayment;
|
||||
101
server/intellipay/lib/handleInvoiceBasedPayment.js
Normal file
101
server/intellipay/lib/handleInvoiceBasedPayment.js
Normal file
@@ -0,0 +1,101 @@
|
||||
const handlePaymentValidationError = require("./handlePaymentValidationError");
|
||||
const {
|
||||
GET_JOBID_BY_MERCHANTID_RONUMBER,
|
||||
INSERT_PAYMENT_RESPONSE,
|
||||
INSERT_NEW_PAYMENT
|
||||
} = require("../../graphql-client/queries");
|
||||
const getPaymentType = require("./getPaymentType");
|
||||
const moment = require("moment");
|
||||
|
||||
const gqlClient = require("../../graphql-client/graphql-client").client;
|
||||
|
||||
/**
|
||||
* @description Handle invoice-based payment processing
|
||||
* @param values
|
||||
* @param logger
|
||||
* @param logMeta
|
||||
* @param res
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
const handleInvoiceBasedPayment = async (values, logger, logMeta, res) => {
|
||||
// Validate required fields
|
||||
if (!values.merchantid) {
|
||||
return handlePaymentValidationError(
|
||||
res,
|
||||
logger,
|
||||
"intellipay-postback-no-merchantid",
|
||||
"Merchant ID is missing",
|
||||
logMeta
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch job data
|
||||
const result = await gqlClient.request(GET_JOBID_BY_MERCHANTID_RONUMBER, {
|
||||
merchantID: values.merchantid,
|
||||
roNumber: values.invoice
|
||||
});
|
||||
|
||||
if (!result?.jobs?.length) {
|
||||
return handlePaymentValidationError(res, logger, "intellipay-postback-job-not-found", "Job not found", logMeta);
|
||||
}
|
||||
|
||||
const job = result.jobs[0];
|
||||
const bodyshop = job?.bodyshop;
|
||||
|
||||
if (!bodyshop) {
|
||||
return handlePaymentValidationError(
|
||||
res,
|
||||
logger,
|
||||
"intellipay-postback-bodyshop-not-found",
|
||||
"Bodyshop not found",
|
||||
logMeta
|
||||
);
|
||||
}
|
||||
|
||||
const ipMapping = bodyshop.intellipay_config?.payment_map;
|
||||
|
||||
logger.log("intellipay-postback-invoice-job-fetched", "DEBUG", "api", null, {
|
||||
job,
|
||||
...logMeta
|
||||
});
|
||||
|
||||
// Create payment record
|
||||
const paymentResult = await gqlClient.request(INSERT_NEW_PAYMENT, {
|
||||
paymentInput: {
|
||||
amount: values.total,
|
||||
transactionid: values.authcode,
|
||||
payer: "Customer",
|
||||
type: getPaymentType(ipMapping, values.cardtype),
|
||||
jobid: job.id,
|
||||
date: moment(Date.now())
|
||||
}
|
||||
});
|
||||
|
||||
logger.log("intellipay-postback-invoice-payment-success", "DEBUG", "api", null, {
|
||||
paymentResult,
|
||||
...logMeta
|
||||
});
|
||||
|
||||
// Create payment response record
|
||||
const responseResults = await gqlClient.request(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,
|
||||
...logMeta
|
||||
});
|
||||
|
||||
return res.sendStatus(200);
|
||||
};
|
||||
|
||||
module.exports = handleInvoiceBasedPayment;
|
||||
18
server/intellipay/lib/handlePaymentValidationError.js
Normal file
18
server/intellipay/lib/handlePaymentValidationError.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @description Log validation error and send response
|
||||
* @param res
|
||||
* @param logger
|
||||
* @param logCode
|
||||
* @param message
|
||||
* @param logMeta
|
||||
* @returns {*}
|
||||
*/
|
||||
const handlePaymentValidationError = (res, logger, logCode, message, logMeta) => {
|
||||
logger.log(logCode, "ERROR", "api", null, {
|
||||
message,
|
||||
...logMeta
|
||||
});
|
||||
return res.status(400).send(`Bad Request: ${message}`);
|
||||
};
|
||||
|
||||
module.exports = handlePaymentValidationError;
|
||||
41
server/intellipay/lib/sendPaymentNotificationEmail.js
Normal file
41
server/intellipay/lib/sendPaymentNotificationEmail.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const { sendTaskEmail } = require("../../email/sendemail");
|
||||
const generateEmailTemplate = require("../../email/generateTemplate");
|
||||
|
||||
/**
|
||||
* @description Send notification email to the user
|
||||
* @param userEmail
|
||||
* @param jobs
|
||||
* @param partialPayments
|
||||
* @param logger
|
||||
* @param logMeta
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const sendPaymentNotificationEmail = async (userEmail, jobs, partialPayments, logger, logMeta) => {
|
||||
try {
|
||||
await sendTaskEmail({
|
||||
to: 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,
|
||||
...logMeta
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = sendPaymentNotificationEmail;
|
||||
Reference in New Issue
Block a user