diff --git a/hasura/migrations/1743531377681_alter_table_public_bodyshops_add_column_intellipay_merchant_id/down.sql b/hasura/migrations/1743531377681_alter_table_public_bodyshops_add_column_intellipay_merchant_id/down.sql new file mode 100644 index 000000000..2d6bb5c01 --- /dev/null +++ b/hasura/migrations/1743531377681_alter_table_public_bodyshops_add_column_intellipay_merchant_id/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."bodyshops" add column "intellipay_merchant_id" text +-- null unique; diff --git a/hasura/migrations/1743531377681_alter_table_public_bodyshops_add_column_intellipay_merchant_id/up.sql b/hasura/migrations/1743531377681_alter_table_public_bodyshops_add_column_intellipay_merchant_id/up.sql new file mode 100644 index 000000000..90006bbda --- /dev/null +++ b/hasura/migrations/1743531377681_alter_table_public_bodyshops_add_column_intellipay_merchant_id/up.sql @@ -0,0 +1,2 @@ +alter table "public"."bodyshops" add column "intellipay_merchant_id" text + null unique; diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index f05ea7ec5..44a7cbffb 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -2832,3 +2832,15 @@ exports.GET_DOCUMENTS_BY_IDS = ` takenat } }`; + +exports.GET_JOBID_BY_MERCHANTID_RONUMBER = ` +query GET_JOBID_BY_MERCHANTID_RONUMBER($merchantID: String!, $roNumber: String!) { + jobs(where: {ro_number: {_eq: $roNumber}, bodyshop: {intellipay_merchant_id: {_eq: $merchantID}}}) { + id + shopid + bodyshop { + id + intellipay_config + } + } +}`; diff --git a/server/intellipay/intellipay.js b/server/intellipay/intellipay.js index 169e27fd2..be8d9f24c 100644 --- a/server/intellipay/intellipay.js +++ b/server/intellipay/intellipay.js @@ -18,21 +18,52 @@ const client = new SecretsManagerClient({ 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) => { - // Development only - if (process.env.NODE_ENV === undefined) { + // 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 }; } - // Production code + // In Production we will use the AWS Secrets Manager if (bodyshop?.imexshopid) { try { const secret = await client.send( @@ -106,7 +137,10 @@ const lightboxCredentials = async (req, res) => { ...shopCredentials, operatingenv: "businessattended" }), - url: `https://${domain}.cpteller.com/api/custapi.cfc?method=autoterminal${req.body.refresh ? "_refresh" : ""}` //autoterminal_refresh + url: getCptellerUrl({ + apiType: "custapi", + params: { method: `autoterminal${req.body.refresh ? "_refresh" : ""}` } + }) }; const response = await axios(options); @@ -178,7 +212,11 @@ const paymentRefund = async (req, res) => { paymentid: req.body.paymentid, amount: req.body.amount }), - url: `https://${domain}.cpteller.com/api/26/webapi.cfc?method=payment_refund` + url: getCptellerUrl({ + apiType: "webapi", + version: "26", + params: { method: "payment_refund" } + }) }; logger.log("intellipay-refund-options-prepared", "DEBUG", req.user?.email, null, { @@ -259,7 +297,10 @@ const generatePaymentUrl = async (req, res) => { invoice: req.body.invoice, createshorturl: true }), - url: `https://${domain}.cpteller.com/api/custapi.cfc?method=generate_lightbox_url` + url: getCptellerUrl({ + apiType: "custapi", + params: { method: "generate_lightbox_url" } + }) }; logger.log("intellipay-generate-payment-url-options-prepared", "DEBUG", req.user?.email, null, { @@ -344,7 +385,7 @@ const checkFee = async (req, res) => { }, { sort: false } // Ensure query string order is preserved ), - url: `https://${domain}.cpteller.com/api/26/webapi.cfc` + url: getCptellerUrl({ apiType: "webapi", version: "26" }) }; logger.log("intellipay-checkfee-options-prepared", "DEBUG", req.user?.email, null, { @@ -506,19 +547,49 @@ const postBack = async (req, res) => { } if (values?.invoice) { - const job = await gqlClient.request(queries.GET_JOB_BY_PK, { - id: 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 }); - const bodyshop = await gqlClient.request(queries.GET_BODYSHOP_BY_ID, { - id: job.jobs_by_pk.shopid - }); + // 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 + }); - const ipMapping = bodyshop.bodyshops_by_pk.intellipay_config?.payment_map; + 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, - bodyshop, ...logResponseMeta }); @@ -528,7 +599,7 @@ const postBack = async (req, res) => { transactionid: values.authcode, payer: "Customer", type: ipMapping ? ipMapping[(values.cardtype || "").toLowerCase()] || values.cardtype : values.cardtype, - jobid: values.invoice, + jobid: job.id, date: moment(Date.now()) } }); @@ -541,9 +612,9 @@ const postBack = async (req, res) => { const responseResults = await gqlClient.request(queries.INSERT_PAYMENT_RESPONSE, { paymentResponse: { amount: values.total, - bodyshopid: bodyshop.bodyshops_by_pk.id, + bodyshopid: bodyshop.id, paymentid: paymentResult.id, - jobid: values.invoice, + jobid: job.id, declinereason: "Approved", ext_paymentid: values.paymentid, successful: true, @@ -576,13 +647,10 @@ const postBack = async (req, res) => { } }; -const postBackCallBack = async (req, res) => {}; - module.exports = { lightboxCredentials, paymentRefund, generatePaymentUrl, checkFee, - postBack, - postBackCallBack + postBack }; diff --git a/server/routes/intellipayRoutes.js b/server/routes/intellipayRoutes.js index 7c3a2ecec..7a0425fd2 100644 --- a/server/routes/intellipayRoutes.js +++ b/server/routes/intellipayRoutes.js @@ -6,8 +6,7 @@ const { paymentRefund, generatePaymentUrl, postBack, - checkFee, - postBackCallBack + checkFee } = require("../intellipay/intellipay"); router.post("/lightbox_credentials", validateFirebaseIdTokenMiddleware, lightboxCredentials); @@ -15,6 +14,5 @@ router.post("/payment_refund", validateFirebaseIdTokenMiddleware, paymentRefund) router.post("/generate_payment_url", validateFirebaseIdTokenMiddleware, generatePaymentUrl); router.post("/checkfee", validateFirebaseIdTokenMiddleware, checkFee); router.post("/postback", postBack); -router.post("/postback-callback", postBackCallBack); module.exports = router;