From d7294ebba65e78519a1497d0042c3b422cad79ec Mon Sep 17 00:00:00 2001 From: Dave Date: Tue, 21 Apr 2026 10:51:13 -0400 Subject: [PATCH] feature/IO-3647-Reynolds-Integration-Phase-2-Optional - Add option to make 'Enhanced Early ROS' optional --- .../shop-info/shop-info.general.component.jsx | 46 +++++++- client/src/translations/en_us/common.json | 1 + client/src/translations/es/common.json | 1 + client/src/translations/fr/common.json | 1 + server/rr/rr-job-export.js | 23 ++-- server/rr/rr-job-export.test.js | 105 ++++++++++++++++++ server/rr/rr-utils.js | 9 ++ server/rr/rr-utils.test.js | 18 +++ 8 files changed, 191 insertions(+), 13 deletions(-) create mode 100644 server/rr/rr-job-export.test.js create mode 100644 server/rr/rr-utils.test.js diff --git a/client/src/components/shop-info/shop-info.general.component.jsx b/client/src/components/shop-info/shop-info.general.component.jsx index fa3a2c3af..cf97cd9ec 100644 --- a/client/src/components/shop-info/shop-info.general.component.jsx +++ b/client/src/components/shop-info/shop-info.general.component.jsx @@ -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 (
@@ -174,7 +181,9 @@ export function ShopInfoGeneral({ form }) { >
-
{t("bodyshop.fields.scoreboard_setup.ignore_blocked_days")}
+
+ {t("bodyshop.fields.scoreboard_setup.ignore_blocked_days")} +
@@ -311,7 +320,12 @@ export function ShopInfoGeneral({ form }) { - + @@ -478,7 +492,12 @@ export function ShopInfoGeneral({ form }) {
{t("bodyshop.fields.system_settings.job_costing.use_paint_scale_data")}
- +
@@ -558,7 +577,12 @@ export function ShopInfoGeneral({ form }) { - + + {isReynoldsMode && ( + + + + )}
diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 33f89f2fd..304423d4e 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -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": { diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 58c7a74b5..9fdfa21b6 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -457,6 +457,7 @@ "md_email_cc": "", "md_from_emails": "", "md_functionality_toggles": { + "enhanced_early_ros": "", "parts_queue_toggle": "" }, "md_hour_split": { diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index e5cccbe55..2f07205c9 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -457,6 +457,7 @@ "md_email_cc": "", "md_from_emails": "", "md_functionality_toggles": { + "enhanced_early_ros": "", "parts_queue_toggle": "" }, "md_hour_split": { diff --git a/server/rr/rr-job-export.js b/server/rr/rr-job-export.js index 15e2f2a34..c4d4d8328 100644 --- a/server/rr/rr-job-export.js +++ b/server/rr/rr-job-export.js @@ -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 || {}; diff --git a/server/rr/rr-job-export.test.js b/server/rr/rr-job-export.test.js new file mode 100644 index 000000000..df8be1ff5 --- /dev/null +++ b/server/rr/rr-job-export.test.js @@ -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(); + }); +}); diff --git a/server/rr/rr-utils.js b/server/rr/rr-utils.js index 0a8cb49ab..5629e7fda 100644 --- a/server/rr/rr-utils.js +++ b/server/rr/rr-utils.js @@ -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, diff --git a/server/rr/rr-utils.test.js b/server/rr/rr-utils.test.js new file mode 100644 index 000000000..3f6810fe7 --- /dev/null +++ b/server/rr/rr-utils.test.js @@ -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); + }); +});