Merged in release/2026-02-27 (pull request #3085)

Release/2026 02 27
This commit is contained in:
Dave Richer
2026-03-04 17:45:15 +00:00
13 changed files with 779 additions and 298 deletions

View File

@@ -0,0 +1,184 @@
import { ApolloProvider } from "@apollo/client/react";
import * as Sentry from "@sentry/react";
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
import { ConfigProvider, Grid } from "antd";
import enLocale from "antd/es/locale/en_US";
import { useEffect, useMemo } from "react";
import { CookiesProvider } from "react-cookie";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
import { setDarkMode } from "../redux/application/application.actions";
import { selectDarkMode } from "../redux/application/application.selectors";
import { selectCurrentUser } from "../redux/user/user.selectors.js";
import { signOutStart } from "../redux/user/user.actions";
import client from "../utils/GraphQLClient";
import App from "./App";
import getTheme from "./themeProvider";
// Base Split configuration
const config = {
core: {
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
key: "anon"
}
};
function SplitClientProvider({ children }) {
const imexshopid = useSelector((state) => state.user.imexshopid);
const splitClient = useSplitClient({ key: imexshopid || "anon" });
useEffect(() => {
if (import.meta.env.DEV && splitClient && imexshopid) {
console.log(`Split client initialized with key: ${imexshopid}, isReady: ${splitClient.isReady}`);
}
}, [splitClient, imexshopid]);
return children;
}
function AppContainer() {
const { t } = useTranslation();
const dispatch = useDispatch();
const currentUser = useSelector(selectCurrentUser);
const isDarkMode = useSelector(selectDarkMode);
const screens = Grid.useBreakpoint();
const isPhone = !screens.md;
const isUltraWide = Boolean(screens.xxxl);
const theme = useMemo(() => {
const baseTheme = getTheme(isDarkMode);
return {
...baseTheme,
token: {
...(baseTheme.token || {}),
screenXXXL: 2160
},
components: {
...(baseTheme.components || {}),
Table: {
...(baseTheme.components?.Table || {}),
cellFontSizeSM: isPhone ? 12 : 13,
cellFontSizeMD: isPhone ? 13 : isUltraWide ? 15 : 14,
cellFontSize: isUltraWide ? 15 : 14,
cellPaddingInlineSM: isPhone ? 8 : 10,
cellPaddingInlineMD: isPhone ? 10 : 14,
cellPaddingInline: isUltraWide ? 20 : 16,
cellPaddingBlockSM: isPhone ? 8 : 10,
cellPaddingBlockMD: isPhone ? 10 : 12,
cellPaddingBlock: isUltraWide ? 14 : 12,
selectionColumnWidth: isPhone ? 44 : 52
}
}
};
}, [isDarkMode, isPhone, isUltraWide]);
const antdInput = useMemo(() => ({ autoComplete: "new-password" }), []);
const antdTable = useMemo(() => ({ scroll: { x: "max-content" } }), []);
const antdPagination = useMemo(
() => ({
showSizeChanger: !isPhone,
totalBoundaryShowSizeChanger: 100
}),
[isPhone]
);
const antdForm = useMemo(
() => ({
validateMessages: {
required: t("general.validation.required", { label: "${label}" })
}
}),
[t]
);
// Global seamless logout listener with redirect to /signin
useEffect(() => {
const handleSeamlessLogout = (event) => {
if (event.data?.type !== "seamlessLogoutRequest") return;
// Only accept messages from the parent window
if (event.source !== window.parent) return;
const targetOrigin = event.origin || "*";
if (currentUser?.authorized !== true) {
window.parent?.postMessage({ type: "seamlessLogoutResponse", status: "already_logged_out" }, targetOrigin);
return;
}
dispatch(signOutStart());
window.parent?.postMessage({ type: "seamlessLogoutResponse", status: "logged_out" }, targetOrigin);
};
window.addEventListener("message", handleSeamlessLogout);
return () => {
window.removeEventListener("message", handleSeamlessLogout);
};
}, [dispatch, currentUser?.authorized]);
// Update data-theme attribute (no cleanup to avoid transient style churn)
useEffect(() => {
document.documentElement.dataset.theme = isDarkMode ? "dark" : "light";
}, [isDarkMode]);
// Sync darkMode with localStorage
useEffect(() => {
const uid = currentUser?.uid;
if (!uid) {
dispatch(setDarkMode(false));
return;
}
const key = `dark-mode-${uid}`;
const raw = localStorage.getItem(key);
if (raw == null) {
dispatch(setDarkMode(false));
return;
}
try {
dispatch(setDarkMode(Boolean(JSON.parse(raw))));
} catch {
dispatch(setDarkMode(false));
}
}, [currentUser?.uid, dispatch]);
// Persist darkMode
useEffect(() => {
const uid = currentUser?.uid;
if (!uid) return;
localStorage.setItem(`dark-mode-${uid}`, JSON.stringify(isDarkMode));
}, [isDarkMode, currentUser?.uid]);
return (
<CookiesProvider>
<ApolloProvider client={client}>
<ConfigProvider
input={antdInput}
locale={enLocale}
theme={theme}
form={antdForm}
table={antdTable}
pagination={antdPagination}
componentSize={isPhone ? "small" : isUltraWide ? "large" : "middle"}
popupOverflow="viewport"
>
<GlobalLoadingBar />
<SplitFactoryProvider config={config}>
<SplitClientProvider>
<App />
</SplitClientProvider>
</SplitFactoryProvider>
</ConfigProvider>
</ApolloProvider>
</CookiesProvider>
);
}
export default Sentry.withProfiler(AppContainer);

View File

@@ -1,7 +1,7 @@
import { ApolloProvider } from "@apollo/client/react"; import { ApolloProvider } from "@apollo/client/react";
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react"; import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
import { ConfigProvider, Grid } from "antd"; import { ConfigProvider } from "antd";
import enLocale from "antd/es/locale/en_US"; import enLocale from "antd/es/locale/en_US";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { CookiesProvider } from "react-cookie"; import { CookiesProvider } from "react-cookie";
@@ -43,47 +43,10 @@ function AppContainer() {
const currentUser = useSelector(selectCurrentUser); const currentUser = useSelector(selectCurrentUser);
const isDarkMode = useSelector(selectDarkMode); const isDarkMode = useSelector(selectDarkMode);
const screens = Grid.useBreakpoint();
const isPhone = !screens.md;
const isUltraWide = Boolean(screens.xxxl);
const theme = useMemo(() => { const theme = useMemo(() => getTheme(isDarkMode), [isDarkMode]);
const baseTheme = getTheme(isDarkMode);
return {
...baseTheme,
token: {
...(baseTheme.token || {}),
screenXXXL: 2160
},
components: {
...(baseTheme.components || {}),
Table: {
...(baseTheme.components?.Table || {}),
cellFontSizeSM: isPhone ? 12 : 13,
cellFontSizeMD: isPhone ? 13 : isUltraWide ? 15 : 14,
cellFontSize: isUltraWide ? 15 : 14,
cellPaddingInlineSM: isPhone ? 8 : 10,
cellPaddingInlineMD: isPhone ? 10 : 14,
cellPaddingInline: isUltraWide ? 20 : 16,
cellPaddingBlockSM: isPhone ? 8 : 10,
cellPaddingBlockMD: isPhone ? 10 : 12,
cellPaddingBlock: isUltraWide ? 14 : 12,
selectionColumnWidth: isPhone ? 44 : 52
}
}
};
}, [isDarkMode, isPhone, isUltraWide]);
const antdInput = useMemo(() => ({ autoComplete: "new-password" }), []); const antdInput = useMemo(() => ({ autoComplete: "new-password" }), []);
const antdTable = useMemo(() => ({ scroll: { x: "max-content" } }), []);
const antdPagination = useMemo(
() => ({
showSizeChanger: !isPhone,
totalBoundaryShowSizeChanger: 100
}),
[isPhone]
);
const antdForm = useMemo( const antdForm = useMemo(
() => ({ () => ({
@@ -159,16 +122,7 @@ function AppContainer() {
return ( return (
<CookiesProvider> <CookiesProvider>
<ApolloProvider client={client}> <ApolloProvider client={client}>
<ConfigProvider <ConfigProvider input={antdInput} locale={enLocale} theme={theme} form={antdForm}>
input={antdInput}
locale={enLocale}
theme={theme}
form={antdForm}
table={antdTable}
pagination={antdPagination}
componentSize={isPhone ? "small" : isUltraWide ? "large" : "middle"}
popupOverflow="viewport"
>
<GlobalLoadingBar /> <GlobalLoadingBar />
<SplitFactoryProvider config={config}> <SplitFactoryProvider config={config}>
<SplitClientProvider> <SplitClientProvider>

View File

@@ -0,0 +1,184 @@
import { ApolloProvider } from "@apollo/client/react";
import * as Sentry from "@sentry/react";
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
import { ConfigProvider, Grid } from "antd";
import enLocale from "antd/es/locale/en_US";
import { useEffect, useMemo } from "react";
import { CookiesProvider } from "react-cookie";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
import { setDarkMode } from "../redux/application/application.actions";
import { selectDarkMode } from "../redux/application/application.selectors";
import { selectCurrentUser } from "../redux/user/user.selectors.js";
import { signOutStart } from "../redux/user/user.actions";
import client from "../utils/GraphQLClient";
import App from "./App";
import getTheme from "./themeProvider";
// Base Split configuration
const config = {
core: {
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
key: "anon"
}
};
function SplitClientProvider({ children }) {
const imexshopid = useSelector((state) => state.user.imexshopid);
const splitClient = useSplitClient({ key: imexshopid || "anon" });
useEffect(() => {
if (import.meta.env.DEV && splitClient && imexshopid) {
console.log(`Split client initialized with key: ${imexshopid}, isReady: ${splitClient.isReady}`);
}
}, [splitClient, imexshopid]);
return children;
}
function AppContainer() {
const { t } = useTranslation();
const dispatch = useDispatch();
const currentUser = useSelector(selectCurrentUser);
const isDarkMode = useSelector(selectDarkMode);
const screens = Grid.useBreakpoint();
const isPhone = !screens.md;
const isUltraWide = Boolean(screens.xxxl);
const theme = useMemo(() => {
const baseTheme = getTheme(isDarkMode);
return {
...baseTheme,
token: {
...(baseTheme.token || {}),
screenXXXL: 2160
},
components: {
...(baseTheme.components || {}),
Table: {
...(baseTheme.components?.Table || {}),
cellFontSizeSM: isPhone ? 12 : 13,
cellFontSizeMD: isPhone ? 13 : isUltraWide ? 15 : 14,
cellFontSize: isUltraWide ? 15 : 14,
cellPaddingInlineSM: isPhone ? 8 : 10,
cellPaddingInlineMD: isPhone ? 10 : 14,
cellPaddingInline: isUltraWide ? 20 : 16,
cellPaddingBlockSM: isPhone ? 8 : 10,
cellPaddingBlockMD: isPhone ? 10 : 12,
cellPaddingBlock: isUltraWide ? 14 : 12,
selectionColumnWidth: isPhone ? 44 : 52
}
}
};
}, [isDarkMode, isPhone, isUltraWide]);
const antdInput = useMemo(() => ({ autoComplete: "new-password" }), []);
const antdTable = useMemo(() => ({ scroll: { x: "max-content" } }), []);
const antdPagination = useMemo(
() => ({
showSizeChanger: !isPhone,
totalBoundaryShowSizeChanger: 100
}),
[isPhone]
);
const antdForm = useMemo(
() => ({
validateMessages: {
required: t("general.validation.required", { label: "${label}" })
}
}),
[t]
);
// Global seamless logout listener with redirect to /signin
useEffect(() => {
const handleSeamlessLogout = (event) => {
if (event.data?.type !== "seamlessLogoutRequest") return;
// Only accept messages from the parent window
if (event.source !== window.parent) return;
const targetOrigin = event.origin || "*";
if (currentUser?.authorized !== true) {
window.parent?.postMessage({ type: "seamlessLogoutResponse", status: "already_logged_out" }, targetOrigin);
return;
}
dispatch(signOutStart());
window.parent?.postMessage({ type: "seamlessLogoutResponse", status: "logged_out" }, targetOrigin);
};
window.addEventListener("message", handleSeamlessLogout);
return () => {
window.removeEventListener("message", handleSeamlessLogout);
};
}, [dispatch, currentUser?.authorized]);
// Update data-theme attribute (no cleanup to avoid transient style churn)
useEffect(() => {
document.documentElement.dataset.theme = isDarkMode ? "dark" : "light";
}, [isDarkMode]);
// Sync darkMode with localStorage
useEffect(() => {
const uid = currentUser?.uid;
if (!uid) {
dispatch(setDarkMode(false));
return;
}
const key = `dark-mode-${uid}`;
const raw = localStorage.getItem(key);
if (raw == null) {
dispatch(setDarkMode(false));
return;
}
try {
dispatch(setDarkMode(Boolean(JSON.parse(raw))));
} catch {
dispatch(setDarkMode(false));
}
}, [currentUser?.uid, dispatch]);
// Persist darkMode
useEffect(() => {
const uid = currentUser?.uid;
if (!uid) return;
localStorage.setItem(`dark-mode-${uid}`, JSON.stringify(isDarkMode));
}, [isDarkMode, currentUser?.uid]);
return (
<CookiesProvider>
<ApolloProvider client={client}>
<ConfigProvider
input={antdInput}
locale={enLocale}
theme={theme}
form={antdForm}
table={antdTable}
pagination={antdPagination}
componentSize={isPhone ? "small" : isUltraWide ? "large" : "middle"}
popupOverflow="viewport"
>
<GlobalLoadingBar />
<SplitFactoryProvider config={config}>
<SplitClientProvider>
<App />
</SplitClientProvider>
</SplitFactoryProvider>
</ConfigProvider>
</ApolloProvider>
</CookiesProvider>
);
}
export default Sentry.withProfiler(AppContainer);

View File

@@ -471,34 +471,34 @@
// padding: 0; // padding: 0;
//} //}
/* globally allow shrink inside table cells */ ///* globally allow shrink inside table cells */
.prod-list-table .ant-table-cell, //.prod-list-table .ant-table-cell,
.prod-list-table .ant-table-cell > * { //.prod-list-table .ant-table-cell > * {
min-width: 0; // min-width: 0;
} //}
//
/* common AntD offenders */ ///* common AntD offenders */
.prod-list-table > .ant-table-cell .ant-space, //.prod-list-table > .ant-table-cell .ant-space,
.ant-table-cell .ant-space-item { //.ant-table-cell .ant-space-item {
min-width: 0; // min-width: 0;
} //}
//
/* Keep your custom header content on the left, push AntD sorter to the far right */ ///* Keep your custom header content on the left, push AntD sorter to the far right */
.prod-list-table .ant-table-column-sorters { //.prod-list-table .ant-table-column-sorters {
display: flex !important; // display: flex !important;
align-items: center; // align-items: center;
width: 100%; // width: 100%;
} //}
//
.prod-list-table .ant-table-column-title { //.prod-list-table .ant-table-column-title {
flex: 1 1 auto; // flex: 1 1 auto;
min-width: 0; /* allows ellipsis to work */ // min-width: 0; /* allows ellipsis to work */
} //}
//
.prod-list-table .ant-table-column-sorter { //.prod-list-table .ant-table-column-sorter {
margin-left: auto; // margin-left: auto;
flex: 0 0 auto; // flex: 0 0 auto;
} //}
.global-search-autocomplete-fix { .global-search-autocomplete-fix {

View File

@@ -0,0 +1,99 @@
import { Grid, Table } from "antd";
import { useMemo } from "react";
import "./responsive-table.styles.scss";
function ResponsiveTable({ className, columns, mobileColumnKeys, scroll, tableLayout, ...rest }) {
const screens = Grid.useBreakpoint();
const isPhone = !screens.md;
const isCompactViewport = !screens.lg;
const prefersHorizontalScroll = isPhone || isCompactViewport;
const isResponsiveFilteringEnabled = ["1", "true", "yes", "on"].includes(
String(import.meta.env.VITE_APP_ENABLE_RESPONSIVE_TABLE_FILTERING || "")
.trim()
.toLowerCase()
);
const resolvedColumns = useMemo(() => {
if (
!isResponsiveFilteringEnabled ||
!Array.isArray(columns) ||
!isPhone ||
!Array.isArray(mobileColumnKeys) ||
mobileColumnKeys.length === 0
) {
return columns;
}
const visibleColumnKeys = new Set(mobileColumnKeys);
const filteredColumns = columns.filter((column) => {
const key = column?.key ?? column?.dataIndex;
// Keep columns with no stable key to avoid accidental loss.
if (key == null) return true;
if (Array.isArray(key)) {
return key.some((part) => visibleColumnKeys.has(part));
}
return visibleColumnKeys.has(key);
});
return filteredColumns.length > 0 ? filteredColumns : columns;
}, [columns, isPhone, isResponsiveFilteringEnabled, mobileColumnKeys]);
const resolvedScroll = useMemo(() => {
if (prefersHorizontalScroll) {
if (scroll == null) {
return { x: "max-content" };
}
if (typeof scroll !== "object" || Array.isArray(scroll)) {
return scroll;
}
const { x, ...baseScroll } = scroll;
return { ...baseScroll, x: x ?? "max-content" };
}
if (scroll == null) {
// Explicitly override ConfigProvider table.scroll desktop defaults.
return {};
}
if (typeof scroll !== "object" || Array.isArray(scroll)) {
return scroll;
}
const { x, ...desktopScroll } = scroll;
// On desktop we prefer fitting columns with ellipsis over forced horizontal scroll.
if (x == null) {
return desktopScroll;
}
return desktopScroll;
}, [prefersHorizontalScroll, scroll]);
const resolvedTableLayout = tableLayout ?? (prefersHorizontalScroll ? "auto" : "fixed");
const responsiveClassName = prefersHorizontalScroll ? undefined : "responsive-table-fit";
const resolvedClassName = [responsiveClassName, className].filter(Boolean).join(" ");
return (
<Table
className={resolvedClassName}
columns={resolvedColumns}
scroll={resolvedScroll}
tableLayout={resolvedTableLayout}
{...rest}
/>
);
}
ResponsiveTable.Summary = Table.Summary;
ResponsiveTable.Column = Table.Column;
ResponsiveTable.ColumnGroup = Table.ColumnGroup;
ResponsiveTable.SELECTION_COLUMN = Table.SELECTION_COLUMN;
ResponsiveTable.EXPAND_COLUMN = Table.EXPAND_COLUMN;
export default ResponsiveTable;

View File

@@ -1,93 +1,7 @@
import { Grid, Table } from "antd"; import { Table } from "antd";
import { useMemo } from "react";
import "./responsive-table.styles.scss";
function ResponsiveTable({ className, columns, mobileColumnKeys, scroll, tableLayout, ...rest }) { function ResponsiveTable(props) {
const screens = Grid.useBreakpoint(); return <Table {...props} />;
const isPhone = !screens.md;
const isCompactViewport = !screens.lg;
const prefersHorizontalScroll = isPhone || isCompactViewport;
const isResponsiveFilteringEnabled = ["1", "true", "yes", "on"].includes(
String(import.meta.env.VITE_APP_ENABLE_RESPONSIVE_TABLE_FILTERING || "")
.trim()
.toLowerCase()
);
const resolvedColumns = useMemo(() => {
if (
!isResponsiveFilteringEnabled ||
!Array.isArray(columns) ||
!isPhone ||
!Array.isArray(mobileColumnKeys) ||
mobileColumnKeys.length === 0
) {
return columns;
}
const visibleColumnKeys = new Set(mobileColumnKeys);
const filteredColumns = columns.filter((column) => {
const key = column?.key ?? column?.dataIndex;
// Keep columns with no stable key to avoid accidental loss.
if (key == null) return true;
if (Array.isArray(key)) {
return key.some((part) => visibleColumnKeys.has(part));
}
return visibleColumnKeys.has(key);
});
return filteredColumns.length > 0 ? filteredColumns : columns;
}, [columns, isPhone, isResponsiveFilteringEnabled, mobileColumnKeys]);
const resolvedScroll = useMemo(() => {
if (prefersHorizontalScroll) {
if (scroll == null) {
return { x: "max-content" };
}
if (typeof scroll !== "object" || Array.isArray(scroll)) {
return scroll;
}
const { x, ...baseScroll } = scroll;
return { ...baseScroll, x: x ?? "max-content" };
}
if (scroll == null) {
// Explicitly override ConfigProvider table.scroll desktop defaults.
return {};
}
if (typeof scroll !== "object" || Array.isArray(scroll)) {
return scroll;
}
const { x, ...desktopScroll } = scroll;
// On desktop we prefer fitting columns with ellipsis over forced horizontal scroll.
if (x == null) {
return desktopScroll;
}
return desktopScroll;
}, [prefersHorizontalScroll, scroll]);
const resolvedTableLayout = tableLayout ?? (prefersHorizontalScroll ? "auto" : "fixed");
const responsiveClassName = prefersHorizontalScroll ? undefined : "responsive-table-fit";
const resolvedClassName = [responsiveClassName, className].filter(Boolean).join(" ");
return (
<Table
className={resolvedClassName}
columns={resolvedColumns}
scroll={resolvedScroll}
tableLayout={resolvedTableLayout}
{...rest}
/>
);
} }
ResponsiveTable.Summary = Table.Summary; ResponsiveTable.Summary = Table.Summary;

View File

@@ -47,7 +47,7 @@ export function TimeTicketModalComponent({
} = useTreatmentsWithConfig({ } = useTreatmentsWithConfig({
attributes: {}, attributes: {},
names: ["Enhanced_Payroll"], names: ["Enhanced_Payroll"],
splitKey: bodyshop.imexshopid splitKey: bodyshop?.imexshopid
}); });
const [loadLineTicketData, { loading, data: lineTicketData, refetch }] = useLazyQuery(GET_LINE_TICKET_BY_PK, { const [loadLineTicketData, { loading, data: lineTicketData, refetch }] = useLazyQuery(GET_LINE_TICKET_BY_PK, {
@@ -347,7 +347,7 @@ export function LaborAllocationContainer({
} = useTreatmentsWithConfig({ } = useTreatmentsWithConfig({
attributes: {}, attributes: {},
names: ["Enhanced_Payroll"], names: ["Enhanced_Payroll"],
splitKey: bodyshop.imexshopid splitKey: bodyshop?.imexshopid
}); });
if (loading) return <LoadingSkeleton />; if (loading) return <LoadingSkeleton />;

View File

@@ -1,6 +1,7 @@
const { RRClient } = require("./lib/index.cjs"); const { RRClient } = require("./lib/index.cjs");
const { getRRConfigFromBodyshop } = require("./rr-config"); const { getRRConfigFromBodyshop } = require("./rr-config");
const CreateRRLogEvent = require("./rr-logger-event"); const CreateRRLogEvent = require("./rr-logger-event");
const { withRRRequestXml } = require("./rr-log-xml");
const InstanceManager = require("../utils/instanceMgr").default; const InstanceManager = require("../utils/instanceMgr").default;
/** /**
@@ -217,14 +218,24 @@ const createRRCustomer = async ({ bodyshop, job, overrides = {}, socket }) => {
try { try {
response = await client.insertCustomer(safePayload, opts); response = await client.insertCustomer(safePayload, opts);
// Very noisy; only show when log level is cranked to SILLY // Very noisy; only show when log level is cranked to SILLY
CreateRRLogEvent(socket, "SILLY", "{CU} insertCustomer: raw response", { response }); CreateRRLogEvent(
socket,
"SILLY",
"{CU} insertCustomer: raw response",
withRRRequestXml(response, { response })
);
} catch (e) { } catch (e) {
CreateRRLogEvent(socket, "ERROR", "RR insertCustomer transport error", { CreateRRLogEvent(
message: e?.message, socket,
code: e?.code, "ERROR",
status: e?.meta?.status || e?.status, "RR insertCustomer transport error",
payload: safePayload withRRRequestXml(e, {
}); message: e?.message,
code: e?.code,
status: e?.meta?.status || e?.status,
payload: safePayload
})
);
throw e; throw e;
} }
@@ -233,12 +244,17 @@ const createRRCustomer = async ({ bodyshop, job, overrides = {}, socket }) => {
let customerNo = data?.dmsRecKey; let customerNo = data?.dmsRecKey;
if (!customerNo) { if (!customerNo) {
CreateRRLogEvent(socket, "ERROR", "RR insertCustomer returned no dmsRecKey/custNo", { CreateRRLogEvent(
status: trx?.status, socket,
statusCode: trx?.statusCode, "ERROR",
message: trx?.message, "RR insertCustomer returned no dmsRecKey/custNo",
data withRRRequestXml(response, {
}); status: trx?.status,
statusCode: trx?.statusCode,
message: trx?.message,
data
})
);
throw new Error( throw new Error(
`RR insertCustomer returned no dmsRecKey (status=${trx?.status ?? "?"} code=${trx?.statusCode ?? "?"}${ `RR insertCustomer returned no dmsRecKey (status=${trx?.status ?? "?"} code=${trx?.statusCode ?? "?"}${

View File

@@ -1,6 +1,7 @@
const { GraphQLClient } = require("graphql-request"); const { GraphQLClient } = require("graphql-request");
const queries = require("../graphql-client/queries"); const queries = require("../graphql-client/queries");
const CreateRRLogEvent = require("./rr-logger-event"); const CreateRRLogEvent = require("./rr-logger-event");
const { extractRRXmlPair } = require("./rr-log-xml");
/** Get bearer token from the socket (same approach used elsewhere) */ /** Get bearer token from the socket (same approach used elsewhere) */
const getAuthToken = (socket) => const getAuthToken = (socket) =>
@@ -178,11 +179,23 @@ const insertRRFailedExportLog = async ({ socket, jobId, job, bodyshop, error, cl
const client = new GraphQLClient(endpoint, {}); const client = new GraphQLClient(endpoint, {});
client.setHeaders({ Authorization: `Bearer ${token}` }); client.setHeaders({ Authorization: `Bearer ${token}` });
const { requestXml, responseXml } = extractRRXmlPair(error);
const xmlFromError =
requestXml || responseXml
? {
...(requestXml ? { request: requestXml } : {}),
...(responseXml ? { response: responseXml } : {})
}
: undefined;
const meta = buildRRExportMeta({ const meta = buildRRExportMeta({
result, result,
extra: { extra: {
error: error?.message || String(error), error: error?.message || String(error),
classification: classification || undefined classification: classification || undefined,
...(requestXml ? { requestXml } : {}),
...(responseXml ? { responseXml } : {}),
...(xmlFromError && !result?.xml ? { xml: xmlFromError } : {})
} }
}); });

View File

@@ -1,6 +1,7 @@
const { buildRRRepairOrderPayload } = require("./rr-job-helpers"); const { buildRRRepairOrderPayload } = require("./rr-job-helpers");
const { buildClientAndOpts } = require("./rr-lookup"); const { buildClientAndOpts } = require("./rr-lookup");
const CreateRRLogEvent = require("./rr-logger-event"); const CreateRRLogEvent = require("./rr-logger-event");
const { withRRRequestXml } = require("./rr-log-xml");
const { extractRrResponsibilityCenters } = require("./rr-responsibility-centers"); const { extractRrResponsibilityCenters } = require("./rr-responsibility-centers");
const CdkCalculateAllocations = require("./rr-calculate-allocations").default; const CdkCalculateAllocations = require("./rr-calculate-allocations").default;
const { resolveRROpCodeFromBodyshop } = require("./rr-utils"); const { resolveRROpCodeFromBodyshop } = require("./rr-utils");
@@ -147,10 +148,7 @@ const createMinimalRRRepairOrder = async (args) => {
const response = await client.createRepairOrder(payload, finalOpts); const response = await client.createRepairOrder(payload, finalOpts);
CreateRRLogEvent(socket, "INFO", "RR minimal Repair Order created", { CreateRRLogEvent(socket, "INFO", "RR minimal Repair Order created", withRRRequestXml(response, { payload, response }));
payload,
response
});
const data = response?.data || null; const data = response?.data || null;
const statusBlocks = response?.statusBlocks || {}; const statusBlocks = response?.statusBlocks || {};
@@ -327,7 +325,7 @@ const updateRRRepairOrderWithFullData = async (args) => {
// Without this, Reynolds won't recognize the OpCode when we send rogg operations // Without this, Reynolds won't recognize the OpCode when we send rogg operations
// The rolabor section tells Reynolds "these jobs exist" even with minimal data // The rolabor section tells Reynolds "these jobs exist" even with minimal data
CreateRRLogEvent(socket, "INFO", "Sending full data for early RO (using create with roNo)", { CreateRRLogEvent(socket, "INFO", "Preparing full data for early RO (using create with roNo)", {
roNo: String(roNo), roNo: String(roNo),
hasRolabor: !!payload.rolabor, hasRolabor: !!payload.rolabor,
hasRogg: !!payload.rogg, hasRogg: !!payload.rogg,
@@ -338,10 +336,18 @@ const updateRRRepairOrderWithFullData = async (args) => {
// Reynolds will merge this with the existing RO header // Reynolds will merge this with the existing RO header
const response = await client.createRepairOrder(payload, finalOpts); const response = await client.createRepairOrder(payload, finalOpts);
CreateRRLogEvent(socket, "INFO", "RR Repair Order full data sent", { CreateRRLogEvent(
payload, socket,
response "INFO",
}); "Sending full data for early RO (using create with roNo)",
withRRRequestXml(response, {
roNo: String(roNo),
hasRolabor: !!payload.rolabor,
hasRogg: !!payload.rogg,
payload,
response
})
);
const data = response?.data || null; const data = response?.data || null;
const statusBlocks = response?.statusBlocks || {}; const statusBlocks = response?.statusBlocks || {};
@@ -501,10 +507,7 @@ const exportJobToRR = async (args) => {
const response = await client.createRepairOrder(payload, finalOpts); const response = await client.createRepairOrder(payload, finalOpts);
CreateRRLogEvent(socket, "INFO", "RR raw Repair Order created", { CreateRRLogEvent(socket, "INFO", "RR raw Repair Order created", withRRRequestXml(response, { payload, response }));
payload,
response
});
const data = response?.data || null; const data = response?.data || null;
const statusBlocks = response?.statusBlocks || {}; const statusBlocks = response?.statusBlocks || {};
@@ -603,10 +606,15 @@ const finalizeRRRepairOrder = async (args) => {
const rrRes = await client.updateRepairOrder(payload, finalOpts); const rrRes = await client.updateRepairOrder(payload, finalOpts);
CreateRRLogEvent(socket, "SILLY", "RR Repair Order finalized", { CreateRRLogEvent(
payload, socket,
response: rrRes "SILLY",
}); "RR Repair Order finalized",
withRRRequestXml(rrRes, {
payload,
response: rrRes
})
);
const data = rrRes?.data || null; const data = rrRes?.data || null;
const statusBlocks = rrRes?.statusBlocks || {}; const statusBlocks = rrRes?.statusBlocks || {};

63
server/rr/rr-log-xml.js Normal file
View File

@@ -0,0 +1,63 @@
/**
* Extract request/response XML from RR response/result shapes.
* @param rrObj
* @returns {{requestXml: string|null, responseXml: string|null}}
*/
const extractRRXmlPair = (rrObj) => {
const xml = rrObj?.xml ?? rrObj?.meta?.xml;
let requestXml = null;
let responseXml = null;
if (typeof xml === "string") {
requestXml = xml;
} else {
if (typeof xml?.request === "string") requestXml = xml.request;
else if (typeof xml?.req === "string") requestXml = xml.req;
else if (typeof xml?.starXml === "string") requestXml = xml.starXml;
if (typeof xml?.response === "string") responseXml = xml.response;
}
if (!requestXml && typeof rrObj?.requestXml === "string") requestXml = rrObj.requestXml;
if (!requestXml && typeof rrObj?.meta?.requestXml === "string") requestXml = rrObj.meta.requestXml;
if (!requestXml && typeof rrObj?.meta?.reqXml === "string") requestXml = rrObj.meta.reqXml;
if (!requestXml && typeof rrObj?.meta?.request === "string") requestXml = rrObj.meta.request;
if (!responseXml && typeof rrObj?.responseXml === "string") responseXml = rrObj.responseXml;
if (!responseXml && typeof rrObj?.meta?.responseXml === "string") responseXml = rrObj.meta.responseXml;
if (!responseXml && typeof rrObj?.meta?.resXml === "string") responseXml = rrObj.meta.resXml;
if (!responseXml && typeof rrObj?.meta?.response === "string") responseXml = rrObj.meta.response;
// If wrapped HTTP response data contains raw XML, surface it.
if (!responseXml && typeof rrObj?.response?.data === "string") {
const xmlData = rrObj.response.data.trim();
if (xmlData.startsWith("<")) responseXml = xmlData;
}
// Try one level down when errors are wrapped.
if ((!requestXml || !responseXml) && rrObj?.cause && rrObj.cause !== rrObj) {
const nested = extractRRXmlPair(rrObj.cause);
if (!requestXml) requestXml = nested.requestXml;
if (!responseXml) responseXml = nested.responseXml;
}
return { requestXml, responseXml };
};
/**
* Add Reynolds request/response XML to RR log metadata when available.
* @param rrObj
* @param meta
* @returns {*}
*/
const withRRRequestXml = (rrObj, meta = {}) => {
const { requestXml, responseXml } = extractRRXmlPair(rrObj);
const xmlMeta = {};
if (requestXml) xmlMeta.requestXml = requestXml;
if (responseXml) xmlMeta.responseXml = responseXml;
return Object.keys(xmlMeta).length ? { ...meta, ...xmlMeta } : meta;
};
module.exports = {
extractRRXmlPair,
withRRRequestXml
};

View File

@@ -12,6 +12,7 @@ const { createRRCustomer } = require("./rr-customers");
const { ensureRRServiceVehicle } = require("./rr-service-vehicles"); const { ensureRRServiceVehicle } = require("./rr-service-vehicles");
const { classifyRRVendorError } = require("./rr-errors"); const { classifyRRVendorError } = require("./rr-errors");
const { markRRExportSuccess, insertRRFailedExportLog } = require("./rr-export-logs"); const { markRRExportSuccess, insertRRFailedExportLog } = require("./rr-export-logs");
const { withRRRequestXml } = require("./rr-log-xml");
const { const {
makeVehicleSearchPayloadFromJob, makeVehicleSearchPayloadFromJob,
ownersFromVinBlocks, ownersFromVinBlocks,
@@ -48,46 +49,6 @@ const resolveJobId = (explicit, payload, job) => explicit || payload?.jobId || j
*/ */
const resolveVin = ({ tx, job }) => tx?.jobData?.vin || job?.v_vin || null; const resolveVin = ({ tx, job }) => tx?.jobData?.vin || job?.v_vin || null;
/**
* Extract request/response XML from RR response/result shapes.
* @param rrObj
* @returns {{requestXml: string|null, responseXml: string|null}}
*/
const extractRRXmlPair = (rrObj) => {
const xml = rrObj?.xml;
let requestXml = null;
let responseXml = null;
if (typeof xml === "string") {
requestXml = xml;
} else {
if (typeof xml?.request === "string") requestXml = xml.request;
else if (typeof xml?.req === "string") requestXml = xml.req;
else if (typeof xml?.starXml === "string") requestXml = xml.starXml;
if (typeof xml?.response === "string") responseXml = xml.response;
}
if (!requestXml && typeof rrObj?.requestXml === "string") requestXml = rrObj.requestXml;
if (!responseXml && typeof rrObj?.responseXml === "string") responseXml = rrObj.responseXml;
return { requestXml, responseXml };
};
/**
* Add Reynolds request/response XML to RR log metadata when available.
* @param rrObj
* @param meta
* @returns {*}
*/
const withRRRequestXml = (rrObj, meta = {}) => {
const { requestXml, responseXml } = extractRRXmlPair(rrObj);
const xmlMeta = {};
if (requestXml) xmlMeta.requestXml = requestXml;
if (responseXml) xmlMeta.responseXml = responseXml;
return Object.keys(xmlMeta).length ? { ...meta, ...xmlMeta } : meta;
};
/** /**
* Sort vehicle owners first in the list, preserving original order otherwise. * Sort vehicle owners first in the list, preserving original order otherwise.
* @param list * @param list
@@ -279,7 +240,12 @@ const rrMultiCustomerSearch = async ({ bodyshop, job, socket, redisHelpers }) =>
const multiResponse = await rrCombinedSearch(bodyshop, q); const multiResponse = await rrCombinedSearch(bodyshop, q);
CreateRRLogEvent(socket, "SILLY", "Multi Customer Search - raw combined search", { response: multiResponse }); CreateRRLogEvent(
socket,
"SILLY",
"Multi Customer Search - raw combined search",
withRRRequestXml(multiResponse, { response: multiResponse })
);
if (fromVin) { if (fromVin) {
const multiBlocks = Array.isArray(multiResponse?.data) ? multiResponse.data : []; const multiBlocks = Array.isArray(multiResponse?.data) ? multiResponse.data : [];
@@ -300,7 +266,7 @@ const rrMultiCustomerSearch = async ({ bodyshop, job, socket, redisHelpers }) =>
const norm = normalizeCustomerCandidates(multiResponse, { ownersSet }); const norm = normalizeCustomerCandidates(multiResponse, { ownersSet });
merged.push(...norm); merged.push(...norm);
} catch (e) { } catch (e) {
CreateRRLogEvent(socket, "WARN", "Multi-search subquery failed", { kind: q.kind, error: e.message }); CreateRRLogEvent(socket, "WARN", "Multi-search subquery failed", withRRRequestXml(e, { kind: q.kind, error: e.message }));
} }
} }
@@ -348,7 +314,7 @@ const registerRREvents = ({ socket, redisHelpers }) => {
count: decorated.length count: decorated.length
}); });
} catch (e) { } catch (e) {
CreateRRLogEvent(socket, "ERROR", "RR combined lookup error", { error: e.message, jobid }); CreateRRLogEvent(socket, "ERROR", "RR combined lookup error", withRRRequestXml(e, { error: e.message, jobid }));
cb?.({ jobid, error: e.message }); cb?.({ jobid, error: e.message });
} }
}); });
@@ -425,7 +391,7 @@ const registerRREvents = ({ socket, redisHelpers }) => {
fromCache fromCache
}); });
} catch (err) { } catch (err) {
CreateRRLogEvent(socket, "ERROR", "rr-get-advisors: failed", { error: err?.message }); CreateRRLogEvent(socket, "ERROR", "rr-get-advisors: failed", withRRRequestXml(err, { error: err?.message }));
ack?.({ ok: false, error: err?.message || "get advisors failed" }); ack?.({ ok: false, error: err?.message || "get advisors failed" });
} }
}); });
@@ -496,11 +462,16 @@ const registerRREvents = ({ socket, redisHelpers }) => {
anyOwner: decorated.some((c) => c.vinOwner || c.isVehicleOwner) anyOwner: decorated.some((c) => c.vinOwner || c.isVehicleOwner)
}); });
} catch (error) { } catch (error) {
CreateRRLogEvent(socket, "ERROR", `Error during RR early RO creation (prepare)`, { CreateRRLogEvent(
error: error.message, socket,
stack: error.stack, "ERROR",
jobid: rid `Error during RR early RO creation (prepare)`,
}); withRRRequestXml(error, {
error: error.message,
stack: error.stack,
jobid: rid
})
);
try { try {
socket.emit("export-failed", { vendor: "rr", jobId: rid, error: error.message }); socket.emit("export-failed", { vendor: "rr", jobId: rid, error: error.message });
@@ -597,7 +568,12 @@ const registerRREvents = ({ socket, redisHelpers }) => {
if (vehQ && vehQ.kind === "vin" && job?.v_vin) { if (vehQ && vehQ.kind === "vin" && job?.v_vin) {
const vinResponse = await rrCombinedSearch(bodyshop, vehQ); const vinResponse = await rrCombinedSearch(bodyshop, vehQ);
CreateRRLogEvent(socket, "SILLY", `VIN owner pre-check response (early RO)`, { response: vinResponse }); CreateRRLogEvent(
socket,
"SILLY",
`VIN owner pre-check response (early RO)`,
withRRRequestXml(vinResponse, { response: vinResponse })
);
const vinBlocks = Array.isArray(vinResponse?.data) ? vinResponse.data : []; const vinBlocks = Array.isArray(vinResponse?.data) ? vinResponse.data : [];
@@ -630,9 +606,14 @@ const registerRREvents = ({ socket, redisHelpers }) => {
} }
} }
} catch (e) { } catch (e) {
CreateRRLogEvent(socket, "WARN", `VIN owner pre-check failed; continuing with selected customer (early RO)`, { CreateRRLogEvent(
error: e?.message socket,
}); "WARN",
`VIN owner pre-check failed; continuing with selected customer (early RO)`,
withRRRequestXml(e, {
error: e?.message
})
);
} }
// Cache final/effective customer selection // Cache final/effective customer selection
@@ -905,14 +886,19 @@ const registerRREvents = ({ socket, redisHelpers }) => {
} catch (error) { } catch (error) {
const cls = classifyRRVendorError(error); const cls = classifyRRVendorError(error);
CreateRRLogEvent(socket, "ERROR", `Error during RR early RO creation (customer-selected)`, { CreateRRLogEvent(
error: error.message, socket,
vendorStatusCode: cls.vendorStatusCode, "ERROR",
code: cls.errorCode, `Error during RR early RO creation (customer-selected)`,
friendly: cls.friendlyMessage, withRRRequestXml(error, {
stack: error.stack, error: error.message,
jobid: rid vendorStatusCode: cls.vendorStatusCode,
}); code: cls.errorCode,
friendly: cls.friendlyMessage,
stack: error.stack,
jobid: rid
})
);
try { try {
if (!bodyshop || !job) { if (!bodyshop || !job) {
@@ -1097,7 +1083,28 @@ const registerRREvents = ({ socket, redisHelpers }) => {
roNo: job.dms_id roNo: job.dms_id
}); });
CreateRRLogEvent(
socket,
"SILLY",
"{4.1} RR RO update response received",
withRRRequestXml(result, {
dmsRoNo: job.dms_id,
success: !!result?.success
})
);
if (!result?.success) { if (!result?.success) {
CreateRRLogEvent(
socket,
"ERROR",
"RR Repair Order update failed",
withRRRequestXml(result, {
jobId: rid,
dmsRoNo: job.dms_id,
roStatus: result?.roStatus,
statusBlocks: result?.statusBlocks
})
);
throw new Error(result?.roStatus?.message || "Failed to update RR Repair Order"); throw new Error(result?.roStatus?.message || "Failed to update RR Repair Order");
} }
@@ -1154,11 +1161,16 @@ const registerRREvents = ({ socket, redisHelpers }) => {
anyOwner: decorated.some((c) => c.vinOwner || c.isVehicleOwner) anyOwner: decorated.some((c) => c.vinOwner || c.isVehicleOwner)
}); });
} catch (error) { } catch (error) {
CreateRRLogEvent(socket, "ERROR", `Error during RR export (prepare)`, { CreateRRLogEvent(
error: error.message, socket,
stack: error.stack, "ERROR",
jobid: rid `Error during RR export (prepare)`,
}); withRRRequestXml(error, {
error: error.message,
stack: error.stack,
jobid: rid
})
);
try { try {
socket.emit("export-failed", { vendor: "rr", jobId: rid, error: error.message }); socket.emit("export-failed", { vendor: "rr", jobId: rid, error: error.message });
@@ -1220,7 +1232,12 @@ const registerRREvents = ({ socket, redisHelpers }) => {
if (vehQ && vehQ.kind === "vin" && job?.v_vin) { if (vehQ && vehQ.kind === "vin" && job?.v_vin) {
const vinResponse = await rrCombinedSearch(bodyshop, vehQ); const vinResponse = await rrCombinedSearch(bodyshop, vehQ);
CreateRRLogEvent(socket, "SILLY", `VIN owner pre-check response`, { response: vinResponse }); CreateRRLogEvent(
socket,
"SILLY",
`VIN owner pre-check response`,
withRRRequestXml(vinResponse, { response: vinResponse })
);
const vinBlocks = Array.isArray(vinResponse?.data) ? vinResponse.data : []; const vinBlocks = Array.isArray(vinResponse?.data) ? vinResponse.data : [];
@@ -1253,9 +1270,14 @@ const registerRREvents = ({ socket, redisHelpers }) => {
} }
} }
} catch (e) { } catch (e) {
CreateRRLogEvent(socket, "WARN", `VIN owner pre-check failed; continuing with selected customer`, { CreateRRLogEvent(
error: e?.message socket,
}); "WARN",
`VIN owner pre-check failed; continuing with selected customer`,
withRRRequestXml(e, {
error: e?.message
})
);
} }
// Cache final/effective customer selection // Cache final/effective customer selection
@@ -1532,14 +1554,19 @@ const registerRREvents = ({ socket, redisHelpers }) => {
} catch (error) { } catch (error) {
const cls = classifyRRVendorError(error); const cls = classifyRRVendorError(error);
CreateRRLogEvent(socket, "ERROR", `Error during RR export (selected-customer)`, { CreateRRLogEvent(
error: error.message, socket,
vendorStatusCode: cls.vendorStatusCode, "ERROR",
code: cls.errorCode, `Error during RR export (selected-customer)`,
friendly: cls.friendlyMessage, withRRRequestXml(error, {
stack: error.stack, error: error.message,
jobid: rid vendorStatusCode: cls.vendorStatusCode,
}); code: cls.errorCode,
friendly: cls.friendlyMessage,
stack: error.stack,
jobid: rid
})
);
try { try {
if (!bodyshop || !job) { if (!bodyshop || !job) {
@@ -1707,14 +1734,19 @@ const registerRREvents = ({ socket, redisHelpers }) => {
} }
} catch (error) { } catch (error) {
const cls = classifyRRVendorError(error); const cls = classifyRRVendorError(error);
CreateRRLogEvent(socket, "ERROR", `Error during RR finalize`, { CreateRRLogEvent(
error: error.message, socket,
vendorStatusCode: cls.vendorStatusCode, "ERROR",
code: cls.errorCode, `Error during RR finalize`,
friendly: cls.friendlyMessage, withRRRequestXml(error, {
stack: error.stack, error: error.message,
jobid: rid vendorStatusCode: cls.vendorStatusCode,
}); code: cls.errorCode,
friendly: cls.friendlyMessage,
stack: error.stack,
jobid: rid
})
);
try { try {
if (!bodyshop || !job) { if (!bodyshop || !job) {

View File

@@ -1,5 +1,6 @@
const { buildClientAndOpts, rrCombinedSearch } = require("./rr-lookup"); const { buildClientAndOpts, rrCombinedSearch } = require("./rr-lookup");
const CreateRRLogEvent = require("./rr-logger-event"); const CreateRRLogEvent = require("./rr-logger-event");
const { withRRRequestXml } = require("./rr-log-xml");
/** /**
* Pick and normalize VIN from inputs * Pick and normalize VIN from inputs
* @param vin * @param vin
@@ -168,9 +169,12 @@ const ensureRRServiceVehicle = async (args = {}) => {
if (bodyshop) { if (bodyshop) {
const combinedSearchResponse = await rrCombinedSearch(bodyshop, { kind: "vin", vin: vinStr, maxResults: 50 }); const combinedSearchResponse = await rrCombinedSearch(bodyshop, { kind: "vin", vin: vinStr, maxResults: 50 });
CreateRRLogEvent(socket, "silly", "{SV} Preflight combined search by VIN: raw response", { CreateRRLogEvent(
response: combinedSearchResponse socket,
}); "silly",
"{SV} Preflight combined search by VIN: raw response",
withRRRequestXml(combinedSearchResponse, { response: combinedSearchResponse })
);
owners = ownersFromCombined(combinedSearchResponse, vinStr); owners = ownersFromCombined(combinedSearchResponse, vinStr);
} }
@@ -194,10 +198,15 @@ const ensureRRServiceVehicle = async (args = {}) => {
} }
} catch (e) { } catch (e) {
// Preflight shouldn't be fatal; log and continue to insert (idempotency will still be handled) // Preflight shouldn't be fatal; log and continue to insert (idempotency will still be handled)
CreateRRLogEvent(socket, "warn", "{SV} VIN preflight lookup failed; continuing to insert", { CreateRRLogEvent(
vin: vinStr, socket,
error: e?.message "warn",
}); "{SV} VIN preflight lookup failed; continuing to insert",
withRRRequestXml(e, {
vin: vinStr,
error: e?.message
})
);
} }
// Vendor says: MODEL DESCRIPTION HAS MAXIMUM LENGTH OF 20 // Vendor says: MODEL DESCRIPTION HAS MAXIMUM LENGTH OF 20
@@ -271,7 +280,7 @@ const ensureRRServiceVehicle = async (args = {}) => {
try { try {
const res = await client.insertServiceVehicle(insertPayload, insertOpts); const res = await client.insertServiceVehicle(insertPayload, insertOpts);
CreateRRLogEvent(socket, "silly", "{SV} insertServiceVehicle: raw response", { res }); CreateRRLogEvent(socket, "silly", "{SV} insertServiceVehicle: raw response", withRRRequestXml(res, { res }));
const data = res?.data ?? {}; const data = res?.data ?? {};
const svId = data?.dmsRecKey || data?.svId || undefined; const svId = data?.dmsRecKey || data?.svId || undefined;
@@ -309,11 +318,16 @@ const ensureRRServiceVehicle = async (args = {}) => {
}; };
} }
CreateRRLogEvent(socket, "error", "{SV} insertServiceVehicle: failure", { CreateRRLogEvent(
message: e?.message, socket,
code: e?.code, "error",
status: e?.meta?.status || e?.status "{SV} insertServiceVehicle: failure",
}); withRRRequestXml(e, {
message: e?.message,
code: e?.code,
status: e?.meta?.status || e?.status
})
);
throw e; throw e;
} }