From 09c4662436ab77b6461ae3598e8a45be7effa054 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 2 Apr 2025 11:34:48 -0400 Subject: [PATCH] feature/IO-2885-IntelliPay-App-Postback - Add Tests --- package-lock.json | 48 +++++- package.json | 1 + .../tests/handleCommentBasedPayment.test.js | 152 ++++++++++++++++++ .../tests/handleInvoiceBasedPayment.test.js | 129 +++++++++++++++ .../intellipay/{ => tests}/intellipay.test.js | 10 +- 5 files changed, 332 insertions(+), 8 deletions(-) create mode 100644 server/intellipay/lib/tests/handleCommentBasedPayment.test.js create mode 100644 server/intellipay/lib/tests/handleInvoiceBasedPayment.test.js rename server/intellipay/{ => tests}/intellipay.test.js (96%) diff --git a/package-lock.json b/package-lock.json index ffe02c7ec..09da429eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,6 +75,7 @@ "eslint": "^9.23.0", "eslint-plugin-react": "^7.37.4", "globals": "^15.15.0", + "mock-require": "^3.0.3", "p-limit": "^3.1.0", "prettier": "^3.5.3", "source-map-explorer": "^2.5.2", @@ -8402,9 +8403,9 @@ } }, "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, "license": "MIT", "dependencies": { @@ -9535,6 +9536,40 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mock-require": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/mock-require/-/mock-require-3.0.3.tgz", + "integrity": "sha512-lLzfLHcyc10MKQnNUCv7dMcoY/2Qxd6wJfbqCcVk3LDb8An4hF6ohk5AztrvgKhJCqj36uyzi/p5se+tvyD+Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-caller-file": "^1.0.2", + "normalize-path": "^2.1.1" + }, + "engines": { + "node": ">=4.3.0" + } + }, + "node_modules/mock-require/node_modules/get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true, + "license": "ISC" + }, + "node_modules/mock-require/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/module-details-from-path": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", @@ -10709,6 +10744,13 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true, + "license": "ISC" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", diff --git a/package.json b/package.json index aa3c57951..0a95a146e 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "eslint": "^9.23.0", "eslint-plugin-react": "^7.37.4", "globals": "^15.15.0", + "mock-require": "^3.0.3", "p-limit": "^3.1.0", "prettier": "^3.5.3", "source-map-explorer": "^2.5.2", diff --git a/server/intellipay/lib/tests/handleCommentBasedPayment.test.js b/server/intellipay/lib/tests/handleCommentBasedPayment.test.js new file mode 100644 index 000000000..9fd7f4ffc --- /dev/null +++ b/server/intellipay/lib/tests/handleCommentBasedPayment.test.js @@ -0,0 +1,152 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import mockRequire from "mock-require"; + +const gqlRequestMock = { request: vi.fn() }; +const getPaymentTypeMock = vi.fn(() => "American Express"); +const sendPaymentNotificationEmailMock = vi.fn(); + +let handleCommentBasedPayment; + +beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + + // Mock dependencies using mock-require BEFORE requiring the target module + mockRequire("../../../graphql-client/graphql-client", { + client: gqlRequestMock + }); + + mockRequire("../getPaymentType", getPaymentTypeMock); + mockRequire("../sendPaymentNotificationEmail", sendPaymentNotificationEmailMock); + + // Now require the module under test + handleCommentBasedPayment = require("../handleCommentBasedPayment"); + + // Chain your GraphQL mocks + gqlRequestMock.request + .mockResolvedValueOnce({ + jobs: [ + { + id: "c1ffe09c-e7d4-46b3-aac5-f23e39563181", + shopid: "bfec8c8c-b7f1-49e0-be4c-524455f4e582" + } + ] + }) + .mockResolvedValueOnce({ + bodyshops_by_pk: { + id: "bfec8c8c-b7f1-49e0-be4c-524455f4e582", + intellipay_config: { + payment_map: { + amex: "American Express" + } + } + } + }) + .mockResolvedValueOnce({ + insert_payments: { + returning: [{ id: "5dfda3c4-c0a6-4b09-a73d-176ed0ac6499" }] + } + }); +}); + +describe("handleCommentBasedPayment", () => { + const mockLogger = { log: vi.fn() }; + const mockRes = { sendStatus: vi.fn() }; + + const values = { + authcode: "5557301", + total: "0.01", + origin: "Dejavoo", + paymentid: "24294378", + cardtype: "Amex" + }; + + const decodedComment = { + payments: [{ jobid: "c1ffe09c-e7d4-46b3-aac5-f23e39563181", amount: 0.01 }], + userEmail: "test@example.com" + }; + + const logMeta = { op: "xyz123" }; + + it("processes comment-based payment and returns 200", async () => { + await handleCommentBasedPayment(values, decodedComment, mockLogger, logMeta, mockRes); + + expect(gqlRequestMock.request).toHaveBeenCalledTimes(3); + expect(getPaymentTypeMock).toHaveBeenCalledWith({ amex: "American Express" }, "Amex"); + expect(sendPaymentNotificationEmailMock).not.toHaveBeenCalled(); + expect(mockRes.sendStatus).toHaveBeenCalledWith(200); + }); + + it("sends notification if origin is OneLink and userEmail exists", async () => { + const oneLinkValues = { ...values, origin: "OneLink" }; + + await handleCommentBasedPayment(oneLinkValues, decodedComment, mockLogger, logMeta, mockRes); + + expect(sendPaymentNotificationEmailMock).toHaveBeenCalledWith( + "test@example.com", + expect.anything(), + expect.anything(), + mockLogger, + logMeta + ); + + expect(mockRes.sendStatus).toHaveBeenCalledWith(200); + }); + + it("handles decodedComment as a direct array", async () => { + const arrayComment = [{ jobid: "c1ffe09c-e7d4-46b3-aac5-f23e39563181", amount: 0.01 }]; + + await handleCommentBasedPayment(values, arrayComment, mockLogger, logMeta, mockRes); + + expect(gqlRequestMock.request).toHaveBeenCalledTimes(3); + expect(mockRes.sendStatus).toHaveBeenCalledWith(200); + }); + + it("does not send email if origin is OneLink but userEmail is missing", async () => { + const commentWithoutEmail = { + payments: decodedComment.payments + // no userEmail + }; + + const oneLinkValues = { ...values, origin: "OneLink" }; + + await handleCommentBasedPayment(oneLinkValues, commentWithoutEmail, mockLogger, logMeta, mockRes); + + expect(sendPaymentNotificationEmailMock).not.toHaveBeenCalled(); + expect(mockRes.sendStatus).toHaveBeenCalledWith(200); + }); + + it("logs important stages of the process", async () => { + await handleCommentBasedPayment(values, decodedComment, mockLogger, logMeta, mockRes); + + const logCalls = mockLogger.log.mock.calls.map(([tag]) => tag); + + expect(logCalls).toContain("intellipay-postback-parsed-comment"); + expect(logCalls).toContain("intellipay-postback-payment-success"); + }); + + it("handles missing payment_map safely", async () => { + gqlRequestMock.request.mockReset(); // 🧹 Clear previous .mockResolvedValueOnce calls + + gqlRequestMock.request + .mockResolvedValueOnce({ + jobs: [{ id: "job1", shopid: "shop1" }] + }) + .mockResolvedValueOnce({ + bodyshops_by_pk: { + id: "shop1", + intellipay_config: null + } + }) + .mockResolvedValueOnce({ + insert_payments: { + returning: [{ id: "payment1" }] + } + }); + + await handleCommentBasedPayment(values, decodedComment, mockLogger, logMeta, mockRes); + + expect(getPaymentTypeMock).toHaveBeenCalledWith(undefined, "Amex"); + expect(mockRes.sendStatus).toHaveBeenCalledWith(200); + }); +}); diff --git a/server/intellipay/lib/tests/handleInvoiceBasedPayment.test.js b/server/intellipay/lib/tests/handleInvoiceBasedPayment.test.js new file mode 100644 index 000000000..b2a886e2f --- /dev/null +++ b/server/intellipay/lib/tests/handleInvoiceBasedPayment.test.js @@ -0,0 +1,129 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import mockRequire from "mock-require"; + +const gqlRequestMock = { request: vi.fn() }; +const getPaymentTypeMock = vi.fn(() => "Visa"); +const handlePaymentValidationErrorMock = vi.fn(); + +let handleInvoiceBasedPayment; + +beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + + mockRequire("../../../graphql-client/graphql-client", { + client: gqlRequestMock + }); + + mockRequire("../getPaymentType", getPaymentTypeMock); + mockRequire("../handlePaymentValidationError", handlePaymentValidationErrorMock); + + handleInvoiceBasedPayment = require("../handleInvoiceBasedPayment"); + + gqlRequestMock.request + .mockResolvedValueOnce({ + jobs: [ + { + id: "job123", + bodyshop: { + id: "shop123", + intellipay_config: { + payment_map: { + visa: "Visa" + } + } + } + } + ] + }) + .mockResolvedValueOnce({ + id: "payment123" + }) + .mockResolvedValueOnce({ + insert_payment_response: { + returning: [{ id: "response123" }] + } + }); +}); + +describe("handleInvoiceBasedPayment", () => { + const mockLogger = { log: vi.fn() }; + const mockRes = { sendStatus: vi.fn() }; + + const values = { + merchantid: "m123", + invoice: "INV-001", + total: 100.0, + authcode: "AUTH123", + cardtype: "visa", + paymentid: "P789" + }; + + const logMeta = { op: "abc123" }; + + it("processes a valid invoice-based payment", async () => { + await handleInvoiceBasedPayment(values, mockLogger, logMeta, mockRes); + + expect(gqlRequestMock.request).toHaveBeenCalledTimes(3); + expect(getPaymentTypeMock).toHaveBeenCalledWith({ visa: "Visa" }, "visa"); + expect(mockRes.sendStatus).toHaveBeenCalledWith(200); + expect(handlePaymentValidationErrorMock).not.toHaveBeenCalled(); + }); + + it("handles missing merchantid with validation error", async () => { + const invalidValues = { ...values, merchantid: undefined }; + + await handleInvoiceBasedPayment(invalidValues, mockLogger, logMeta, mockRes); + + expect(handlePaymentValidationErrorMock).toHaveBeenCalledWith( + mockRes, + mockLogger, + "intellipay-postback-no-merchantid", + "Merchant ID is missing", + logMeta + ); + expect(gqlRequestMock.request).not.toHaveBeenCalled(); + }); + + it("handles job not found with validation error", async () => { + gqlRequestMock.request.mockReset(); + gqlRequestMock.request.mockResolvedValueOnce({ jobs: [] }); + + await handleInvoiceBasedPayment(values, mockLogger, logMeta, mockRes); + + expect(handlePaymentValidationErrorMock).toHaveBeenCalledWith( + mockRes, + mockLogger, + "intellipay-postback-job-not-found", + "Job not found", + logMeta + ); + }); + + it("handles missing bodyshop with validation error", async () => { + gqlRequestMock.request.mockReset(); + gqlRequestMock.request.mockResolvedValueOnce({ + jobs: [{ id: "job123", bodyshop: null }] + }); + + await handleInvoiceBasedPayment(values, mockLogger, logMeta, mockRes); + + expect(handlePaymentValidationErrorMock).toHaveBeenCalledWith( + mockRes, + mockLogger, + "intellipay-postback-bodyshop-not-found", + "Bodyshop not found", + logMeta + ); + }); + + it("logs all expected stages of the process", async () => { + await handleInvoiceBasedPayment(values, mockLogger, logMeta, mockRes); + + const logTags = mockLogger.log.mock.calls.map(([tag]) => tag); + + expect(logTags).toContain("intellipay-postback-invoice-job-fetched"); + expect(logTags).toContain("intellipay-postback-invoice-payment-success"); + expect(logTags).toContain("intellipay-postback-invoice-response-success"); + }); +}); diff --git a/server/intellipay/intellipay.test.js b/server/intellipay/tests/intellipay.test.js similarity index 96% rename from server/intellipay/intellipay.test.js rename to server/intellipay/tests/intellipay.test.js index 472e2ea0f..689bfa517 100644 --- a/server/intellipay/intellipay.test.js +++ b/server/intellipay/tests/intellipay.test.js @@ -1,10 +1,10 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -const getPaymentType = require("./lib/getPaymentType"); -const decodeComment = require("./lib/decodeComment"); -const getCptellerUrl = require("./lib/getCptellerUrl"); -const handlePaymentValidationError = require("./lib/handlePaymentValidationError"); -const getShopCredentials = require("./lib/getShopCredentials"); +const getPaymentType = require("../lib/getPaymentType"); +const decodeComment = require("../lib/decodeComment"); +const getCptellerUrl = require("../lib/getCptellerUrl"); +const handlePaymentValidationError = require("../lib/handlePaymentValidationError"); +const getShopCredentials = require("../lib/getShopCredentials"); /** * @description Decode a base64-encoded JSON comment