Compare commits
88 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54ac0c84a7 | ||
|
|
4d59798d8d | ||
|
|
f95dab544d | ||
|
|
41e43dda96 | ||
|
|
cec60db78c | ||
|
|
f556d59ad7 | ||
|
|
09c4662436 | ||
|
|
9bf6ba9cf0 | ||
|
|
7843ca9b1a | ||
|
|
c78b9866a3 | ||
|
|
c8701aba63 | ||
|
|
09c1a8ae35 | ||
|
|
0ef2814de3 | ||
|
|
8e105f0b36 | ||
|
|
ba4da3e35c | ||
|
|
f6e65f82e5 | ||
|
|
8b7bb099f3 | ||
|
|
663d91b648 | ||
|
|
2a7686ec75 | ||
|
|
549cb56cdf | ||
|
|
146bb6c5c0 | ||
|
|
67b6da7c31 | ||
|
|
624894621b | ||
|
|
3fba215266 | ||
|
|
bbf291e8f3 | ||
|
|
341fc09c22 | ||
|
|
fb30529808 | ||
|
|
46999145fc | ||
|
|
9d1f810af2 | ||
|
|
b9693aae95 | ||
|
|
02f5f1985c | ||
|
|
37edceee84 | ||
|
|
1fd63012b0 | ||
|
|
cf084fa168 | ||
|
|
96af289640 | ||
|
|
f8df351de6 | ||
|
|
b8c096f4ff | ||
|
|
93ad23b615 | ||
|
|
0a918535bb | ||
|
|
4863b16b5f | ||
|
|
a27f5e2153 | ||
|
|
3ffea50072 | ||
|
|
34af7d3880 | ||
|
|
4432721c27 | ||
|
|
65ad4d9426 | ||
|
|
18924b4f08 | ||
|
|
c524f5f0e0 | ||
|
|
2fbac78eec | ||
|
|
4734971d48 | ||
|
|
fc1055c644 | ||
|
|
24798390b5 | ||
|
|
a992dead04 | ||
|
|
f039cd8d0d | ||
|
|
494e691230 | ||
|
|
4cc7366290 | ||
|
|
fd9d660a61 | ||
|
|
0b5bd4f718 | ||
|
|
7511b42bd4 | ||
|
|
26f94c4d5b | ||
|
|
aa55f4840b | ||
|
|
2810428d19 | ||
|
|
83da64f96b | ||
|
|
1f8d027f97 | ||
|
|
2f8ba20a5b | ||
|
|
b525f920e0 | ||
|
|
91fe6745fe | ||
|
|
b9073fe3f5 | ||
|
|
2c95b49ae1 | ||
|
|
9bde06e110 | ||
|
|
30449ca113 | ||
|
|
0405d19f98 | ||
|
|
2c5310403b | ||
|
|
e2ef4f1caf | ||
|
|
b32a2d4d86 | ||
|
|
7c92484ae0 | ||
|
|
67cada5d8e | ||
|
|
4bf68b637f | ||
|
|
b40c433865 | ||
|
|
55ed499ab5 | ||
|
|
353bc3bc05 | ||
|
|
df5c96345c | ||
|
|
2c7c187c45 | ||
|
|
3a5a78d60a | ||
|
|
6dd2871c07 | ||
|
|
ef36ab9da0 | ||
|
|
a917f6bcdf | ||
|
|
c5d6457146 | ||
|
|
f3831e934f |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -121,3 +121,10 @@ logs/oAuthClient-log.log
|
||||
/*.env.*
|
||||
.idea/*
|
||||
.idea
|
||||
|
||||
# Vitest
|
||||
vitest-report*/
|
||||
vitest-coverage/
|
||||
*.vitest.log
|
||||
test-output.txt
|
||||
|
||||
|
||||
@@ -12,3 +12,5 @@ VITE_APP_AXIOS_BASE_API_URL=/api/
|
||||
VITE_APP_REPORTS_SERVER_URL=https://reports.test.imex.online
|
||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||
VITE_APP_INSTANCE=IMEX
|
||||
TEST_USERNAME="test@imex.dev"
|
||||
TEST_PASSWORD="test123"
|
||||
|
||||
@@ -14,3 +14,5 @@ VITE_APP_REPORTS_SERVER_URL=https://reports.test.romeonline.io
|
||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||
VITE_APP_COUNTRY=USA
|
||||
VITE_APP_INSTANCE=ROME
|
||||
TEST_USERNAME="test@imex.dev"
|
||||
TEST_PASSWORD="test123"
|
||||
|
||||
11
client/.gitignore
vendored
11
client/.gitignore
vendored
@@ -1,3 +1,14 @@
|
||||
# Vitest
|
||||
vitest-report*/
|
||||
vitest-coverage/
|
||||
*.vitest.log
|
||||
test-output.txt
|
||||
|
||||
# Playwright
|
||||
playwright-report/
|
||||
test-results/
|
||||
playwright/.cache/
|
||||
*.playwright.log
|
||||
|
||||
# Sentry Config File
|
||||
.sentryclirc
|
||||
|
||||
2591
client/package-lock.json
generated
2591
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,24 +8,23 @@
|
||||
"private": true,
|
||||
"proxy": "http://localhost:4000",
|
||||
"dependencies": {
|
||||
"@ant-design/pro-layout": "^7.22.3",
|
||||
"@ant-design/pro-layout": "^7.22.4",
|
||||
"@apollo/client": "^3.13.5",
|
||||
"@emotion/is-prop-valid": "^1.3.1",
|
||||
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
||||
"@firebase/analytics": "^0.10.12",
|
||||
"@firebase/app": "^0.11.3",
|
||||
"@firebase/auth": "^1.9.1",
|
||||
"@firebase/app": "^0.11.4",
|
||||
"@firebase/auth": "^1.10.0",
|
||||
"@firebase/firestore": "^4.7.10",
|
||||
"@firebase/messaging": "^0.12.17",
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@reduxjs/toolkit": "^2.6.1",
|
||||
"@sentry/cli": "^2.42.4",
|
||||
"@sentry/react": "^9.9.0",
|
||||
"@sentry/vite-plugin": "^3.2.2",
|
||||
"@splitsoftware/splitio-react": "^2.0.1",
|
||||
"@sentry/cli": "^2.43.0",
|
||||
"@sentry/react": "^9.10.1",
|
||||
"@sentry/vite-plugin": "^3.2.4",
|
||||
"@splitsoftware/splitio-react": "^2.1.0",
|
||||
"@tanem/react-nprogress": "^5.0.53",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"antd": "^5.24.5",
|
||||
"antd": "^5.24.6",
|
||||
"apollo-link-logger": "^2.0.1",
|
||||
"apollo-link-sentry": "^4.2.0",
|
||||
"autosize": "^6.0.1",
|
||||
@@ -70,7 +69,7 @@
|
||||
"react-resizable": "^3.0.5",
|
||||
"react-router-dom": "^6.30.0",
|
||||
"react-sticky": "^6.0.3",
|
||||
"react-virtuoso": "^4.12.5",
|
||||
"react-virtuoso": "^4.12.6",
|
||||
"recharts": "^2.15.0",
|
||||
"redux": "^5.0.1",
|
||||
"redux-actions": "^3.0.3",
|
||||
@@ -78,7 +77,7 @@
|
||||
"redux-saga": "^1.3.0",
|
||||
"redux-state-sync": "^3.1.4",
|
||||
"reselect": "^5.1.1",
|
||||
"sass": "^1.86.0",
|
||||
"sass": "^1.86.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"styled-components": "^6.1.16",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
@@ -100,7 +99,14 @@
|
||||
"build:production:imex": "env-cmd -f .env.production.imex npm run build",
|
||||
"build:production:rome": "env-cmd -f .env.production.rome npm run build",
|
||||
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .",
|
||||
"eulaize": "node src/utils/eulaize.js"
|
||||
"eulaize": "node src/utils/eulaize.js",
|
||||
"test:unit": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:e2e:imex": "playwright test --config playwright.config.js",
|
||||
"test:e2e:rome": "playwright test --config playwright.rome.config.js",
|
||||
"test:e2e:imex:headed": "playwright test --config playwright.config.js --headed",
|
||||
"test:e2e:rome:headed": "playwright test --config playwright.rome.config.js --headed",
|
||||
"test:e2e:report": "playwright show-report"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -128,7 +134,12 @@
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@eslint/js": "^9.23.0",
|
||||
"@sentry/webpack-plugin": "^3.2.2",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@sentry/webpack-plugin": "^3.2.4",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"browserslist": "^4.24.4",
|
||||
"browserslist-to-esbuild": "^2.1.1",
|
||||
"chalk": "^5.4.1",
|
||||
@@ -136,17 +147,20 @@
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"globals": "^15.15.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"memfs": "^4.17.0",
|
||||
"os-browserify": "^0.3.0",
|
||||
"playwright": "^1.51.1",
|
||||
"react-error-overlay": "^6.1.0",
|
||||
"redux-logger": "^3.0.6",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"vite": "^6.2.3",
|
||||
"vite": "^6.2.4",
|
||||
"vite-plugin-babel": "^1.3.0",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-node-polyfills": "^0.23.0",
|
||||
"vite-plugin-pwa": "^0.21.2",
|
||||
"vite-plugin-pwa": "^1.0.0",
|
||||
"vite-plugin-style-import": "^2.0.0",
|
||||
"vitest": "^3.1.1",
|
||||
"workbox-window": "^7.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
25
client/playwright.config.js
Normal file
25
client/playwright.config.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineConfig } from "@playwright/test";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config({
|
||||
path: "./.env.development.imex",
|
||||
prefix: "TEST_"
|
||||
});
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests/e2e",
|
||||
testMatch: "*.e2e.js",
|
||||
timeout: 60 * 1000,
|
||||
reporter: [["list"], ["html"]],
|
||||
use: {
|
||||
baseURL: "https://localhost:3000",
|
||||
browser: "chromium",
|
||||
ignoreHTTPSErrors: true
|
||||
},
|
||||
webServer: {
|
||||
command: "npm run start:imex",
|
||||
ignoreHTTPSErrors: true,
|
||||
url: "https://localhost:3000/health", // Health check endpoint will tell us when the server is ready
|
||||
reuseExistingServer: !process.env.CI // Reuse server locally, not in CI
|
||||
}
|
||||
});
|
||||
25
client/playwright.rome.config.js
Normal file
25
client/playwright.rome.config.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineConfig } from "@playwright/test";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config({
|
||||
path: "./.env.development.rome",
|
||||
prefix: "TEST_"
|
||||
});
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests/e2e",
|
||||
testMatch: "*.e2e.js",
|
||||
timeout: 60 * 1000,
|
||||
reporter: [["list"], ["html"]],
|
||||
use: {
|
||||
baseURL: "https://localhost:3000",
|
||||
browser: "chromium",
|
||||
ignoreHTTPSErrors: true
|
||||
},
|
||||
webServer: {
|
||||
command: "npm run start:rome",
|
||||
ignoreHTTPSErrors: true,
|
||||
url: "https://localhost:3000/health", // Health check endpoint will tell us when the server is ready
|
||||
reuseExistingServer: !process.env.CI // Reuse server locally, not in CI
|
||||
}
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import client from "../utils/GraphQLClient";
|
||||
import App from "./App";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import themeProvider from "./themeProvider";
|
||||
import { CookiesProvider } from "react-cookie";
|
||||
|
||||
// Base Split configuration
|
||||
const config = {
|
||||
@@ -38,26 +39,28 @@ function AppContainer() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ApolloProvider client={client}>
|
||||
<ConfigProvider
|
||||
input={{ autoComplete: "new-password" }}
|
||||
locale={enLocale}
|
||||
theme={themeProvider}
|
||||
form={{
|
||||
validateMessages: {
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
required: t("general.validation.required", { label: "${label}" })
|
||||
}
|
||||
}}
|
||||
>
|
||||
<GlobalLoadingBar />
|
||||
<SplitFactoryProvider config={config}>
|
||||
<SplitClientProvider>
|
||||
<App />
|
||||
</SplitClientProvider>
|
||||
</SplitFactoryProvider>
|
||||
</ConfigProvider>
|
||||
</ApolloProvider>
|
||||
<CookiesProvider>
|
||||
<ApolloProvider client={client}>
|
||||
<ConfigProvider
|
||||
input={{ autoComplete: "new-password" }}
|
||||
locale={enLocale}
|
||||
theme={themeProvider}
|
||||
form={{
|
||||
validateMessages: {
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
required: t("general.validation.required", { label: "${label}" })
|
||||
}
|
||||
}}
|
||||
>
|
||||
<GlobalLoadingBar />
|
||||
<SplitFactoryProvider config={config}>
|
||||
<SplitClientProvider>
|
||||
<App />
|
||||
</SplitClientProvider>
|
||||
</SplitFactoryProvider>
|
||||
</ConfigProvider>
|
||||
</ApolloProvider>
|
||||
</CookiesProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Alert component should render Alert component 1`] = `ShallowWrapper {}`;
|
||||
@@ -1,19 +0,0 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import Alert from "./alert.component";
|
||||
|
||||
describe("Alert component", () => {
|
||||
let wrapper;
|
||||
beforeEach(() => {
|
||||
const mockProps = {
|
||||
type: "error",
|
||||
message: "Test error message."
|
||||
};
|
||||
|
||||
wrapper = shallow(<Alert {...mockProps} />);
|
||||
});
|
||||
|
||||
it("should render Alert component", () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
31
client/src/components/alert/alert.component.test.jsx
Normal file
31
client/src/components/alert/alert.component.test.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import AlertComponent from "./alert.component";
|
||||
|
||||
describe("AlertComponent", () => {
|
||||
it("renders with default props", () => {
|
||||
render(<AlertComponent message="Default Alert" />);
|
||||
expect(screen.getByText("Default Alert")).toBeInTheDocument();
|
||||
expect(screen.getByRole("alert")).toHaveClass("ant-alert");
|
||||
});
|
||||
|
||||
it("applies type prop correctly", () => {
|
||||
render(<AlertComponent message="Success Alert" type="success" />);
|
||||
const alert = screen.getByRole("alert");
|
||||
expect(screen.getByText("Success Alert")).toBeInTheDocument();
|
||||
expect(alert).toHaveClass("ant-alert-success");
|
||||
});
|
||||
|
||||
it("displays description when provided", () => {
|
||||
render(<AlertComponent message="Error Alert" description="Something went wrong" type="error" />);
|
||||
expect(screen.getByText("Error Alert")).toBeInTheDocument();
|
||||
expect(screen.getByText("Something went wrong")).toBeInTheDocument();
|
||||
expect(screen.getByRole("alert")).toHaveClass("ant-alert-error");
|
||||
});
|
||||
|
||||
it("is closable and shows icon when props are set", () => {
|
||||
render(<AlertComponent message="Warning Alert" type="warning" showIcon closable />);
|
||||
expect(screen.getByText("Warning Alert")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /close/i })).toBeInTheDocument(); // Close button
|
||||
});
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AllocationsAssignmentComponent component should create an allocation on save 1`] = `ReactWrapper {}`;
|
||||
|
||||
exports[`AllocationsAssignmentComponent component should render AllocationsAssignmentComponent component 1`] = `ReactWrapper {}`;
|
||||
@@ -1,35 +0,0 @@
|
||||
import { mount } from "enzyme";
|
||||
import React from "react";
|
||||
import { MockBodyshop } from "../../utils/TestingHelpers";
|
||||
import { AllocationsAssignmentComponent } from "./allocations-assignment.component";
|
||||
|
||||
describe("AllocationsAssignmentComponent component", () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockProps = {
|
||||
bodyshop: MockBodyshop,
|
||||
handleAssignment: jest.fn(),
|
||||
assignment: {},
|
||||
setAssignment: jest.fn(),
|
||||
visibilityState: [false, jest.fn()],
|
||||
maxHours: 4
|
||||
};
|
||||
|
||||
wrapper = mount(<AllocationsAssignmentComponent {...mockProps} />);
|
||||
});
|
||||
|
||||
it("should render AllocationsAssignmentComponent component", () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render a list of employees", () => {
|
||||
const empList = wrapper.find("#employeeSelector");
|
||||
expect(empList.children()).to.have.lengthOf(2);
|
||||
});
|
||||
|
||||
it("should create an allocation on save", () => {
|
||||
wrapper.find("Button").simulate("click");
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -340,6 +340,7 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
||||
args: [],
|
||||
imex: () => {
|
||||
window.$crisp.push(["set", "user:company", [payload.shopname]]);
|
||||
window.$crisp.push(["set", "session:segments", [[`region:${payload.region_config}`]]]);
|
||||
if (authRecord[0] && authRecord[0].user.validemail) {
|
||||
window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]);
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import { configure } from "enzyme";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
|
||||
configure({ adapter: new Adapter() });
|
||||
@@ -3679,7 +3679,8 @@
|
||||
"signinerror": {
|
||||
"auth/user-disabled": "User account disabled. ",
|
||||
"auth/user-not-found": "A user with this email does not exist.",
|
||||
"auth/wrong-password": "The email and password combination you provided is incorrect."
|
||||
"auth/wrong-password": "The email and password combination you provided is incorrect.",
|
||||
"auth/invalid-email": "A user with this email does not exist."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3679,7 +3679,8 @@
|
||||
"signinerror": {
|
||||
"auth/user-disabled": "",
|
||||
"auth/user-not-found": "",
|
||||
"auth/wrong-password": ""
|
||||
"auth/wrong-password": "",
|
||||
"auth/invalid-email": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3679,7 +3679,8 @@
|
||||
"signinerror": {
|
||||
"auth/user-disabled": "",
|
||||
"auth/user-not-found": "",
|
||||
"auth/wrong-password": ""
|
||||
"auth/wrong-password": "",
|
||||
"auth/invalid-email": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
export const MockBodyshop = {
|
||||
address1: "123 Fake St",
|
||||
address2: "Unit #100",
|
||||
city: "Vancouver",
|
||||
country: "Canada",
|
||||
created_at: "2019-12-10T20:03:06.420853+00:00",
|
||||
email: "snaptsoft@gmail.com",
|
||||
federal_tax_id: "GST10150492",
|
||||
id: "52b7357c-0edd-4c95-85c3-dfdbcdfad9ac",
|
||||
insurance_vendor_id: "F123456",
|
||||
logo_img_path: "https://www.snapt.ca/assets/logo-placeholder.png",
|
||||
md_ro_statuses: {
|
||||
statuses: [
|
||||
"Open",
|
||||
"Scheduled",
|
||||
"Arrived",
|
||||
"Repair Plan",
|
||||
"Parts",
|
||||
"Body",
|
||||
"Prep",
|
||||
"Paint",
|
||||
"Reassembly",
|
||||
"Sublet",
|
||||
"Detail",
|
||||
"Completed",
|
||||
"Delivered",
|
||||
"Invoiced",
|
||||
"Exported"
|
||||
],
|
||||
open_statuses: ["Open", "Scheduled", "Arrived", "Repair Plan", "Parts", "Body", "Prep", "Paint"],
|
||||
default_arrived: "Arrived",
|
||||
default_exported: "Exported",
|
||||
default_imported: "Open",
|
||||
default_invoiced: "Invoiced",
|
||||
default_completed: "Completed",
|
||||
default_delivered: "Delivered",
|
||||
default_scheduled: "Scheduled"
|
||||
},
|
||||
md_order_statuses: {
|
||||
statuses: ["Ordered", "Received", "Canceled", "Backordered"],
|
||||
default_bo: "Backordered",
|
||||
default_ordered: "Ordered",
|
||||
default_canceled: "Canceled",
|
||||
default_received: "Received"
|
||||
},
|
||||
shopname: "Testing Collision",
|
||||
state: "BC",
|
||||
state_tax_id: "PST1000-2991",
|
||||
updated_at: "2020-03-23T22:06:03.509544+00:00",
|
||||
zip_post: "V6B 1M9",
|
||||
region_config: "CA_BC",
|
||||
md_responsibility_centers: {
|
||||
costs: [
|
||||
"Aftermarket",
|
||||
"ATS",
|
||||
"Body",
|
||||
"Detail",
|
||||
"Daignostic",
|
||||
"Electrical",
|
||||
"Chrome",
|
||||
"Frame",
|
||||
"Mechanical",
|
||||
"Refinish",
|
||||
"Structural",
|
||||
"Existing",
|
||||
"Glass",
|
||||
"LKQ",
|
||||
"OEM",
|
||||
"OEM Partial",
|
||||
"Re-cored",
|
||||
"Remanufactured",
|
||||
"Other",
|
||||
"Sublet",
|
||||
"Towing"
|
||||
],
|
||||
profits: [
|
||||
"Aftermarket",
|
||||
"ATS",
|
||||
"Body",
|
||||
"Detail",
|
||||
"Daignostic",
|
||||
"Electrical",
|
||||
"Chrome",
|
||||
"Frame",
|
||||
"Mechanical",
|
||||
"Refinish",
|
||||
"Structural",
|
||||
"Existing",
|
||||
"Glass",
|
||||
"LKQ",
|
||||
"OEM",
|
||||
"OEM Partial",
|
||||
"Re-cored",
|
||||
"Remanufactured",
|
||||
"Other",
|
||||
"Sublet",
|
||||
"Towing"
|
||||
],
|
||||
defaults: {
|
||||
ATS: "ATS",
|
||||
LAB: "Body",
|
||||
LAD: "Diagnostic",
|
||||
LAE: "Electrical",
|
||||
LAF: "Frame",
|
||||
LAG: "Glass",
|
||||
LAM: "Mechanical",
|
||||
LAR: "Refinish",
|
||||
LAS: "Structural",
|
||||
LAU: "Detail",
|
||||
PAA: "Aftermarket",
|
||||
PAC: "Chrome",
|
||||
PAL: "LKQ",
|
||||
PAM: "Remanufactured",
|
||||
PAN: "OEM",
|
||||
PAO: "Other",
|
||||
PAP: "OEM Partial",
|
||||
PAR: "16",
|
||||
TOW: "Towing"
|
||||
}
|
||||
},
|
||||
employees: [
|
||||
{
|
||||
id: "075b744c-8919-49ca-abb2-ccd51040326d",
|
||||
first_name: "Patrick",
|
||||
last_name: "BODY123",
|
||||
employee_number: "101",
|
||||
cost_center: "Body",
|
||||
__typename: "employees"
|
||||
},
|
||||
{
|
||||
id: "8cc787d3-1cfe-49d3-8a15-8469cd5c2e41",
|
||||
first_name: "Patrick",
|
||||
last_name: "Painter",
|
||||
employee_number: "10211",
|
||||
cost_center: "REFINISH",
|
||||
__typename: "employees"
|
||||
}
|
||||
]
|
||||
};
|
||||
14
client/tests/Button.test.jsx
Normal file
14
client/tests/Button.test.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
// Button.test.jsx
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { Button } from "antd";
|
||||
import "antd/dist/reset.css"; // Optional: include if needed for styling reset
|
||||
|
||||
describe("AntD Button", () => {
|
||||
it("renders with correct text", () => {
|
||||
render(<Button>Click me</Button>);
|
||||
const button = screen.getByRole("button", { name: /click me/i });
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
6
client/tests/e2e/homepage.e2e.js
Normal file
6
client/tests/e2e/homepage.e2e.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("homepage loads correctly", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page.locator("h1")).toContainText("ImEX Online");
|
||||
});
|
||||
28
client/tests/e2e/signin.e2e.js
Normal file
28
client/tests/e2e/signin.e2e.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { login } from "./utils/login";
|
||||
|
||||
test.describe("SignInComponent", () => {
|
||||
test("successfully logs in with valid credentials", async ({ page }) => {
|
||||
const email = process.env.TEST_USERNAME;
|
||||
const password = process.env.TEST_PASSWORD;
|
||||
|
||||
await login(page, { email, password });
|
||||
|
||||
// Additional assertions after login (optional)
|
||||
await expect(page).toHaveURL(/\/manage\//);
|
||||
});
|
||||
|
||||
test("displays error on invalid credentials", async ({ page }) => {
|
||||
await page.goto("/"); // Adjust if login route differs
|
||||
|
||||
// Fill form with invalid credentials
|
||||
await page.fill('input[placeholder="Username"]', "wronguser@example.com");
|
||||
await page.fill('input[placeholder="Password"]', "wrongpassword");
|
||||
await page.click("button.login-btn");
|
||||
|
||||
// Check for error alert
|
||||
const alert = page.locator(".ant-alert-error");
|
||||
await expect(alert).toBeVisible();
|
||||
await expect(alert).toContainText("A user with this email does not exist.");
|
||||
});
|
||||
});
|
||||
21
client/tests/e2e/utils/login.js
Normal file
21
client/tests/e2e/utils/login.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
export async function login(page, { email, password }) {
|
||||
// Navigate to the login page
|
||||
await page.goto("/"); // Adjust if your login route differs (e.g., '/login')
|
||||
|
||||
// Fill email field
|
||||
await page.fill('input[placeholder="Username"]', email); // Matches Ant Design Input placeholder
|
||||
|
||||
// Fill password field
|
||||
await page.fill('input[placeholder="Password"]', password);
|
||||
|
||||
// Click login button
|
||||
await page.click("button.login-btn");
|
||||
|
||||
// Wait for navigation or success indicator (e.g., redirect to /manage/)
|
||||
await page.waitForURL(/\/manage\//, { timeout: 10000 }); // Adjust based on redirect
|
||||
|
||||
// Verify successful login (e.g., check for a dashboard element)
|
||||
await expect(page.locator("text=Manage")).toBeVisible(); // Adjust to your app’s post-login UI
|
||||
}
|
||||
5
client/tests/setup.js
Normal file
5
client/tests/setup.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import { afterEach } from "vitest";
|
||||
import { cleanup } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom";
|
||||
|
||||
afterEach(() => cleanup());
|
||||
27
client/tests/setupI18n.js
Normal file
27
client/tests/setupI18n.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import en_Translation from "../src/translations/en_us/common.json";
|
||||
import es_Translation from "../src/translations/es/common.json";
|
||||
import fr_Translation from "../src/translations/fr/common.json";
|
||||
|
||||
const resources = {
|
||||
"en-US": en_Translation,
|
||||
"fr-CA": fr_Translation,
|
||||
"es-MX": es_Translation
|
||||
};
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
resources,
|
||||
lng: "en-US", // Default to en-US for tests (no LanguageDetector)
|
||||
fallbackLng: "en-US",
|
||||
debug: false, // Disable debug in tests
|
||||
react: {
|
||||
useSuspense: false // Disable Suspense for Vitest
|
||||
},
|
||||
interpolation: {
|
||||
escapeValue: false, // React handles XSS
|
||||
skipOnVariables: false
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -191,11 +191,13 @@ export default defineConfig({
|
||||
"@sentry/react": ["@sentry/react"],
|
||||
"@splitsoftware/splitio-react": ["@splitsoftware/splitio-react"],
|
||||
logrocket: ["logrocket"],
|
||||
"@firebase/analytics": ["@firebase/analytics"],
|
||||
"@firebase/app": ["@firebase/app"],
|
||||
"@firebase/firestore": ["@firebase/firestore"],
|
||||
"@firebase/auth": ["@firebase/auth"],
|
||||
"@firebase/messaging": ["@firebase/messaging"],
|
||||
firebase: [
|
||||
"@firebase/analytics",
|
||||
"@firebase/app",
|
||||
"@firebase/firestore",
|
||||
"@firebase/auth",
|
||||
"@firebase/messaging"
|
||||
],
|
||||
markerjs2: ["markerjs2"],
|
||||
"@apollo/client": ["@apollo/client"],
|
||||
"libphonenumber-js": ["libphonenumber-js"]
|
||||
@@ -218,7 +220,13 @@ export default defineConfig({
|
||||
"react-router-dom",
|
||||
"dayjs",
|
||||
"redux",
|
||||
"react-redux"
|
||||
"react-redux",
|
||||
"@firebase/app",
|
||||
"@firebase/analytics",
|
||||
"@firebase/firestore",
|
||||
"@firebase/auth",
|
||||
"@firebase/messaging",
|
||||
"@firebase/util"
|
||||
],
|
||||
esbuildOptions: {
|
||||
// Update for Vite 6: Use proper file extensions
|
||||
|
||||
19
client/vitest.config.js
Normal file
19
client/vitest.config.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
globals: true,
|
||||
setupFiles: "./tests/setup.js",
|
||||
testTimeout: 10000 // 10 seconds (in milliseconds)
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
silenceDeprecations: ["import"] // Suppress @import warnings
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."bodyshops" add column "intellipay_merchant_id" text
|
||||
null unique;
|
||||
2774
package-lock.json
generated
2774
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@@ -10,17 +10,19 @@
|
||||
"setup": "rm -rf node_modules && npm i && cd client && rm -rf node_modules && npm i",
|
||||
"setup:win": "rimraf node_modules && npm i && cd client && rimraf node_modules && npm i",
|
||||
"start": "node server.js",
|
||||
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\""
|
||||
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\"",
|
||||
"test:unit": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-cloudwatch-logs": "^3.772.0",
|
||||
"@aws-sdk/client-elasticache": "^3.772.0",
|
||||
"@aws-sdk/client-s3": "^3.772.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.772.0",
|
||||
"@aws-sdk/client-ses": "^3.772.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.772.0",
|
||||
"@aws-sdk/lib-storage": "^3.743.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.731.1",
|
||||
"@aws-sdk/client-cloudwatch-logs": "^3.777.0",
|
||||
"@aws-sdk/client-elasticache": "^3.777.0",
|
||||
"@aws-sdk/client-s3": "^3.779.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.777.0",
|
||||
"@aws-sdk/client-ses": "^3.777.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.777.0",
|
||||
"@aws-sdk/lib-storage": "^3.779.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.779.0",
|
||||
"@opensearch-project/opensearch": "^2.13.0",
|
||||
"@socket.io/admin-ui": "^0.5.1",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
@@ -30,8 +32,7 @@
|
||||
"bee-queue": "^1.7.1",
|
||||
"better-queue": "^3.8.12",
|
||||
"bluebird": "^3.7.2",
|
||||
"body-parser": "^1.20.3",
|
||||
"bullmq": "^5.44.4",
|
||||
"bullmq": "^5.45.2",
|
||||
"chart.js": "^4.4.8",
|
||||
"cloudinary": "^2.6.0",
|
||||
"compression": "^1.8.0",
|
||||
@@ -39,7 +40,7 @@
|
||||
"cors": "2.8.5",
|
||||
"crisp-status-reporter": "^1.2.2",
|
||||
"csrf": "^3.1.0",
|
||||
"dd-trace": "^5.43.0",
|
||||
"dd-trace": "^5.45.0",
|
||||
"dinero.js": "^1.9.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.1",
|
||||
@@ -80,8 +81,11 @@
|
||||
"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"
|
||||
"source-map-explorer": "^2.5.2",
|
||||
"supertest": "^7.1.0",
|
||||
"vitest": "^3.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,8 +84,8 @@ const SOCKETIO_CORS_ORIGIN_DEV = ["http://localhost:3333", "https://localhost:33
|
||||
const applyMiddleware = ({ app }) => {
|
||||
app.use(compression());
|
||||
app.use(cookieParser());
|
||||
app.use(bodyParser.json({ limit: "50mb" }));
|
||||
app.use(bodyParser.urlencoded({ limit: "50mb", extended: true }));
|
||||
app.use(express.json({ limit: "50mb" }));
|
||||
app.use(express.urlencoded({ limit: "50mb", extended: true }));
|
||||
app.use(cors({ credentials: true, exposedHeaders: ["set-cookie"] }));
|
||||
|
||||
// Helper middleware
|
||||
|
||||
@@ -39,12 +39,14 @@ exports.createShop = async (req, res) => {
|
||||
|
||||
try {
|
||||
const result = await client.request(
|
||||
`mutation INSERT_BODYSHOPS($bs: bodyshops_insert_input!){
|
||||
insert_bodyshops_one(object:$bs){
|
||||
id
|
||||
|
||||
}
|
||||
}`,
|
||||
`mutation INSERT_BODYSHOPS($bs: bodyshops_insert_input!) {
|
||||
insert_bodyshops_one(object: $bs) {
|
||||
id
|
||||
vendors {
|
||||
id
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{
|
||||
bs: {
|
||||
...bodyshop,
|
||||
@@ -54,12 +56,39 @@ exports.createShop = async (req, res) => {
|
||||
{ countertype: "ihbnum", count: 1 },
|
||||
{ countertype: "paymentnum", count: 1 }
|
||||
]
|
||||
},
|
||||
vendors: {
|
||||
data: [{ name: "In-House" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
res.json(result);
|
||||
const bodyshopId = result.insert_bodyshops_one.id;
|
||||
const vendorId = result.insert_bodyshops_one.vendors[0].id;
|
||||
|
||||
if (!bodyshopId || !vendorId) {
|
||||
throw new Error("Failed to create bodyshop or vendor");
|
||||
}
|
||||
|
||||
const updateBodyshop = await client.request(
|
||||
`mutation UPDATE_BODYSHOP($id: uuid!, $inhousevendorid: uuid!) {
|
||||
update_bodyshops_by_pk(pk_columns: { id: $id }, _set: { inhousevendorid: $inhousevendorid }) {
|
||||
id
|
||||
}
|
||||
}`,
|
||||
{
|
||||
id: bodyshopId,
|
||||
inhousevendorid: vendorId
|
||||
}
|
||||
);
|
||||
res.status(200).json(updateBodyshop);
|
||||
} catch (error) {
|
||||
logger.log("admin-create-shop-error", "error", req.user.email, null, {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
request: req.body,
|
||||
ioadmin: true
|
||||
});
|
||||
res.status(500).json(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2768,6 +2768,9 @@ exports.GET_BODYSHOP_BY_ID = `
|
||||
id
|
||||
md_order_statuses
|
||||
shopname
|
||||
imexshopid
|
||||
intellipay_config
|
||||
state
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -2829,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
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
@@ -1,63 +1,22 @@
|
||||
const path = require("path");
|
||||
require("dotenv").config({
|
||||
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
||||
});
|
||||
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 { 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");
|
||||
|
||||
const domain = process.env.NODE_ENV ? "secure" : "test";
|
||||
|
||||
const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager");
|
||||
const { InstanceRegion, InstanceEndpoints } = require("../utils/instanceMgr");
|
||||
|
||||
const client = new SecretsManagerClient({
|
||||
region: InstanceRegion()
|
||||
});
|
||||
|
||||
const gqlClient = require("../graphql-client/graphql-client").client;
|
||||
|
||||
const getShopCredentials = async (bodyshop) => {
|
||||
// Development only
|
||||
if (process.env.NODE_ENV === undefined) {
|
||||
return {
|
||||
merchantkey: process.env.INTELLIPAY_MERCHANTKEY,
|
||||
apikey: process.env.INTELLIPAY_APIKEY
|
||||
};
|
||||
}
|
||||
|
||||
// Production code
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const decodeComment = (comment) => {
|
||||
try {
|
||||
return comment ? JSON.parse(Buffer.from(comment, "base64").toString()) : null;
|
||||
} catch (error) {
|
||||
return null; // Handle malformed base64 string gracefully
|
||||
}
|
||||
};
|
||||
|
||||
exports.lightbox_credentials = async (req, res) => {
|
||||
/**
|
||||
* @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,
|
||||
@@ -73,17 +32,17 @@ exports.lightbox_credentials = async (req, res) => {
|
||||
|
||||
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
||||
|
||||
if (shopCredentials.error) {
|
||||
if (shopCredentials?.error) {
|
||||
logger.log("intellipay-credentials-error", "ERROR", req.user?.email, null, {
|
||||
message: shopCredentials.error?.message,
|
||||
...logMeta
|
||||
});
|
||||
res.json({
|
||||
|
||||
return res.json({
|
||||
message: shopCredentials.error?.message,
|
||||
type: "intellipay-credentials-error",
|
||||
...logMeta
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -94,7 +53,10 @@ exports.lightbox_credentials = 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);
|
||||
@@ -104,13 +66,14 @@ exports.lightbox_credentials = async (req, res) => {
|
||||
...logMeta
|
||||
});
|
||||
|
||||
res.send(response.data);
|
||||
return res.send(response.data);
|
||||
} catch (error) {
|
||||
logger.log("intellipay-lightbox-error", "ERROR", req.user?.email, null, {
|
||||
message: error?.message,
|
||||
...logMeta
|
||||
});
|
||||
res.json({
|
||||
|
||||
return res.json({
|
||||
message: error?.message,
|
||||
type: "intellipay-lightbox-error",
|
||||
...logMeta
|
||||
@@ -118,7 +81,13 @@ exports.lightbox_credentials = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
exports.payment_refund = async (req, res) => {
|
||||
/**
|
||||
* @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,
|
||||
@@ -136,18 +105,17 @@ exports.payment_refund = async (req, res) => {
|
||||
|
||||
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
||||
|
||||
if (shopCredentials.error) {
|
||||
if (shopCredentials?.error) {
|
||||
logger.log("intellipay-refund-credentials-error", "ERROR", req.user?.email, null, {
|
||||
credentialsError: shopCredentials.error,
|
||||
...logResponseMeta
|
||||
});
|
||||
|
||||
res.status(400).json({
|
||||
return res.status(400).json({
|
||||
credentialsError: shopCredentials.error,
|
||||
type: "intellipay-refund-credentials-error",
|
||||
...logResponseMeta
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -160,7 +128,11 @@ exports.payment_refund = 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, {
|
||||
@@ -175,13 +147,14 @@ exports.payment_refund = async (req, res) => {
|
||||
...logResponseMeta
|
||||
});
|
||||
|
||||
res.send(response.data);
|
||||
return res.send(response.data);
|
||||
} catch (error) {
|
||||
logger.log("intellipay-refund-error", "ERROR", req.user?.email, null, {
|
||||
message: error?.message,
|
||||
...logResponseMeta
|
||||
});
|
||||
res.status(500).json({
|
||||
|
||||
return res.status(500).json({
|
||||
message: error?.message,
|
||||
type: "intellipay-refund-error",
|
||||
...logResponseMeta
|
||||
@@ -189,7 +162,13 @@ exports.payment_refund = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
exports.generate_payment_url = async (req, res) => {
|
||||
/**
|
||||
* @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,
|
||||
@@ -209,17 +188,17 @@ exports.generate_payment_url = async (req, res) => {
|
||||
|
||||
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
||||
|
||||
if (shopCredentials.error) {
|
||||
if (shopCredentials?.error) {
|
||||
logger.log("intellipay-generate-payment-url-credentials-error", "ERROR", req.user?.email, null, {
|
||||
message: shopCredentials.error?.message,
|
||||
...logResponseMeta
|
||||
});
|
||||
res.status(400).json({
|
||||
|
||||
return res.status(400).json({
|
||||
message: shopCredentials.error?.message,
|
||||
type: "intellipay-generate-payment-url-credentials-error",
|
||||
...logResponseMeta
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -234,7 +213,10 @@ exports.generate_payment_url = 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, {
|
||||
@@ -250,18 +232,25 @@ exports.generate_payment_url = async (req, res) => {
|
||||
...logResponseMeta
|
||||
});
|
||||
|
||||
res.send(response.data);
|
||||
return res.send(response.data);
|
||||
} catch (error) {
|
||||
logger.log("intellipay-generate-payment-url-error", "ERROR", req.user?.email, null, {
|
||||
message: error?.message,
|
||||
...logResponseMeta
|
||||
});
|
||||
res.status(500).json({ message: error?.message, ...logResponseMeta });
|
||||
|
||||
return res.status(500).json({ message: error?.message, ...logResponseMeta });
|
||||
}
|
||||
};
|
||||
|
||||
//Reference: https://intellipay.com/dist/webapi26.html#operation/fee
|
||||
exports.checkfee = async (req, res) => {
|
||||
/**
|
||||
* @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,
|
||||
@@ -274,24 +263,24 @@ exports.checkfee = async (req, res) => {
|
||||
|
||||
logger.log("intellipay-checkfee-request-received", "DEBUG", req.user?.email, null, logResponseMeta);
|
||||
|
||||
if (!req.body.amount || req.body.amount <= 0) {
|
||||
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
|
||||
});
|
||||
res.json({ fee: 0 });
|
||||
return;
|
||||
|
||||
return res.json({ fee: 0 });
|
||||
}
|
||||
|
||||
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
||||
|
||||
if (shopCredentials.error) {
|
||||
if (shopCredentials?.error) {
|
||||
logger.log("intellipay-checkfee-credentials-error", "ERROR", req.user?.email, null, {
|
||||
message: shopCredentials.error?.message,
|
||||
...logResponseMeta
|
||||
});
|
||||
res.status(400).json({ error: shopCredentials.error?.message, ...logResponseMeta });
|
||||
return;
|
||||
|
||||
return res.status(400).json({ error: shopCredentials.error?.message, ...logResponseMeta });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -312,7 +301,7 @@ exports.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, {
|
||||
@@ -327,196 +316,92 @@ exports.checkfee = async (req, res) => {
|
||||
message: response.data?.error,
|
||||
...logResponseMeta
|
||||
});
|
||||
res.status(400).json({
|
||||
|
||||
return res.status(400).json({
|
||||
error: response.data?.error,
|
||||
type: "intellipay-checkfee-api-error",
|
||||
...logResponseMeta
|
||||
});
|
||||
} else if (response.data < 0) {
|
||||
}
|
||||
|
||||
if (response.data < 0) {
|
||||
logger.log("intellipay-checkfee-negative-fee", "ERROR", req.user?.email, null, {
|
||||
message: "Fee amount returned is negative.",
|
||||
...logResponseMeta
|
||||
});
|
||||
res.json({
|
||||
|
||||
return res.json({
|
||||
error: "Fee amount negative. Check API credentials & account configuration.",
|
||||
...logResponseMeta,
|
||||
type: "intellipay-checkfee-negative-fee"
|
||||
});
|
||||
} else {
|
||||
logger.log("intellipay-checkfee-success", "DEBUG", req.user?.email, null, {
|
||||
fee: response.data,
|
||||
...logResponseMeta
|
||||
});
|
||||
res.json({ fee: response.data, ...logResponseMeta });
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
res.status(500).json({ error: error?.message, logResponseMeta });
|
||||
|
||||
return res.status(500).json({ error: error?.message, logResponseMeta });
|
||||
}
|
||||
};
|
||||
|
||||
exports.postback = async (req, res) => {
|
||||
/**
|
||||
* @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 logResponseMeta = {
|
||||
bodyshop: {
|
||||
id: req.body?.bodyshop?.id,
|
||||
imexshopid: req.body?.bodyshop?.imexshopid,
|
||||
name: req.body?.bodyshop?.shopname,
|
||||
state: req.body?.bodyshop?.state
|
||||
},
|
||||
iprequest: values,
|
||||
decodedComment
|
||||
};
|
||||
const ipMapping = req.body?.bodyshop?.intellipay_config?.payment_map;
|
||||
const logMeta = { iprequest: values, decodedComment };
|
||||
|
||||
logger.log("intellipay-postback-received", "DEBUG", req.user?.email, null, logResponseMeta);
|
||||
logger.log("intellipay-postback-received", "DEBUG", "api", null, logMeta);
|
||||
|
||||
try {
|
||||
if ((!values.invoice || values.invoice === "") && !decodedComment) {
|
||||
//invoice is specified through the pay link. Comment by IO.
|
||||
logger.log("intellipay-postback-ignored", "DEBUG", req.user?.email, null, {
|
||||
// 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
|
||||
});
|
||||
res.sendStatus(200);
|
||||
return;
|
||||
return res.sendStatus(200);
|
||||
}
|
||||
|
||||
// Process payment based on data type
|
||||
if (decodedComment) {
|
||||
//Shifted the order to have this first to retain backwards compatibility for the old style of short link.
|
||||
//This has been triggered by IO and may have multiple jobs.
|
||||
const parsedComment = decodedComment;
|
||||
|
||||
logger.log("intellipay-postback-parsed-comment", "DEBUG", req.user?.email, null, {
|
||||
parsedComment,
|
||||
...logResponseMeta
|
||||
});
|
||||
|
||||
//Adding in the user email to the short pay email.
|
||||
//Need to check this to ensure backwards compatibility for clients that don't update.
|
||||
|
||||
const partialPayments = Array.isArray(parsedComment) ? parsedComment : parsedComment.payments;
|
||||
|
||||
// Fetch jobs by job IDs
|
||||
const jobs = await gqlClient.request(queries.GET_JOBS_BY_PKS, {
|
||||
ids: partialPayments.map((p) => p.jobid)
|
||||
});
|
||||
|
||||
logger.log("intellipay-postback-jobs-fetched", "DEBUG", req.user?.email, null, {
|
||||
jobs,
|
||||
parsedComment,
|
||||
...logResponseMeta
|
||||
});
|
||||
|
||||
// Insert new payments
|
||||
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: jobs.jobs[0].shopid,
|
||||
jobid: p.jobid,
|
||||
declinereason: "Approved",
|
||||
ext_paymentid: values.paymentid,
|
||||
successful: true,
|
||||
response: values
|
||||
}
|
||||
}
|
||||
}))
|
||||
});
|
||||
|
||||
logger.log("intellipay-postback-payment-success", "DEBUG", req.user?.email, 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", req.user?.email, null, {
|
||||
message: error.message,
|
||||
jobs,
|
||||
paymentResult,
|
||||
...logResponseMeta
|
||||
});
|
||||
});
|
||||
}
|
||||
res.sendStatus(200);
|
||||
} else if (values.invoice) {
|
||||
const job = await gqlClient.request(queries.GET_JOB_BY_PK, {
|
||||
id: values.invoice
|
||||
});
|
||||
|
||||
logger.log("intellipay-postback-invoice-job-fetched", "DEBUG", req.user?.email, 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: values.invoice,
|
||||
date: moment(Date.now())
|
||||
}
|
||||
});
|
||||
|
||||
logger.log("intellipay-postback-invoice-payment-success", "DEBUG", req.user?.email, null, {
|
||||
paymentResult,
|
||||
...logResponseMeta
|
||||
});
|
||||
|
||||
const responseResults = await gqlClient.request(queries.INSERT_PAYMENT_RESPONSE, {
|
||||
paymentResponse: {
|
||||
amount: values.total,
|
||||
bodyshopid: job.jobs_by_pk.shopid,
|
||||
paymentid: paymentResult.id,
|
||||
jobid: values.invoice,
|
||||
declinereason: "Approved",
|
||||
ext_paymentid: values.paymentid,
|
||||
successful: true,
|
||||
response: values
|
||||
}
|
||||
});
|
||||
|
||||
logger.log("intellipay-postback-invoice-response-success", "DEBUG", req.user?.email, null, {
|
||||
responseResults,
|
||||
...logResponseMeta
|
||||
});
|
||||
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");
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log("intellipay-postback-error", "ERROR", req.user?.email, null, {
|
||||
logger.log("intellipay-postback-error", "ERROR", "api", null, {
|
||||
message: error?.message,
|
||||
...logResponseMeta
|
||||
...logMeta
|
||||
});
|
||||
res.status(400).json({ successful: false, error: error.message, ...logResponseMeta });
|
||||
return res.status(400).json({ successful: false, error: error.message, ...logMeta });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
lightboxCredentials,
|
||||
paymentRefund,
|
||||
generatePaymentUrl,
|
||||
checkFee,
|
||||
postBack
|
||||
};
|
||||
|
||||
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;
|
||||
152
server/intellipay/lib/tests/handleCommentBasedPayment.test.js
Normal file
152
server/intellipay/lib/tests/handleCommentBasedPayment.test.js
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
129
server/intellipay/lib/tests/handleInvoiceBasedPayment.test.js
Normal file
129
server/intellipay/lib/tests/handleInvoiceBasedPayment.test.js
Normal file
@@ -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");
|
||||
});
|
||||
});
|
||||
277
server/intellipay/lib/tests/intelliPayGeneralLibs.test.js
Normal file
277
server/intellipay/lib/tests/intelliPayGeneralLibs.test.js
Normal file
@@ -0,0 +1,277 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
const getPaymentType = require("../getPaymentType");
|
||||
const decodeComment = require("../decodeComment");
|
||||
const getCptellerUrl = require("../getCptellerUrl");
|
||||
const handlePaymentValidationError = require("../handlePaymentValidationError");
|
||||
const getShopCredentials = require("../getShopCredentials");
|
||||
|
||||
describe("Payment Processing Functions", () => {
|
||||
// DecodeComment Tests
|
||||
describe("decodeComment", () => {
|
||||
it("decodes a valid base64-encoded JSON comment", () => {
|
||||
const encoded = "eyJ0ZXN0IjoiZGF0YSJ9";
|
||||
const expected = { test: "data" };
|
||||
expect(decodeComment(encoded)).toEqual(expected);
|
||||
});
|
||||
|
||||
it("decodes a complex base64-encoded JSON with payments", () => {
|
||||
const encoded = "eyJwYXltZW50cyI6W3siam9iaWQiOiIxMjMifV19";
|
||||
const expected = { payments: [{ jobid: "123" }] };
|
||||
expect(decodeComment(encoded)).toEqual(expected);
|
||||
});
|
||||
|
||||
it("returns null when comment is null", () => {
|
||||
expect(decodeComment(null)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when comment is undefined", () => {
|
||||
expect(decodeComment(undefined)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when comment is an empty string", () => {
|
||||
expect(decodeComment("")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when comment is malformed base64", () => {
|
||||
expect(decodeComment("!@#$%")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when comment is valid base64 but not valid JSON", () => {
|
||||
expect(decodeComment("aW52YWxpZA==")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// GetPaymentType Tests
|
||||
describe("getPaymentType", () => {
|
||||
it("returns mapped value when card type exists in mapping", () => {
|
||||
const ipMapping = { visa: "Visa Card", amex: "American Express" };
|
||||
expect(getPaymentType(ipMapping, "visa")).toBe("Visa Card");
|
||||
});
|
||||
|
||||
it("returns original value when card type not in mapping", () => {
|
||||
const ipMapping = { visa: "Visa Card" };
|
||||
expect(getPaymentType(ipMapping, "mastercard")).toBe("mastercard");
|
||||
});
|
||||
|
||||
it("handles lowercase conversion", () => {
|
||||
const ipMapping = { visa: "Visa Card" };
|
||||
expect(getPaymentType(ipMapping, "VISA")).toBe("Visa Card");
|
||||
});
|
||||
|
||||
it("handles null mapping", () => {
|
||||
expect(getPaymentType(null, "visa")).toBe("visa");
|
||||
});
|
||||
|
||||
it("handles undefined mapping", () => {
|
||||
expect(getPaymentType(undefined, "visa")).toBe("visa");
|
||||
});
|
||||
|
||||
it("handles empty string card type", () => {
|
||||
const ipMapping = { visa: "Visa Card" };
|
||||
expect(getPaymentType(ipMapping, "")).toBe("");
|
||||
});
|
||||
|
||||
it("handles undefined card type", () => {
|
||||
const ipMapping = { visa: "Visa Card" };
|
||||
expect(getPaymentType(ipMapping, undefined)).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
// GetCptellerUrl Tests
|
||||
describe("getCptellerUrl", () => {
|
||||
const originalEnv = process.env.NODE_ENV;
|
||||
|
||||
afterEach(() => {
|
||||
process.env.NODE_ENV = originalEnv;
|
||||
});
|
||||
|
||||
it("uses test domain in non-production environment", () => {
|
||||
process.env.NODE_ENV = "";
|
||||
const url = getCptellerUrl({ apiType: "webapi" });
|
||||
expect(url).toEqual("https://test.cpteller.com/api/webapi.cfc");
|
||||
});
|
||||
|
||||
it("uses secure domain in production environment", () => {
|
||||
process.env.NODE_ENV = "production";
|
||||
const url = getCptellerUrl({ apiType: "webapi" });
|
||||
expect(url).toEqual("https://secure.cpteller.com/api/webapi.cfc");
|
||||
});
|
||||
|
||||
it("adds version number for webapi type", () => {
|
||||
process.env.NODE_ENV = "";
|
||||
const url = getCptellerUrl({ apiType: "webapi", version: "26" });
|
||||
expect(url).toEqual("https://test.cpteller.com/api/26/webapi.cfc");
|
||||
});
|
||||
|
||||
it("constructs custapi URL without version number", () => {
|
||||
process.env.NODE_ENV = "";
|
||||
const url = getCptellerUrl({ apiType: "custapi", version: "26" });
|
||||
expect(url).toEqual("https://test.cpteller.com/api/custapi.cfc");
|
||||
});
|
||||
|
||||
it("adds query parameters to the URL", () => {
|
||||
process.env.NODE_ENV = "";
|
||||
const url = getCptellerUrl({
|
||||
apiType: "webapi",
|
||||
params: { method: "payment_refund", test: "value" }
|
||||
});
|
||||
expect(url).toEqual("https://test.cpteller.com/api/webapi.cfc?method=payment_refund&test=value");
|
||||
});
|
||||
|
||||
it("handles empty params object", () => {
|
||||
process.env.NODE_ENV = "";
|
||||
const url = getCptellerUrl({ apiType: "webapi", params: {} });
|
||||
expect(url).toEqual("https://test.cpteller.com/api/webapi.cfc");
|
||||
});
|
||||
|
||||
it("defaults to webapi when no apiType is provided", () => {
|
||||
process.env.NODE_ENV = "";
|
||||
const url = getCptellerUrl({});
|
||||
expect(url).toEqual("https://test.cpteller.com/api/webapi.cfc");
|
||||
});
|
||||
|
||||
it("combines version and query parameters correctly", () => {
|
||||
process.env.NODE_ENV = "";
|
||||
const url = getCptellerUrl({
|
||||
apiType: "webapi",
|
||||
version: "26",
|
||||
params: { method: "fee" }
|
||||
});
|
||||
expect(url).toEqual("https://test.cpteller.com/api/26/webapi.cfc?method=fee");
|
||||
});
|
||||
});
|
||||
|
||||
// GetShopCredentials Tests
|
||||
describe("getShopCredentials", () => {
|
||||
const originalEnv = { ...process.env };
|
||||
let mockSend;
|
||||
|
||||
beforeEach(() => {
|
||||
mockSend = vi.fn();
|
||||
vi.mock("@aws-sdk/client-secrets-manager", () => {
|
||||
return {
|
||||
SecretsManagerClient: vi.fn(() => ({
|
||||
send: mockSend
|
||||
})),
|
||||
GetSecretValueCommand: vi.fn((input) => input)
|
||||
};
|
||||
});
|
||||
|
||||
process.env.INTELLIPAY_MERCHANTKEY = "test-merchant-key";
|
||||
process.env.INTELLIPAY_APIKEY = "test-api-key";
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
vi.restoreAllMocks();
|
||||
vi.unmock("@aws-sdk/client-secrets-manager");
|
||||
});
|
||||
|
||||
it("returns environment variables in non-production environment", async () => {
|
||||
process.env.NODE_ENV = "development";
|
||||
const result = await getShopCredentials({ imexshopid: "12345" });
|
||||
expect(result).toEqual({
|
||||
merchantkey: "test-merchant-key",
|
||||
apikey: "test-api-key"
|
||||
});
|
||||
expect(mockSend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns undefined when imexshopid is missing in production", async () => {
|
||||
process.env.NODE_ENV = "production";
|
||||
const result = await getShopCredentials({ name: "Test Shop" });
|
||||
expect(result).toBeUndefined();
|
||||
expect(mockSend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns undefined for null bodyshop in production", async () => {
|
||||
process.env.NODE_ENV = "production";
|
||||
const result = await getShopCredentials(null);
|
||||
expect(result).toBeUndefined();
|
||||
expect(mockSend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns undefined for undefined bodyshop in production", async () => {
|
||||
process.env.NODE_ENV = "production";
|
||||
const result = await getShopCredentials(undefined);
|
||||
expect(result).toBeUndefined();
|
||||
expect(mockSend).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// HandlePaymentValidationError Tests
|
||||
describe("handlePaymentValidationError", () => {
|
||||
it("logs error and sends 400 response", () => {
|
||||
const mockLog = vi.fn();
|
||||
const mockLogger = { log: mockLog };
|
||||
const mockRes = {
|
||||
status: vi.fn().mockReturnThis(),
|
||||
send: vi.fn().mockReturnThis()
|
||||
};
|
||||
|
||||
const logCode = "test-validation-error";
|
||||
const message = "Invalid data";
|
||||
const logMeta = { field: "test", value: 123 };
|
||||
|
||||
const result = handlePaymentValidationError(mockRes, mockLogger, logCode, message, logMeta);
|
||||
|
||||
expect(mockLog).toHaveBeenCalledWith(logCode, "ERROR", "api", null, {
|
||||
message,
|
||||
...logMeta
|
||||
});
|
||||
expect(mockRes.status).toHaveBeenCalledWith(400);
|
||||
expect(mockRes.send).toHaveBeenCalledWith(`Bad Request: ${message}`);
|
||||
expect(result).toBe(mockRes);
|
||||
});
|
||||
|
||||
it("formats different error messages correctly", () => {
|
||||
const mockLog = vi.fn();
|
||||
const mockLogger = { log: mockLog };
|
||||
const mockRes = {
|
||||
status: vi.fn().mockReturnThis(),
|
||||
send: vi.fn().mockReturnThis()
|
||||
};
|
||||
|
||||
handlePaymentValidationError(mockRes, mockLogger, "error-code", "Custom error");
|
||||
expect(mockRes.send).toHaveBeenCalledWith("Bad Request: Custom error");
|
||||
});
|
||||
|
||||
it("passes different logCodes to logger", () => {
|
||||
const mockLog = vi.fn();
|
||||
const mockLogger = { log: mockLog };
|
||||
const mockRes = {
|
||||
status: vi.fn().mockReturnThis(),
|
||||
send: vi.fn().mockReturnThis()
|
||||
};
|
||||
|
||||
handlePaymentValidationError(mockRes, mockLogger, "custom-log-code", "Error message");
|
||||
expect(mockLog).toHaveBeenCalledWith("custom-log-code", "ERROR", "api", null, { message: "Error message" });
|
||||
});
|
||||
|
||||
it("works with minimal logMeta", () => {
|
||||
const mockLog = vi.fn();
|
||||
const mockLogger = { log: mockLog };
|
||||
const mockRes = {
|
||||
status: vi.fn().mockReturnThis(),
|
||||
send: vi.fn().mockReturnThis()
|
||||
};
|
||||
|
||||
handlePaymentValidationError(mockRes, mockLogger, "error-code", "Error message", {});
|
||||
expect(mockLog).toHaveBeenCalledWith("error-code", "ERROR", "api", null, { message: "Error message" });
|
||||
});
|
||||
|
||||
it("works with undefined logMeta", () => {
|
||||
const mockLog = vi.fn();
|
||||
const mockLogger = { log: mockLog };
|
||||
const mockRes = {
|
||||
status: vi.fn().mockReturnThis(),
|
||||
send: vi.fn().mockReturnThis()
|
||||
};
|
||||
|
||||
handlePaymentValidationError(mockRes, mockLogger, "error-code", "Error message");
|
||||
expect(mockLog).toHaveBeenCalledWith("error-code", "ERROR", "api", null, { message: "Error message" });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,18 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
||||
const { lightbox_credentials, payment_refund, generate_payment_url, postback, checkfee } = require("../intellipay/intellipay");
|
||||
const {
|
||||
lightboxCredentials,
|
||||
paymentRefund,
|
||||
generatePaymentUrl,
|
||||
postBack,
|
||||
checkFee
|
||||
} = require("../intellipay/intellipay");
|
||||
|
||||
router.post("/lightbox_credentials", validateFirebaseIdTokenMiddleware, lightbox_credentials);
|
||||
router.post("/payment_refund", validateFirebaseIdTokenMiddleware, payment_refund);
|
||||
router.post("/generate_payment_url", validateFirebaseIdTokenMiddleware, generate_payment_url);
|
||||
router.post("/checkfee", validateFirebaseIdTokenMiddleware, checkfee);
|
||||
router.post("/postback", postback);
|
||||
router.post("/lightbox_credentials", validateFirebaseIdTokenMiddleware, lightboxCredentials);
|
||||
router.post("/payment_refund", validateFirebaseIdTokenMiddleware, paymentRefund);
|
||||
router.post("/generate_payment_url", validateFirebaseIdTokenMiddleware, generatePaymentUrl);
|
||||
router.post("/checkfee", validateFirebaseIdTokenMiddleware, checkFee);
|
||||
router.post("/postback", postBack);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
14
server/tests/api.test.js
Normal file
14
server/tests/api.test.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import request from "supertest";
|
||||
import express from "express";
|
||||
|
||||
const app = express();
|
||||
app.get("/api/health", (req, res) => res.json({ status: "ok" }));
|
||||
|
||||
describe("API", () => {
|
||||
it("returns health status", async () => {
|
||||
const response = await request(app).get("/api/health");
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({ status: "ok" });
|
||||
});
|
||||
});
|
||||
11
server/tests/math.test.js
Normal file
11
server/tests/math.test.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
function add(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
describe("Math", () => {
|
||||
it("adds two numbers correctly", () => {
|
||||
expect(add(2, 3)).toBe(5);
|
||||
});
|
||||
});
|
||||
13
vitest.config.js
Normal file
13
vitest.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const { defineConfig } = require("vitest/config");
|
||||
|
||||
module.exports = defineConfig({
|
||||
test: {
|
||||
environment: "node",
|
||||
globals: true,
|
||||
include: [
|
||||
"./server/tests/**/*.{test,spec}.[jt]s", // Existing pattern for /server/tests
|
||||
"./server/**/*.test.js" // New pattern for test.js in server and subfolders
|
||||
],
|
||||
exclude: ["**/client/**", "**/node_modules/**", "**/dist/**"] // Explicitly exclude /client
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user