Merged in release/2026-05-08 (pull request #3200)

feature/IO-3647-Reynolds-Integration-Phase-2-Optional - Add option to make 'Enhanced Early ROS' optional
This commit is contained in:
Dave Richer
2026-04-21 14:52:31 +00:00
8 changed files with 191 additions and 13 deletions

View File

@@ -12,6 +12,8 @@ import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.c
import { buildSectionActionButton, renderListOrEmpty } from "../layout-form-row/config-list-actions.utils.jsx";
import InlineValidatedFormRow from "../layout-form-row/inline-validated-form-row.component.jsx";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { bodyshopHasDmsKey, DMS_MAP, getDmsMode } from "../../utils/dmsUtils.js";
import {
INLINE_TITLE_GROUP_STYLE,
INLINE_TITLE_HANDLE_STYLE,
@@ -25,16 +27,21 @@ import {
const timeZonesList = Intl.supportedValuesOf("timeZone");
const mapStateToProps = createStructuredSelector({});
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoGeneral);
export function ShopInfoGeneral({ form }) {
export function ShopInfoGeneral({ form, bodyshop }) {
const { t } = useTranslation();
const insuranceCompanies = Form.useWatch(["md_ins_cos"], form) || [];
const duplicateInsuranceCompanyIndexes = getDuplicateIndexSetByNormalizedName(insuranceCompanies, "name");
const hasDMSKey = bodyshop ? bodyshopHasDmsKey(bodyshop) : false;
const dmsMode = bodyshop ? getDmsMode(bodyshop, "off") : "none";
const isReynoldsMode = hasDMSKey && dmsMode === DMS_MAP.reynolds;
return (
<div>
@@ -174,7 +181,9 @@ export function ShopInfoGeneral({ form }) {
>
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
<div style={INLINE_TITLE_SWITCH_GROUP_STYLE}>
<div style={INLINE_TITLE_LABEL_STYLE}>{t("bodyshop.fields.scoreboard_setup.ignore_blocked_days")}</div>
<div style={INLINE_TITLE_LABEL_STYLE}>
{t("bodyshop.fields.scoreboard_setup.ignore_blocked_days")}
</div>
<Form.Item noStyle name={["scoreboard_target", "ignoreblockeddays"]} valuePropName="checked">
<Switch />
</Form.Item>
@@ -311,7 +320,12 @@ export function ShopInfoGeneral({ form }) {
</Form.Item>
</Col>
<Col xs={24} sm={12} xl={8}>
<Form.Item key="use_fippa" label={t("bodyshop.fields.use_fippa")} name={["use_fippa"]} valuePropName="checked">
<Form.Item
key="use_fippa"
label={t("bodyshop.fields.use_fippa")}
name={["use_fippa"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
</Col>
@@ -478,7 +492,12 @@ export function ShopInfoGeneral({ form }) {
<div style={INLINE_TITLE_LABEL_STYLE}>
{t("bodyshop.fields.system_settings.job_costing.use_paint_scale_data")}
</div>
<Form.Item noStyle key="use_paint_scale_data" name={["use_paint_scale_data"]} valuePropName="checked">
<Form.Item
noStyle
key="use_paint_scale_data"
name={["use_paint_scale_data"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
</div>
@@ -558,7 +577,12 @@ export function ShopInfoGeneral({ form }) {
</Form.Item>
</LayoutFormRow>
<LayoutFormRow header={t("bodyshop.labels.shop_enabled_features")} id="sharing" grow style={{ marginBottom: 0 }}>
<LayoutFormRow
header={t("bodyshop.labels.shop_enabled_features")}
id="sharing"
grow
style={{ marginBottom: 0 }}
>
<Form.Item
label={t("general.actions.sharetoteams")}
valuePropName="checked"
@@ -566,6 +590,16 @@ export function ShopInfoGeneral({ form }) {
>
<Switch />
</Form.Item>
{isReynoldsMode && (
<Form.Item
initialValue
label={t("bodyshop.fields.md_functionality_toggles.enhanced_early_ros")}
name={["md_functionality_toggles", "enhanced_early_ros"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
)}
</LayoutFormRow>
</div>
</>

View File

@@ -463,6 +463,7 @@
"md_email_cc": "Auto Email CC: $t(parts_orders.labels.{{template}})",
"md_from_emails": "Additional From Emails",
"md_functionality_toggles": {
"enhanced_early_ros": "Enable Enhance Early ROs",
"parts_queue_toggle": "Auto Add Imported/Supplemented Jobs to Parts Queue"
},
"md_hour_split": {

View File

@@ -457,6 +457,7 @@
"md_email_cc": "",
"md_from_emails": "",
"md_functionality_toggles": {
"enhanced_early_ros": "",
"parts_queue_toggle": ""
},
"md_hour_split": {

View File

@@ -457,6 +457,7 @@
"md_email_cc": "",
"md_from_emails": "",
"md_functionality_toggles": {
"enhanced_early_ros": "",
"parts_queue_toggle": ""
},
"md_hour_split": {

View File

@@ -4,7 +4,7 @@ const CreateRRLogEvent = require("./rr-logger-event");
const { withRRRequestXml } = require("./rr-log-xml");
const { extractRrResponsibilityCenters } = require("./rr-responsibility-centers");
const CdkCalculateAllocations = require("./rr-calculate-allocations").default;
const { resolveRROpCodeFromBodyshop } = require("./rr-utils");
const { isEnhancedEarlyROEnabled, resolveRROpCodeFromBodyshop } = require("./rr-utils");
/**
* Derive RR status information from response object.
@@ -139,11 +139,14 @@ const createMinimalRRRepairOrder = async (args) => {
resolvedMileageIn: mileageIn
});
const earlyRoOpCode = resolveRROpCode(bodyshop, txEnvelope);
const earlyRoLabor = buildMinimalRolaborFromJob(job, {
opCode: earlyRoOpCode,
payType: "Cust"
});
const enhancedEarlyROEnabled = isEnhancedEarlyROEnabled(bodyshop);
const earlyRoOpCode = enhancedEarlyROEnabled ? resolveRROpCode(bodyshop, txEnvelope) : null;
const earlyRoLabor = enhancedEarlyROEnabled
? buildMinimalRolaborFromJob(job, {
opCode: earlyRoOpCode,
payType: "Cust"
})
: null;
const payload = {
customerNo: String(selected),
@@ -176,13 +179,19 @@ const createMinimalRRRepairOrder = async (args) => {
CreateRRLogEvent(socket, "INFO", "Creating minimal RR Repair Order (early creation)", {
payload,
enhancedEarlyROEnabled,
earlyRoOpCode,
hasRolabor: !!earlyRoLabor
});
const response = await client.createRepairOrder(payload, finalOpts);
CreateRRLogEvent(socket, "INFO", "RR minimal Repair Order created", withRRRequestXml(response, { payload, response }));
CreateRRLogEvent(
socket,
"INFO",
"RR minimal Repair Order created",
withRRRequestXml(response, { payload, response })
);
const data = response?.data || null;
const statusBlocks = response?.statusBlocks || {};

View File

@@ -0,0 +1,105 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const mock = require("mock-require");
const helpersModuleId = require.resolve("./rr-job-helpers");
const lookupModuleId = require.resolve("./rr-lookup");
const loggerEventModuleId = require.resolve("./rr-logger-event");
const logXmlModuleId = require.resolve("./rr-log-xml");
const responsibilityCentersModuleId = require.resolve("./rr-responsibility-centers");
const allocationsModuleId = require.resolve("./rr-calculate-allocations");
const jobExportModuleId = require.resolve("./rr-job-export");
const makeBodyshop = (mdFunctionalityToggles) => ({
rr_configuration: {
defaults: {
prefix: "51",
base: "DOZ",
suffix: ""
}
},
...(mdFunctionalityToggles ? { md_functionality_toggles: mdFunctionalityToggles } : {})
});
const makeJob = () => ({
id: "job-1",
ro_number: "RO-123",
v_vin: "1HGBH41JXMN109186",
joblines: [{ mod_lbr_ty: "LAB", mod_lb_hrs: 2, lbr_amt: 200 }]
});
const loadJobExport = ({
buildMinimalRolaborFromJob = vi.fn(() => ({ ops: [{ opCode: "51DOZ" }] })),
createRepairOrder = vi.fn(async () => ({ success: true, data: { dmsRoNo: "12345" } }))
} = {}) => {
mock.stopAll();
mock(helpersModuleId, {
buildRRRepairOrderPayload: vi.fn(),
buildMinimalRolaborFromJob
});
mock(lookupModuleId, {
buildClientAndOpts: () => ({
client: { createRepairOrder },
opts: { envelope: { sender: {} } }
})
});
mock(loggerEventModuleId, vi.fn());
mock(logXmlModuleId, {
withRRRequestXml: (response, payload) => payload
});
mock(responsibilityCentersModuleId, {
extractRrResponsibilityCenters: vi.fn(() => [])
});
mock(allocationsModuleId, {
default: vi.fn()
});
delete require.cache[jobExportModuleId];
return {
...require(jobExportModuleId),
buildMinimalRolaborFromJob,
createRepairOrder
};
};
afterEach(() => {
mock.stopAll();
delete require.cache[jobExportModuleId];
});
describe("server/rr/rr-job-export", () => {
it("sends early RO labor totals by default", async () => {
const { createMinimalRRRepairOrder, createRepairOrder, buildMinimalRolaborFromJob } = loadJobExport();
await createMinimalRRRepairOrder({
bodyshop: makeBodyshop(),
job: makeJob(),
advisorNo: "70754",
selectedCustomer: { custNo: "1134485" },
txEnvelope: {}
});
expect(buildMinimalRolaborFromJob).toHaveBeenCalledWith(makeJob(), {
opCode: "51DOZ",
payType: "Cust"
});
expect(createRepairOrder.mock.calls[0][0].rolabor).toEqual({ ops: [{ opCode: "51DOZ" }] });
});
it("omits early RO labor totals when the shop opts out", async () => {
const { createMinimalRRRepairOrder, createRepairOrder, buildMinimalRolaborFromJob } = loadJobExport();
await createMinimalRRRepairOrder({
bodyshop: makeBodyshop({ enhanced_early_ros: false }),
job: makeJob(),
advisorNo: "70754",
selectedCustomer: { custNo: "1134485" },
txEnvelope: {}
});
expect(buildMinimalRolaborFromJob).not.toHaveBeenCalled();
expect(createRepairOrder.mock.calls[0][0].rolabor).toBeUndefined();
});
});

View File

@@ -205,10 +205,19 @@ const resolveRROpCodeFromBodyshop = (bodyshop) => {
return `${prefix}${base}${suffix}`;
};
/**
* Enhanced Early RO labor totals are enabled by default for backwards compatibility.
* Shops can explicitly set md_functionality_toggles.enhanced_early_ros to false to opt out.
* @param bodyshop
* @returns {boolean}
*/
const isEnhancedEarlyROEnabled = (bodyshop) => bodyshop?.md_functionality_toggles?.enhanced_early_ros !== false;
module.exports = {
RRCacheEnums,
defaultRRTTL,
getTransactionType,
isEnhancedEarlyROEnabled,
ownersFromVinBlocks,
makeVehicleSearchPayloadFromJob,
normalizeCustomerCandidates,

View File

@@ -0,0 +1,18 @@
import { describe, expect, it } from "vitest";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const { isEnhancedEarlyROEnabled } = require("./rr-utils");
describe("server/rr/rr-utils", () => {
it("keeps enhanced early ROs enabled when the shop setting is missing", () => {
expect(isEnhancedEarlyROEnabled()).toBe(true);
expect(isEnhancedEarlyROEnabled({})).toBe(true);
expect(isEnhancedEarlyROEnabled({ md_functionality_toggles: {} })).toBe(true);
});
it("only disables enhanced early ROs when the shop explicitly opts out", () => {
expect(isEnhancedEarlyROEnabled({ md_functionality_toggles: { enhanced_early_ros: false } })).toBe(false);
expect(isEnhancedEarlyROEnabled({ md_functionality_toggles: { enhanced_early_ros: true } })).toBe(true);
});
});