diff --git a/client/src/components/dms-customer-selector/dms-customer-selector.component.jsx b/client/src/components/dms-customer-selector/dms-customer-selector.component.jsx index 650349d39..205ceb414 100644 --- a/client/src/components/dms-customer-selector/dms-customer-selector.component.jsx +++ b/client/src/components/dms-customer-selector/dms-customer-selector.component.jsx @@ -37,8 +37,8 @@ export function DmsCustomerSelector(props) { const rrProps = { rrOpenRoLimit: rrOptions.openRoLimit, onRrOpenRoFinished: rrOptions.onOpenRoFinished, - rrCashierPending: rrOptions.cashierPending, - onRrCashierFinished: rrOptions.onCashierFinished + rrValidationPending: rrOptions.validationPending, + onValidationFinished: rrOptions.onValidationFinished }; return ; } diff --git a/client/src/components/dms-customer-selector/rr-customer-selector.jsx b/client/src/components/dms-customer-selector/rr-customer-selector.jsx index 9a1991b61..e8d830815 100644 --- a/client/src/components/dms-customer-selector/rr-customer-selector.jsx +++ b/client/src/components/dms-customer-selector/rr-customer-selector.jsx @@ -49,8 +49,8 @@ export default function RRCustomerSelector({ socket, rrOpenRoLimit = false, onRrOpenRoFinished, - rrCashierPending = false, - onRrCashierFinished + rrValidationPending = false, + onValidationFinished }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); @@ -58,10 +58,10 @@ export default function RRCustomerSelector({ const [selectedCustomer, setSelectedCustomer] = useState(null); const [refreshing, setRefreshing] = useState(false); - // Show dialog automatically when cashiering is pending + // Show dialog automatically when validation is pending useEffect(() => { - if (rrCashierPending) setOpen(true); - }, [rrCashierPending]); + if (rrValidationPending) setOpen(true); + }, [rrValidationPending]); // Listen for RR customer selection list useEffect(() => { @@ -196,22 +196,21 @@ export default function RRCustomerSelector({ /> )} - {/* Cashiering step banner */} - {rrCashierPending && ( + {/* Validation step banner */} + {rrValidationPending && (
- We created the Repair Order in Reynolds. Please complete the cashiering/closeout steps in - Reynolds. When done, click Finished/Close to finalize and mark this export as - complete. + We created the Repair Order. Please validate the totals and taxes in the DMS system. When done, + click Finished to finalize and mark this export as complete.
- diff --git a/client/src/pages/dms/dms.container.jsx b/client/src/pages/dms/dms.container.jsx index 9650d912b..e4acf5862 100644 --- a/client/src/pages/dms/dms.container.jsx +++ b/client/src/pages/dms/dms.container.jsx @@ -43,7 +43,7 @@ const DMS_SOCKET_EVENTS = { [DMS_MAP.reynolds]: { log: "rr-log-event", partialResult: "rr-export-job:result", - cashierNeeded: "rr-cashiering-required", + validationNeeded: "rr-validation-required", exportSuccess: "export-success", exportFailed: "export-failed" }, @@ -104,7 +104,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse const [rrOpenRoLimit, setRrOpenRoLimit] = useState(false); const clearRrOpenRoLimit = () => setRrOpenRoLimit(false); - const [rrCashierPending, setRrCashierPending] = useState(false); + const [rrValidationPending, setrrValidationPending] = useState(false); const { loading, error, data } = useQuery(QUERY_JOB_EXPORT_DMS, { variables: { id: jobId }, @@ -259,8 +259,8 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse const jobIdResolved = payload?.jobId ?? payload; notification.success({ message: t("jobs.successes.exported") }); - // Clear RR cashier flag if any - setRrCashierPending(false); + // Clear RR Validation flag if any + setrrValidationPending(false); insertAuditTrail({ jobid: jobIdResolved, @@ -276,40 +276,41 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse // RR-only extras const onPartialResult = () => { - setRrCashierPending(true); + setrrValidationPending(true); setLogs((prev) => [ ...prev, { timestamp: new Date(), level: "INFO", message: - "Repair Order created in Reynolds. Complete cashiering in Reynolds, then click Finished/Close to finalize." + "Repair Order created in Reynolds. Complete validation in Reynolds, then click Finished/Close to finalize." } ]); notification.info({ message: "Reynolds RO created", description: - "Complete cashiering in Reynolds, then click Finished/Close to finalize and mark this export complete.", + "Complete validation in Reynolds, then click Finished/Close to finalize and mark this export complete.", duration: 8 }); }; - const onCashierRequired = (payload) => { - setRrCashierPending(true); + const onValidationRequired = (payload) => { + setrrValidationPending(true); setLogs((prev) => [ ...prev, { timestamp: new Date(), level: "INFO", message: - "Repair Order created in Reynolds. Complete cashiering in Reynolds, then click Finished/Close to finalize.", + "Repair Order created in Reynolds. Complete validation in Reynolds, then click Finished/Close to finalize.", meta: { payload } } ]); }; if (mode === DMS_MAP.reynolds && channels.partialResult) activeSocket.on(channels.partialResult, onPartialResult); - if (mode === DMS_MAP.reynolds && channels.cashierNeeded) activeSocket.on(channels.cashierNeeded, onCashierRequired); + if (mode === DMS_MAP.reynolds && channels.validationNeeded) + activeSocket.on(channels.validationrNeeded, onValidationRequired); return () => { activeSocket.off("connect", onConnect); @@ -323,8 +324,8 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse if (mode === DMS_MAP.reynolds && channels.partialResult) activeSocket.off(channels.partialResult, onPartialResult); - if (mode === DMS_MAP.reynolds && channels.cashierNeeded) - activeSocket.off(channels.cashierNeeded, onCashierRequired); + if (mode === DMS_MAP.reynolds && channels.validationNeeded) + activeSocket.off(channels.validationNeeded, onValidationRequired); // Only tear down legacy socket listeners; don't disconnect WSS from here if (!isWssMode(mode)) { @@ -335,7 +336,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse }, [mode, activeSocket, channels, logLevel, notification, t, insertAuditTrail, history]); // RR finalize callback (unchanged public behavior) - const handleRrCashierFinished = () => { + const handleRrValidationFinished = () => { if (!jobId) return; if (!isWssMode(mode)) return; // RR is WSS-only activeSocket.emit("rr-finalize-repair-order", { jobId }, (ack) => { @@ -385,8 +386,8 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse rrOptions={{ openRoLimit: rrOpenRoLimit, onOpenRoFinished: clearRrOpenRoLimit, - cashierPending: rrCashierPending, - onCashierFinished: handleRrCashierFinished + validationPending: rrValidationPending, + onValidationFinished: handleRrValidationFinished }} /> diff --git a/server/rr/rr-customers.js b/server/rr/rr-customers.js index 628d6c962..f8eec1c54 100644 --- a/server/rr/rr-customers.js +++ b/server/rr/rr-customers.js @@ -1,6 +1,7 @@ const { RRClient } = require("./lib/index.cjs"); const { getRRConfigFromBodyshop } = require("./rr-config"); const RRLogger = require("./rr-logger"); +const InstanceManager = require("../utils/instanceMgr").default; /** * Country code map for normalization @@ -189,7 +190,7 @@ const buildCustomerPayloadFromJob = (job, overrides = {}) => { firstName: firstName || undefined, lastName: lastName || undefined, customerName: companyName || undefined, - createdBy: overrides.createdBy || "ImEX Online", + createdBy: overrides.createdBy || InstanceManager({ imex: "ImEX Online", rome: "Rome Online" }), customerType: overrides.customerType || "R", // Retail default addresses, phones, diff --git a/server/rr/rr-export-logs.js b/server/rr/rr-export-logs.js index 3e451b516..269b7d43b 100644 --- a/server/rr/rr-export-logs.js +++ b/server/rr/rr-export-logs.js @@ -122,6 +122,7 @@ const markRRExportSuccess = async ({ socket, jobId, job, bodyshop, result, metaE const insertRRFailedExportLog = async ({ socket, jobId, job, bodyshop, error, classification, result }) => { const endpoint = process.env.GRAPHQL_ENDPOINT; if (!endpoint) throw new Error("GRAPHQL_ENDPOINT not configured"); + const token = getAuthToken(socket); if (!token) throw new Error("Auth token missing on socket"); diff --git a/server/rr/rr-job-export.js b/server/rr/rr-job-export.js index 835446ef6..e34ddbf86 100644 --- a/server/rr/rr-job-export.js +++ b/server/rr/rr-job-export.js @@ -72,14 +72,7 @@ const exportJobToRR = async (args) => { const roStatus = data?.roStatus || null; // Extract canonical roNo you'll need for finalize step - const roNo = - data?.dmsRoNo ?? - data?.outsdRoNo ?? - roStatus?.dmsRoNo ?? - roStatus?.DMSRoNo ?? - roStatus?.outsdRoNo ?? - roStatus?.OutsdRoNo ?? - null; + const roNo = data?.dmsRoNo ?? data?.outsdRoNo ?? roStatus?.dmsRoNo ?? null; return { success: rrRes?.success === true || roStatus?.status === "Success", diff --git a/server/rr/rr-job-helpers.js b/server/rr/rr-job-helpers.js index 606eaac03..6c89a5e25 100644 --- a/server/rr/rr-job-helpers.js +++ b/server/rr/rr-job-helpers.js @@ -194,10 +194,13 @@ const normalizeVehicleCandidates = (res) => { for (const sv of serv) { const v = sv?.Vehicle || {}; const vin = v?.Vin || v?.VIN || v?.vin; + if (!vin) continue; + const year = v?.VehicleYr || v?.ModelYear || v?.Year; const make = v?.VehicleMake || v?.MakeName || v?.Make; const model = v?.MdlNo || v?.ModelDesc || v?.Model; + const label = [year, make, model, vin].filter(Boolean).join(" "); out.push({ vin, year, make, model, label, _blk: blk }); } diff --git a/server/rr/rr-register-socket-events.js b/server/rr/rr-register-socket-events.js index a981b16b6..5d799a0dc 100644 --- a/server/rr/rr-register-socket-events.js +++ b/server/rr/rr-register-socket-events.js @@ -32,8 +32,7 @@ const ADVISORS_CACHE_TTL = 7 * 24 * 60 * 60; // seconds * @param job * @returns {*|null} */ -const resolveJobId = (explicit, payload, job) => - explicit || payload?.jobId || payload?.jobid || job?.id || job?.jobId || job?.jobid || null; +const resolveJobId = (explicit, payload, job) => explicit || payload?.jobId || job?.id || null; /** * Resolve VIN from tx/job shapes @@ -218,6 +217,7 @@ const registerRREvents = ({ socket, redisHelpers }) => { try { const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket); const bodyshop = await getBodyshopForSocket({ bodyshopId, socket }); + CreateRRLogEvent(socket, "DEBUG", "rr-lookup-combined: begin", { jobid, params }); const res = await rrCombinedSearch(bodyshop, params || {}); @@ -230,13 +230,14 @@ const registerRREvents = ({ socket, redisHelpers }) => { const normalized = sortVehicleOwnerFirst(normalizeCustomerCandidates(res, { ownersSet })); const rid = resolveJobId(jobid, { jobid }, null); - const decorated = normalized.map((c) => (c.vinOwner != null ? c : { ...c, vinOwner: !!c.isVehicleOwner })); cb?.({ jobid: rid, data: decorated }); + socket.emit("rr-select-customer", decorated); CreateRRLogEvent(socket, "DEBUG", "rr-lookup-combined: emitted rr-select-customer", { - count: decorated.length + count: decorated.length, + res }); } catch (e) { CreateRRLogEvent(socket, "ERROR", "RR combined lookup error", { error: e.message, jobid }); @@ -320,8 +321,10 @@ const registerRREvents = ({ socket, redisHelpers }) => { socket.on("rr-export-job", async ({ jobid, jobId, txEnvelope } = {}) => { const rid = resolveJobId(jobid || jobId, { jobId, jobid }, null); + try { if (!rid) throw new Error("RR export: jobid required"); + CreateRRLogEvent(socket, "DEBUG", `{1} Received RR export request`, { jobid: rid }); await redisHelpers.setSessionTransactionData( @@ -331,6 +334,7 @@ const registerRREvents = ({ socket, redisHelpers }) => { txEnvelope || {}, defaultRRTTL ); + CreateRRLogEvent(socket, "DEBUG", `{1.1} Cached txEnvelope`, { hasTxEnvelope: !!txEnvelope }); const job = await QueryJobData({ redisHelpers }, rid); @@ -341,12 +345,14 @@ const registerRREvents = ({ socket, redisHelpers }) => { job, defaultRRTTL ); + CreateRRLogEvent(socket, "DEBUG", `{1.2} Cached JobData`, { vin: job?.v_vin, ro: job?.ro_number }); const adv = readAdvisorNo( { txEnvelope }, await redisHelpers.getSessionTransactionData(socket.id, getTransactionType(rid), RRCacheEnums.AdvisorNo) ); + if (adv) { await redisHelpers.setSessionTransactionData( socket.id, @@ -355,6 +361,7 @@ const registerRREvents = ({ socket, redisHelpers }) => { String(adv), defaultRRTTL ); + CreateRRLogEvent(socket, "DEBUG", `{1.3} Cached advisorNo`, { advisorNo: String(adv) }); } @@ -362,9 +369,10 @@ const registerRREvents = ({ socket, redisHelpers }) => { const bodyshop = await getBodyshopForSocket({ bodyshopId, socket }); CreateRRLogEvent(socket, "DEBUG", `{2} Running multi-search (Full Name + VIN)`); - const candidates = await rrMultiCustomerSearch({ bodyshop, job, socket, redisHelpers }); + const candidates = await rrMultiCustomerSearch({ bodyshop, job, socket, redisHelpers }); const decorated = candidates.map((c) => (c.vinOwner != null ? c : { ...c, vinOwner: !!c.isVehicleOwner })); + socket.emit("rr-select-customer", decorated); CreateRRLogEvent(socket, "DEBUG", `{2.1} Emitted rr-select-customer`, { count: decorated.length, @@ -376,6 +384,7 @@ const registerRREvents = ({ socket, redisHelpers }) => { stack: error.stack, jobid: rid }); + try { socket.emit("export-failed", { vendor: "rr", jobId: rid, error: error.message }); } catch { @@ -399,24 +408,31 @@ const registerRREvents = ({ socket, redisHelpers }) => { }); const ns = getTransactionType(rid); + let selectedCustNo = (custNo && String(custNo)) || (selectedCustomerId && String(selectedCustomerId)) || (await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.SelectedCustomer)); job = await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.JobData); + const txEnvelope = (await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.txEnvelope)) || {}; + if (!job) throw new Error("Staged JobData not found (run rr-export-job first)."); const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket); + bodyshop = await getBodyshopForSocket({ bodyshopId, socket }); // Create customer (if requested or none chosen) if (create === true || !selectedCustNo) { CreateRRLogEvent(socket, "DEBUG", `{3.1} Creating RR customer`); + const created = await createRRCustomer({ bodyshop, job, socket }); selectedCustNo = String(created?.customerNo); + if (!selectedCustNo) throw new Error("RR create customer returned no custNo"); + CreateRRLogEvent(socket, "DEBUG", `{3.2} Created customer`, { custNo: selectedCustNo }); } @@ -426,6 +442,7 @@ const registerRREvents = ({ socket, redisHelpers }) => { if (vehQ && vehQ.kind === "vin" && job?.v_vin) { const resVin = await rrCombinedSearch(bodyshop, vehQ); const blocksVin = Array.isArray(resVin?.data) ? resVin.data : Array.isArray(resVin) ? resVin : []; + try { await redisHelpers.setSessionTransactionData( socket.id, @@ -437,9 +454,12 @@ const registerRREvents = ({ socket, redisHelpers }) => { } catch { // } + const ownersSet = ownersFromVinBlocks(blocksVin, job.v_vin); + if (ownersSet?.size) { const sel = String(selectedCustNo); + if (!ownersSet.has(sel)) { const [existingOwner] = Array.from(ownersSet).map(String); CreateRRLogEvent(socket, "DEBUG", `{3.2a} VIN exists; switching to VIN owner`, { @@ -471,6 +491,7 @@ const registerRREvents = ({ socket, redisHelpers }) => { // Cache final/effective customer selection const effectiveCustNo = String(selectedCustNo); + await redisHelpers.setSessionTransactionData( socket.id, ns, @@ -478,6 +499,7 @@ const registerRREvents = ({ socket, redisHelpers }) => { effectiveCustNo, defaultRRTTL ); + CreateRRLogEvent(socket, "DEBUG", `{3.3} Cached selected customer`, { custNo: effectiveCustNo }); // Build client & routing @@ -489,12 +511,13 @@ const registerRREvents = ({ socket, redisHelpers }) => { const tx = { jobData: { ...job, - vin: job?.v_vin || job?.vin || job?.vehicleVin || undefined + vin: job?.v_vin }, txEnvelope }; const vin = resolveVin({ tx, job }); + if (!vin) { CreateRRLogEvent(socket, "ERROR", "{3.x} No VIN found for ensureRRServiceVehicle", { jobid: rid }); throw new Error("ensureRRServiceVehicle: vin required"); @@ -524,9 +547,9 @@ const registerRREvents = ({ socket, redisHelpers }) => { CreateRRLogEvent(socket, "DEBUG", "{3.4} ensureRRServiceVehicle: done", ensured); - // Advisor no const cachedAdvisor = await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.AdvisorNo); const advisorNo = readAdvisorNo({ txEnvelope }, cachedAdvisor); + if (!advisorNo) { CreateRRLogEvent(socket, "ERROR", `Advisor is required (advisorNo)`); await insertRRFailedExportLog({ @@ -540,6 +563,7 @@ const registerRREvents = ({ socket, redisHelpers }) => { socket.emit("export-failed", { vendor: "rr", jobId: rid, error: "Advisor is required (advisorNo)." }); return ack?.({ ok: false, error: "Advisor is required (advisorNo)." }); } + await redisHelpers.setSessionTransactionData( socket.id, ns, @@ -572,17 +596,9 @@ const registerRREvents = ({ socket, redisHelpers }) => { const data = result?.data || {}; // Prefer explicit return from export function; then fall back to fields - const dmsRoNo = - result?.roNo ?? data?.dmsRoNo ?? data?.DMSRoNo ?? data?.roStatus?.dmsRoNo ?? data?.roStatus?.DMSRoNo ?? null; + const dmsRoNo = result?.roNo ?? data?.dmsRoNo ?? null; - const outsdRoNo = - data?.outsdRoNo ?? - data?.OutsdRoNo ?? - data?.roStatus?.outsdRoNo ?? - data?.roStatus?.OutsdRoNo ?? - job?.ro_number ?? - job?.id ?? - null; + const outsdRoNo = data?.outsdRoNo ?? job?.ro_number ?? job?.id ?? null; await redisHelpers.setSessionTransactionData( socket.id, @@ -598,13 +614,13 @@ const registerRREvents = ({ socket, redisHelpers }) => { defaultRRTTL ); - CreateRRLogEvent(socket, "INFO", `{5} RO created. Waiting for cashiering.`, { + CreateRRLogEvent(socket, "INFO", `{5} RO created. Waiting for validation.`, { dmsRoNo: dmsRoNo || null, outsdRoNo: outsdRoNo || null }); // Tell FE to prompt for "Finished/Close" - socket.emit("rr-cashiering-required", { jobId: rid, dmsRoNo, outsdRoNo }); + socket.emit("rr-validation-required", { jobId: rid, dmsRoNo, outsdRoNo }); // Still emit info result if you want socket.emit("rr-export-job:result", { jobId: rid, bodyshopId: bodyshop?.id, result }); @@ -616,6 +632,7 @@ const registerRREvents = ({ socket, redisHelpers }) => { const vendorStatusCode = Number( result?.roStatus?.statusCode ?? result?.roStatus?.StatusCode ?? result?.statusBlocks?.transaction?.statusCode ); + const cls = classifyRRVendorError({ code: vendorStatusCode, message: result?.roStatus?.message ?? result?.roStatus?.Message ?? result?.error ?? "RR export failed" diff --git a/server/rr/rr-utils.js b/server/rr/rr-utils.js index 7ae801d49..b39d686e4 100644 --- a/server/rr/rr-utils.js +++ b/server/rr/rr-utils.js @@ -61,7 +61,6 @@ const normalizeCustomerCandidates = (res, { ownersSet = null } = {}) => { const chosen = arr.find((a) => (a?.Type || a?.type || "").toString().toUpperCase() === "P") || arr[0]; - // NEW: include County const line1 = chosen?.Addr1 ?? chosen?.AddressLine1 ?? chosen?.Line1 ?? chosen?.Street1 ?? undefined; const line2 = chosen?.Addr2 ?? chosen?.AddressLine2 ?? chosen?.Line2 ?? chosen?.Street2 ?? undefined; const city = chosen?.City ?? chosen?.city ?? undefined; @@ -152,10 +151,7 @@ const readAdvisorNo = (payload, cached) => { const tx = payload?.txEnvelope || payload?.envelope || {}; const get = (v) => (v != null && String(v).trim() !== "" ? String(v).trim() : null); - - let value = get(tx?.advisorNo) || get(payload?.advisorNo) || get(cached) || null; - - return value; + return get(tx?.advisorNo) || get(payload?.advisorNo) || get(cached) || null; }; /** @@ -170,7 +166,7 @@ const RRCacheEnums = { VINCandidates: "RR.VINCandidates", SelectedVin: "RR.SelectedVin", ExportResult: "RR.ExportResult", - PendingRO: "RR.PendingRO" // NEW: cache created RO to finalize later + PendingRO: "RR.PendingRO" }; /**