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.*
|
/*.env.*
|
||||||
.idea/*
|
.idea/*
|
||||||
.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_REPORTS_SERVER_URL=https://reports.test.imex.online
|
||||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||||
VITE_APP_INSTANCE=IMEX
|
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_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||||
VITE_APP_COUNTRY=USA
|
VITE_APP_COUNTRY=USA
|
||||||
VITE_APP_INSTANCE=ROME
|
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
|
# Sentry Config File
|
||||||
.sentryclirc
|
.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,
|
"private": true,
|
||||||
"proxy": "http://localhost:4000",
|
"proxy": "http://localhost:4000",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/pro-layout": "^7.22.3",
|
"@ant-design/pro-layout": "^7.22.4",
|
||||||
"@apollo/client": "^3.13.5",
|
"@apollo/client": "^3.13.5",
|
||||||
"@emotion/is-prop-valid": "^1.3.1",
|
"@emotion/is-prop-valid": "^1.3.1",
|
||||||
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
||||||
"@firebase/analytics": "^0.10.12",
|
"@firebase/analytics": "^0.10.12",
|
||||||
"@firebase/app": "^0.11.3",
|
"@firebase/app": "^0.11.4",
|
||||||
"@firebase/auth": "^1.9.1",
|
"@firebase/auth": "^1.10.0",
|
||||||
"@firebase/firestore": "^4.7.10",
|
"@firebase/firestore": "^4.7.10",
|
||||||
"@firebase/messaging": "^0.12.17",
|
"@firebase/messaging": "^0.12.17",
|
||||||
"@jsreport/browser-client": "^3.1.0",
|
"@jsreport/browser-client": "^3.1.0",
|
||||||
"@reduxjs/toolkit": "^2.6.1",
|
"@reduxjs/toolkit": "^2.6.1",
|
||||||
"@sentry/cli": "^2.42.4",
|
"@sentry/cli": "^2.43.0",
|
||||||
"@sentry/react": "^9.9.0",
|
"@sentry/react": "^9.10.1",
|
||||||
"@sentry/vite-plugin": "^3.2.2",
|
"@sentry/vite-plugin": "^3.2.4",
|
||||||
"@splitsoftware/splitio-react": "^2.0.1",
|
"@splitsoftware/splitio-react": "^2.1.0",
|
||||||
"@tanem/react-nprogress": "^5.0.53",
|
"@tanem/react-nprogress": "^5.0.53",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"antd": "^5.24.6",
|
||||||
"antd": "^5.24.5",
|
|
||||||
"apollo-link-logger": "^2.0.1",
|
"apollo-link-logger": "^2.0.1",
|
||||||
"apollo-link-sentry": "^4.2.0",
|
"apollo-link-sentry": "^4.2.0",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
@@ -70,7 +69,7 @@
|
|||||||
"react-resizable": "^3.0.5",
|
"react-resizable": "^3.0.5",
|
||||||
"react-router-dom": "^6.30.0",
|
"react-router-dom": "^6.30.0",
|
||||||
"react-sticky": "^6.0.3",
|
"react-sticky": "^6.0.3",
|
||||||
"react-virtuoso": "^4.12.5",
|
"react-virtuoso": "^4.12.6",
|
||||||
"recharts": "^2.15.0",
|
"recharts": "^2.15.0",
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
"redux-actions": "^3.0.3",
|
"redux-actions": "^3.0.3",
|
||||||
@@ -78,7 +77,7 @@
|
|||||||
"redux-saga": "^1.3.0",
|
"redux-saga": "^1.3.0",
|
||||||
"redux-state-sync": "^3.1.4",
|
"redux-state-sync": "^3.1.4",
|
||||||
"reselect": "^5.1.1",
|
"reselect": "^5.1.1",
|
||||||
"sass": "^1.86.0",
|
"sass": "^1.86.1",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"styled-components": "^6.1.16",
|
"styled-components": "^6.1.16",
|
||||||
"subscriptions-transport-ws": "^0.11.0",
|
"subscriptions-transport-ws": "^0.11.0",
|
||||||
@@ -100,7 +99,14 @@
|
|||||||
"build:production:imex": "env-cmd -f .env.production.imex npm run build",
|
"build:production:imex": "env-cmd -f .env.production.imex npm run build",
|
||||||
"build:production:rome": "env-cmd -f .env.production.rome 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 .",
|
"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": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
@@ -128,7 +134,12 @@
|
|||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@eslint/js": "^9.23.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": "^4.24.4",
|
||||||
"browserslist-to-esbuild": "^2.1.1",
|
"browserslist-to-esbuild": "^2.1.1",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
@@ -136,17 +147,20 @@
|
|||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"eslint-plugin-react": "^7.37.4",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
|
"jsdom": "^26.0.0",
|
||||||
"memfs": "^4.17.0",
|
"memfs": "^4.17.0",
|
||||||
"os-browserify": "^0.3.0",
|
"os-browserify": "^0.3.0",
|
||||||
|
"playwright": "^1.51.1",
|
||||||
"react-error-overlay": "^6.1.0",
|
"react-error-overlay": "^6.1.0",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"source-map-explorer": "^2.5.3",
|
"source-map-explorer": "^2.5.3",
|
||||||
"vite": "^6.2.3",
|
"vite": "^6.2.4",
|
||||||
"vite-plugin-babel": "^1.3.0",
|
"vite-plugin-babel": "^1.3.0",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-plugin-node-polyfills": "^0.23.0",
|
"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",
|
"vite-plugin-style-import": "^2.0.0",
|
||||||
|
"vitest": "^3.1.1",
|
||||||
"workbox-window": "^7.3.0"
|
"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 App from "./App";
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
import themeProvider from "./themeProvider";
|
import themeProvider from "./themeProvider";
|
||||||
|
import { CookiesProvider } from "react-cookie";
|
||||||
|
|
||||||
// Base Split configuration
|
// Base Split configuration
|
||||||
const config = {
|
const config = {
|
||||||
@@ -38,26 +39,28 @@ function AppContainer() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ApolloProvider client={client}>
|
<CookiesProvider>
|
||||||
<ConfigProvider
|
<ApolloProvider client={client}>
|
||||||
input={{ autoComplete: "new-password" }}
|
<ConfigProvider
|
||||||
locale={enLocale}
|
input={{ autoComplete: "new-password" }}
|
||||||
theme={themeProvider}
|
locale={enLocale}
|
||||||
form={{
|
theme={themeProvider}
|
||||||
validateMessages: {
|
form={{
|
||||||
// eslint-disable-next-line no-template-curly-in-string
|
validateMessages: {
|
||||||
required: t("general.validation.required", { label: "${label}" })
|
// eslint-disable-next-line no-template-curly-in-string
|
||||||
}
|
required: t("general.validation.required", { label: "${label}" })
|
||||||
}}
|
}
|
||||||
>
|
}}
|
||||||
<GlobalLoadingBar />
|
>
|
||||||
<SplitFactoryProvider config={config}>
|
<GlobalLoadingBar />
|
||||||
<SplitClientProvider>
|
<SplitFactoryProvider config={config}>
|
||||||
<App />
|
<SplitClientProvider>
|
||||||
</SplitClientProvider>
|
<App />
|
||||||
</SplitFactoryProvider>
|
</SplitClientProvider>
|
||||||
</ConfigProvider>
|
</SplitFactoryProvider>
|
||||||
</ApolloProvider>
|
</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: [],
|
args: [],
|
||||||
imex: () => {
|
imex: () => {
|
||||||
window.$crisp.push(["set", "user:company", [payload.shopname]]);
|
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) {
|
if (authRecord[0] && authRecord[0].user.validemail) {
|
||||||
window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]);
|
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": {
|
"signinerror": {
|
||||||
"auth/user-disabled": "User account disabled. ",
|
"auth/user-disabled": "User account disabled. ",
|
||||||
"auth/user-not-found": "A user with this email does not exist.",
|
"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": {
|
"signinerror": {
|
||||||
"auth/user-disabled": "",
|
"auth/user-disabled": "",
|
||||||
"auth/user-not-found": "",
|
"auth/user-not-found": "",
|
||||||
"auth/wrong-password": ""
|
"auth/wrong-password": "",
|
||||||
|
"auth/invalid-email": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3679,7 +3679,8 @@
|
|||||||
"signinerror": {
|
"signinerror": {
|
||||||
"auth/user-disabled": "",
|
"auth/user-disabled": "",
|
||||||
"auth/user-not-found": "",
|
"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"],
|
"@sentry/react": ["@sentry/react"],
|
||||||
"@splitsoftware/splitio-react": ["@splitsoftware/splitio-react"],
|
"@splitsoftware/splitio-react": ["@splitsoftware/splitio-react"],
|
||||||
logrocket: ["logrocket"],
|
logrocket: ["logrocket"],
|
||||||
"@firebase/analytics": ["@firebase/analytics"],
|
firebase: [
|
||||||
"@firebase/app": ["@firebase/app"],
|
"@firebase/analytics",
|
||||||
"@firebase/firestore": ["@firebase/firestore"],
|
"@firebase/app",
|
||||||
"@firebase/auth": ["@firebase/auth"],
|
"@firebase/firestore",
|
||||||
"@firebase/messaging": ["@firebase/messaging"],
|
"@firebase/auth",
|
||||||
|
"@firebase/messaging"
|
||||||
|
],
|
||||||
markerjs2: ["markerjs2"],
|
markerjs2: ["markerjs2"],
|
||||||
"@apollo/client": ["@apollo/client"],
|
"@apollo/client": ["@apollo/client"],
|
||||||
"libphonenumber-js": ["libphonenumber-js"]
|
"libphonenumber-js": ["libphonenumber-js"]
|
||||||
@@ -218,7 +220,13 @@ export default defineConfig({
|
|||||||
"react-router-dom",
|
"react-router-dom",
|
||||||
"dayjs",
|
"dayjs",
|
||||||
"redux",
|
"redux",
|
||||||
"react-redux"
|
"react-redux",
|
||||||
|
"@firebase/app",
|
||||||
|
"@firebase/analytics",
|
||||||
|
"@firebase/firestore",
|
||||||
|
"@firebase/auth",
|
||||||
|
"@firebase/messaging",
|
||||||
|
"@firebase/util"
|
||||||
],
|
],
|
||||||
esbuildOptions: {
|
esbuildOptions: {
|
||||||
// Update for Vite 6: Use proper file extensions
|
// 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": "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",
|
"setup:win": "rimraf node_modules && npm i && cd client && rimraf node_modules && npm i",
|
||||||
"start": "node server.js",
|
"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": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-cloudwatch-logs": "^3.772.0",
|
"@aws-sdk/client-cloudwatch-logs": "^3.777.0",
|
||||||
"@aws-sdk/client-elasticache": "^3.772.0",
|
"@aws-sdk/client-elasticache": "^3.777.0",
|
||||||
"@aws-sdk/client-s3": "^3.772.0",
|
"@aws-sdk/client-s3": "^3.779.0",
|
||||||
"@aws-sdk/client-secrets-manager": "^3.772.0",
|
"@aws-sdk/client-secrets-manager": "^3.777.0",
|
||||||
"@aws-sdk/client-ses": "^3.772.0",
|
"@aws-sdk/client-ses": "^3.777.0",
|
||||||
"@aws-sdk/credential-provider-node": "^3.772.0",
|
"@aws-sdk/credential-provider-node": "^3.777.0",
|
||||||
"@aws-sdk/lib-storage": "^3.743.0",
|
"@aws-sdk/lib-storage": "^3.779.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.731.1",
|
"@aws-sdk/s3-request-presigner": "^3.779.0",
|
||||||
"@opensearch-project/opensearch": "^2.13.0",
|
"@opensearch-project/opensearch": "^2.13.0",
|
||||||
"@socket.io/admin-ui": "^0.5.1",
|
"@socket.io/admin-ui": "^0.5.1",
|
||||||
"@socket.io/redis-adapter": "^8.3.0",
|
"@socket.io/redis-adapter": "^8.3.0",
|
||||||
@@ -30,8 +32,7 @@
|
|||||||
"bee-queue": "^1.7.1",
|
"bee-queue": "^1.7.1",
|
||||||
"better-queue": "^3.8.12",
|
"better-queue": "^3.8.12",
|
||||||
"bluebird": "^3.7.2",
|
"bluebird": "^3.7.2",
|
||||||
"body-parser": "^1.20.3",
|
"bullmq": "^5.45.2",
|
||||||
"bullmq": "^5.44.4",
|
|
||||||
"chart.js": "^4.4.8",
|
"chart.js": "^4.4.8",
|
||||||
"cloudinary": "^2.6.0",
|
"cloudinary": "^2.6.0",
|
||||||
"compression": "^1.8.0",
|
"compression": "^1.8.0",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"crisp-status-reporter": "^1.2.2",
|
"crisp-status-reporter": "^1.2.2",
|
||||||
"csrf": "^3.1.0",
|
"csrf": "^3.1.0",
|
||||||
"dd-trace": "^5.43.0",
|
"dd-trace": "^5.45.0",
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.1",
|
||||||
@@ -80,8 +81,11 @@
|
|||||||
"eslint": "^9.23.0",
|
"eslint": "^9.23.0",
|
||||||
"eslint-plugin-react": "^7.37.4",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
|
"mock-require": "^3.0.3",
|
||||||
"p-limit": "^3.1.0",
|
"p-limit": "^3.1.0",
|
||||||
"prettier": "^3.5.3",
|
"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 }) => {
|
const applyMiddleware = ({ app }) => {
|
||||||
app.use(compression());
|
app.use(compression());
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.use(bodyParser.json({ limit: "50mb" }));
|
app.use(express.json({ limit: "50mb" }));
|
||||||
app.use(bodyParser.urlencoded({ limit: "50mb", extended: true }));
|
app.use(express.urlencoded({ limit: "50mb", extended: true }));
|
||||||
app.use(cors({ credentials: true, exposedHeaders: ["set-cookie"] }));
|
app.use(cors({ credentials: true, exposedHeaders: ["set-cookie"] }));
|
||||||
|
|
||||||
// Helper middleware
|
// Helper middleware
|
||||||
|
|||||||
@@ -39,12 +39,14 @@ exports.createShop = async (req, res) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await client.request(
|
const result = await client.request(
|
||||||
`mutation INSERT_BODYSHOPS($bs: bodyshops_insert_input!){
|
`mutation INSERT_BODYSHOPS($bs: bodyshops_insert_input!) {
|
||||||
insert_bodyshops_one(object:$bs){
|
insert_bodyshops_one(object: $bs) {
|
||||||
id
|
id
|
||||||
|
vendors {
|
||||||
}
|
id
|
||||||
}`,
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
{
|
{
|
||||||
bs: {
|
bs: {
|
||||||
...bodyshop,
|
...bodyshop,
|
||||||
@@ -54,12 +56,39 @@ exports.createShop = async (req, res) => {
|
|||||||
{ countertype: "ihbnum", count: 1 },
|
{ countertype: "ihbnum", count: 1 },
|
||||||
{ countertype: "paymentnum", 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) {
|
} 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);
|
res.status(500).json(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2768,6 +2768,9 @@ exports.GET_BODYSHOP_BY_ID = `
|
|||||||
id
|
id
|
||||||
md_order_statuses
|
md_order_statuses
|
||||||
shopname
|
shopname
|
||||||
|
imexshopid
|
||||||
|
intellipay_config
|
||||||
|
state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -2829,3 +2832,15 @@ exports.GET_DOCUMENTS_BY_IDS = `
|
|||||||
takenat
|
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 Dinero = require("dinero.js");
|
||||||
const qs = require("query-string");
|
const qs = require("query-string");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const moment = require("moment");
|
|
||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
const { sendTaskEmail } = require("../email/sendemail");
|
const { isEmpty, isNumber } = require("lodash");
|
||||||
const generateEmailTemplate = require("../email/generateTemplate");
|
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";
|
/**
|
||||||
|
* @description Get lightbox credentials for the shop
|
||||||
const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager");
|
* @param req
|
||||||
const { InstanceRegion, InstanceEndpoints } = require("../utils/instanceMgr");
|
* @param res
|
||||||
|
* @returns {Promise<void>}
|
||||||
const client = new SecretsManagerClient({
|
*/
|
||||||
region: InstanceRegion()
|
const lightboxCredentials = async (req, res) => {
|
||||||
});
|
|
||||||
|
|
||||||
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) => {
|
|
||||||
const decodedComment = decodeComment(req.body?.comment);
|
const decodedComment = decodeComment(req.body?.comment);
|
||||||
const logMeta = {
|
const logMeta = {
|
||||||
iPayData: req.body?.iPayData,
|
iPayData: req.body?.iPayData,
|
||||||
@@ -73,17 +32,17 @@ exports.lightbox_credentials = async (req, res) => {
|
|||||||
|
|
||||||
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
||||||
|
|
||||||
if (shopCredentials.error) {
|
if (shopCredentials?.error) {
|
||||||
logger.log("intellipay-credentials-error", "ERROR", req.user?.email, null, {
|
logger.log("intellipay-credentials-error", "ERROR", req.user?.email, null, {
|
||||||
message: shopCredentials.error?.message,
|
message: shopCredentials.error?.message,
|
||||||
...logMeta
|
...logMeta
|
||||||
});
|
});
|
||||||
res.json({
|
|
||||||
|
return res.json({
|
||||||
message: shopCredentials.error?.message,
|
message: shopCredentials.error?.message,
|
||||||
type: "intellipay-credentials-error",
|
type: "intellipay-credentials-error",
|
||||||
...logMeta
|
...logMeta
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -94,7 +53,10 @@ exports.lightbox_credentials = async (req, res) => {
|
|||||||
...shopCredentials,
|
...shopCredentials,
|
||||||
operatingenv: "businessattended"
|
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);
|
const response = await axios(options);
|
||||||
@@ -104,13 +66,14 @@ exports.lightbox_credentials = async (req, res) => {
|
|||||||
...logMeta
|
...logMeta
|
||||||
});
|
});
|
||||||
|
|
||||||
res.send(response.data);
|
return res.send(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("intellipay-lightbox-error", "ERROR", req.user?.email, null, {
|
logger.log("intellipay-lightbox-error", "ERROR", req.user?.email, null, {
|
||||||
message: error?.message,
|
message: error?.message,
|
||||||
...logMeta
|
...logMeta
|
||||||
});
|
});
|
||||||
res.json({
|
|
||||||
|
return res.json({
|
||||||
message: error?.message,
|
message: error?.message,
|
||||||
type: "intellipay-lightbox-error",
|
type: "intellipay-lightbox-error",
|
||||||
...logMeta
|
...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 decodedComment = decodeComment(req.body.iPayData?.comment);
|
||||||
const logResponseMeta = {
|
const logResponseMeta = {
|
||||||
iPayData: req.body?.iPayData,
|
iPayData: req.body?.iPayData,
|
||||||
@@ -136,18 +105,17 @@ exports.payment_refund = async (req, res) => {
|
|||||||
|
|
||||||
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
||||||
|
|
||||||
if (shopCredentials.error) {
|
if (shopCredentials?.error) {
|
||||||
logger.log("intellipay-refund-credentials-error", "ERROR", req.user?.email, null, {
|
logger.log("intellipay-refund-credentials-error", "ERROR", req.user?.email, null, {
|
||||||
credentialsError: shopCredentials.error,
|
credentialsError: shopCredentials.error,
|
||||||
...logResponseMeta
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(400).json({
|
return res.status(400).json({
|
||||||
credentialsError: shopCredentials.error,
|
credentialsError: shopCredentials.error,
|
||||||
type: "intellipay-refund-credentials-error",
|
type: "intellipay-refund-credentials-error",
|
||||||
...logResponseMeta
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -160,7 +128,11 @@ exports.payment_refund = async (req, res) => {
|
|||||||
paymentid: req.body.paymentid,
|
paymentid: req.body.paymentid,
|
||||||
amount: req.body.amount
|
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, {
|
logger.log("intellipay-refund-options-prepared", "DEBUG", req.user?.email, null, {
|
||||||
@@ -175,13 +147,14 @@ exports.payment_refund = async (req, res) => {
|
|||||||
...logResponseMeta
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
|
|
||||||
res.send(response.data);
|
return res.send(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("intellipay-refund-error", "ERROR", req.user?.email, null, {
|
logger.log("intellipay-refund-error", "ERROR", req.user?.email, null, {
|
||||||
message: error?.message,
|
message: error?.message,
|
||||||
...logResponseMeta
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
res.status(500).json({
|
|
||||||
|
return res.status(500).json({
|
||||||
message: error?.message,
|
message: error?.message,
|
||||||
type: "intellipay-refund-error",
|
type: "intellipay-refund-error",
|
||||||
...logResponseMeta
|
...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 decodedComment = decodeComment(req.body.comment);
|
||||||
const logResponseMeta = {
|
const logResponseMeta = {
|
||||||
iPayData: req.body?.iPayData,
|
iPayData: req.body?.iPayData,
|
||||||
@@ -209,17 +188,17 @@ exports.generate_payment_url = async (req, res) => {
|
|||||||
|
|
||||||
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
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, {
|
logger.log("intellipay-generate-payment-url-credentials-error", "ERROR", req.user?.email, null, {
|
||||||
message: shopCredentials.error?.message,
|
message: shopCredentials.error?.message,
|
||||||
...logResponseMeta
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
res.status(400).json({
|
|
||||||
|
return res.status(400).json({
|
||||||
message: shopCredentials.error?.message,
|
message: shopCredentials.error?.message,
|
||||||
type: "intellipay-generate-payment-url-credentials-error",
|
type: "intellipay-generate-payment-url-credentials-error",
|
||||||
...logResponseMeta
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -234,7 +213,10 @@ exports.generate_payment_url = async (req, res) => {
|
|||||||
invoice: req.body.invoice,
|
invoice: req.body.invoice,
|
||||||
createshorturl: true
|
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, {
|
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
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
|
|
||||||
res.send(response.data);
|
return res.send(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("intellipay-generate-payment-url-error", "ERROR", req.user?.email, null, {
|
logger.log("intellipay-generate-payment-url-error", "ERROR", req.user?.email, null, {
|
||||||
message: error?.message,
|
message: error?.message,
|
||||||
...logResponseMeta
|
...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 = {
|
const logResponseMeta = {
|
||||||
bodyshop: {
|
bodyshop: {
|
||||||
id: req.body?.bodyshop?.id,
|
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);
|
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, {
|
logger.log("intellipay-checkfee-skip", "DEBUG", req.user?.email, null, {
|
||||||
message: "Amount is zero or undefined, skipping fee check.",
|
message: "Amount is zero or undefined, skipping fee check.",
|
||||||
...logResponseMeta
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
res.json({ fee: 0 });
|
|
||||||
return;
|
return res.json({ fee: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
||||||
|
|
||||||
if (shopCredentials.error) {
|
if (shopCredentials?.error) {
|
||||||
logger.log("intellipay-checkfee-credentials-error", "ERROR", req.user?.email, null, {
|
logger.log("intellipay-checkfee-credentials-error", "ERROR", req.user?.email, null, {
|
||||||
message: shopCredentials.error?.message,
|
message: shopCredentials.error?.message,
|
||||||
...logResponseMeta
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
res.status(400).json({ error: shopCredentials.error?.message, ...logResponseMeta });
|
|
||||||
return;
|
return res.status(400).json({ error: shopCredentials.error?.message, ...logResponseMeta });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -312,7 +301,7 @@ exports.checkfee = async (req, res) => {
|
|||||||
},
|
},
|
||||||
{ sort: false } // Ensure query string order is preserved
|
{ 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, {
|
logger.log("intellipay-checkfee-options-prepared", "DEBUG", req.user?.email, null, {
|
||||||
@@ -327,196 +316,92 @@ exports.checkfee = async (req, res) => {
|
|||||||
message: response.data?.error,
|
message: response.data?.error,
|
||||||
...logResponseMeta
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
res.status(400).json({
|
|
||||||
|
return res.status(400).json({
|
||||||
error: response.data?.error,
|
error: response.data?.error,
|
||||||
type: "intellipay-checkfee-api-error",
|
type: "intellipay-checkfee-api-error",
|
||||||
...logResponseMeta
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
} else if (response.data < 0) {
|
}
|
||||||
|
|
||||||
|
if (response.data < 0) {
|
||||||
logger.log("intellipay-checkfee-negative-fee", "ERROR", req.user?.email, null, {
|
logger.log("intellipay-checkfee-negative-fee", "ERROR", req.user?.email, null, {
|
||||||
message: "Fee amount returned is negative.",
|
message: "Fee amount returned is negative.",
|
||||||
...logResponseMeta
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
res.json({
|
|
||||||
|
return res.json({
|
||||||
error: "Fee amount negative. Check API credentials & account configuration.",
|
error: "Fee amount negative. Check API credentials & account configuration.",
|
||||||
...logResponseMeta,
|
...logResponseMeta,
|
||||||
type: "intellipay-checkfee-negative-fee"
|
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) {
|
} catch (error) {
|
||||||
logger.log("intellipay-checkfee-error", "ERROR", req.user?.email, null, {
|
logger.log("intellipay-checkfee-error", "ERROR", req.user?.email, null, {
|
||||||
message: error?.message,
|
message: error?.message,
|
||||||
...logResponseMeta
|
...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 { body: values } = req;
|
||||||
const decodedComment = decodeComment(values?.comment);
|
const decodedComment = decodeComment(values?.comment);
|
||||||
const logResponseMeta = {
|
const logMeta = { iprequest: values, decodedComment };
|
||||||
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;
|
|
||||||
|
|
||||||
logger.log("intellipay-postback-received", "DEBUG", req.user?.email, null, logResponseMeta);
|
logger.log("intellipay-postback-received", "DEBUG", "api", null, logMeta);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ((!values.invoice || values.invoice === "") && !decodedComment) {
|
// Handle empty/invalid requests
|
||||||
//invoice is specified through the pay link. Comment by IO.
|
if (isEmpty(values?.invoice) && !decodedComment) {
|
||||||
logger.log("intellipay-postback-ignored", "DEBUG", req.user?.email, null, {
|
logger.log("intellipay-postback-ignored", "DEBUG", "api", null, {
|
||||||
message: "No invoice or comment provided",
|
message: "No invoice or comment provided",
|
||||||
...logResponseMeta
|
...logMeta
|
||||||
});
|
});
|
||||||
res.sendStatus(200);
|
return res.sendStatus(200);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process payment based on data type
|
||||||
if (decodedComment) {
|
if (decodedComment) {
|
||||||
//Shifted the order to have this first to retain backwards compatibility for the old style of short link.
|
return await handleCommentBasedPayment(values, decodedComment, logger, logMeta, res);
|
||||||
//This has been triggered by IO and may have multiple jobs.
|
} else if (values?.invoice) {
|
||||||
const parsedComment = decodedComment;
|
return await handleInvoiceBasedPayment(values, logger, logMeta, res);
|
||||||
|
} else {
|
||||||
logger.log("intellipay-postback-parsed-comment", "DEBUG", req.user?.email, null, {
|
// This should be caught by first validation, but as a safeguard
|
||||||
parsedComment,
|
logValidationError("intellipay-postback-invalid", "No valid invoice or comment provided", logMeta);
|
||||||
...logResponseMeta
|
return res.status(400).send("Bad Request: No valid invoice or comment provided");
|
||||||
});
|
|
||||||
|
|
||||||
//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);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("intellipay-postback-error", "ERROR", req.user?.email, null, {
|
logger.log("intellipay-postback-error", "ERROR", "api", null, {
|
||||||
message: error?.message,
|
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 express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
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("/lightbox_credentials", validateFirebaseIdTokenMiddleware, lightboxCredentials);
|
||||||
router.post("/payment_refund", validateFirebaseIdTokenMiddleware, payment_refund);
|
router.post("/payment_refund", validateFirebaseIdTokenMiddleware, paymentRefund);
|
||||||
router.post("/generate_payment_url", validateFirebaseIdTokenMiddleware, generate_payment_url);
|
router.post("/generate_payment_url", validateFirebaseIdTokenMiddleware, generatePaymentUrl);
|
||||||
router.post("/checkfee", validateFirebaseIdTokenMiddleware, checkfee);
|
router.post("/checkfee", validateFirebaseIdTokenMiddleware, checkFee);
|
||||||
router.post("/postback", postback);
|
router.post("/postback", postBack);
|
||||||
|
|
||||||
module.exports = router;
|
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