feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Cashiering Checkpoint
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { Alert, Button, Checkbox, Col, message, Table } from "antd";
|
import { Alert, Button, Checkbox, Col, message, Space, Table } from "antd";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -58,7 +58,14 @@ function rrAddressToString(addr) {
|
|||||||
return parts.join(", ");
|
return parts.join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DmsCustomerSelector({ bodyshop, jobid, rrOpenRoLimit = false, onRrOpenRoFinished }) {
|
export function DmsCustomerSelector({
|
||||||
|
bodyshop,
|
||||||
|
jobid,
|
||||||
|
rrOpenRoLimit = false,
|
||||||
|
onRrOpenRoFinished,
|
||||||
|
rrCashierPending = false,
|
||||||
|
onRrCashierFinished
|
||||||
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [customerList, setcustomerList] = useState([]);
|
const [customerList, setcustomerList] = useState([]);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -87,6 +94,14 @@ export function DmsCustomerSelector({ bodyshop, jobid, rrOpenRoLimit = false, on
|
|||||||
}, [customerList]);
|
}, [customerList]);
|
||||||
const rrHasVinOwner = rrOwnerSet.size > 0;
|
const rrHasVinOwner = rrOwnerSet.size > 0;
|
||||||
|
|
||||||
|
// If cashiering is pending, surface this banner by opening selector
|
||||||
|
useEffect(() => {
|
||||||
|
if (dms === "rr" && rrCashierPending) {
|
||||||
|
setOpen(true);
|
||||||
|
setDmsType("rr");
|
||||||
|
}
|
||||||
|
}, [dms, rrCashierPending]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dms === "rr") {
|
if (dms === "rr") {
|
||||||
const handleRrSelectCustomer = (list) => {
|
const handleRrSelectCustomer = (list) => {
|
||||||
@@ -160,10 +175,11 @@ export function DmsCustomerSelector({ bodyshop, jobid, rrOpenRoLimit = false, on
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dmsType === "rr") {
|
if (dmsType === "rr") {
|
||||||
wsssocket.emit("rr-selected-customer", { jobId: jobid, custNo: String(selectedCustomer) }, (ack) => {
|
// Keep the selector open; server will raise rr-cashiering-required
|
||||||
|
wsssocket.emit("rr-selected-customer", { jobId: jobid, create: true }, (ack) => {
|
||||||
if (ack?.ok) {
|
if (ack?.ok) {
|
||||||
setOpen(false);
|
message.success(t("dms.messages.customerCreated"));
|
||||||
setSelectedCustomer(null);
|
// Keep dialog open; cashiering banner will appear via `rr-cashiering-required`
|
||||||
} else if (ack?.error) {
|
} else if (ack?.error) {
|
||||||
message.error(ack.error);
|
message.error(ack.error);
|
||||||
}
|
}
|
||||||
@@ -200,10 +216,10 @@ export function DmsCustomerSelector({ bodyshop, jobid, rrOpenRoLimit = false, on
|
|||||||
if (dmsType === "rr" && rrHasVinOwner) return;
|
if (dmsType === "rr" && rrHasVinOwner) return;
|
||||||
|
|
||||||
if (dmsType === "rr") {
|
if (dmsType === "rr") {
|
||||||
|
// Keep open; server will raise rr-cashiering-required
|
||||||
wsssocket.emit("rr-selected-customer", { jobId: jobid, create: true }, (ack) => {
|
wsssocket.emit("rr-selected-customer", { jobId: jobid, create: true }, (ack) => {
|
||||||
if (ack?.ok) {
|
if (ack?.ok) {
|
||||||
if (ack.custNo) setSelectedCustomer(String(ack.custNo));
|
if (ack.custNo) setSelectedCustomer(String(ack.custNo));
|
||||||
setOpen(false);
|
|
||||||
message.success(t("dms.messages.customerCreated"));
|
message.success(t("dms.messages.customerCreated"));
|
||||||
} else if (ack?.error) {
|
} else if (ack?.error) {
|
||||||
message.error(ack.error);
|
message.error(ack.error);
|
||||||
@@ -388,6 +404,31 @@ export function DmsCustomerSelector({ bodyshop, jobid, rrOpenRoLimit = false, on
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* NEW: Cashiering required banner */}
|
||||||
|
{dmsType === "rr" && rrCashierPending && (
|
||||||
|
<Alert
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
message="Complete cashiering in Reynolds"
|
||||||
|
description={
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||||
|
<div>
|
||||||
|
We created the Repair Order in Reynolds. Please complete the cashiering/closeout steps in
|
||||||
|
Reynolds. When done, click <strong>Finished/Close</strong> to finalize and mark this export as
|
||||||
|
complete.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" onClick={onRrCashierFinished}>
|
||||||
|
Finished / Close
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
||||||
<Button onClick={onUseSelected} disabled={!selectedCustomer || (dmsType === "rr" && rrOpenRoLimit)}>
|
<Button onClick={onUseSelected} disabled={!selectedCustomer || (dmsType === "rr" && rrOpenRoLimit)}>
|
||||||
{t("jobs.actions.dms.useselected")}
|
{t("jobs.actions.dms.useselected")}
|
||||||
@@ -405,7 +446,7 @@ export function DmsCustomerSelector({ bodyshop, jobid, rrOpenRoLimit = false, on
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* NEW: VIN ownership enforced with Refresh */}
|
{/* VIN ownership enforced with Refresh */}
|
||||||
{dmsType === "rr" && rrHasVinOwner && (
|
{dmsType === "rr" && rrHasVinOwner && (
|
||||||
<Alert
|
<Alert
|
||||||
type="warning"
|
type="warning"
|
||||||
|
|||||||
@@ -83,6 +83,9 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
const [rrOpenRoLimit, setRrOpenRoLimit] = useState(false);
|
const [rrOpenRoLimit, setRrOpenRoLimit] = useState(false);
|
||||||
const clearRrOpenRoLimit = () => setRrOpenRoLimit(false);
|
const clearRrOpenRoLimit = () => setRrOpenRoLimit(false);
|
||||||
|
|
||||||
|
// NEW: RR “cashiering required” UX hold
|
||||||
|
const [rrCashierPending, setRrCashierPending] = useState(false);
|
||||||
|
|
||||||
const handleExportFailed = (payload = {}) => {
|
const handleExportFailed = (payload = {}) => {
|
||||||
const { title, friendlyMessage, error, severity, errorCode, vendorStatusCode } = payload;
|
const { title, friendlyMessage, error, severity, errorCode, vendorStatusCode } = payload;
|
||||||
|
|
||||||
@@ -148,7 +151,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
}, [t, setBreadcrumbs, setSelectedHeader]);
|
}, [t, setBreadcrumbs, setSelectedHeader]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// ✅ RR uses the new wss socket and takes precedence over Fortellis flag
|
// ✅ RR path uses WSS and has two-step flow
|
||||||
if (dms === "rr") {
|
if (dms === "rr") {
|
||||||
// set log level on connect and immediately
|
// set log level on connect and immediately
|
||||||
wsssocket.emit("set-log-level", logLevel);
|
wsssocket.emit("set-log-level", logLevel);
|
||||||
@@ -165,9 +168,11 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
|
|
||||||
const handleLogEvent = (payload) => setLogs((prev) => [...prev, payload]);
|
const handleLogEvent = (payload) => setLogs((prev) => [...prev, payload]);
|
||||||
|
|
||||||
|
// FINAL step only (emitted by server after rr-finalize-repair-order)
|
||||||
const handleExportSuccess = (payload) => {
|
const handleExportSuccess = (payload) => {
|
||||||
const jobId = payload?.jobId ?? payload; // RR sends object; legacy sends raw id
|
const jobId = payload?.jobId ?? payload; // RR sends object; legacy sends raw id
|
||||||
notification.success({ message: t("jobs.successes.exported") });
|
notification.success({ message: t("jobs.successes.exported") });
|
||||||
|
setRrCashierPending(false);
|
||||||
insertAuditTrail({
|
insertAuditTrail({
|
||||||
jobid: jobId,
|
jobid: jobId,
|
||||||
operation: AuditTrailMapping.jobexported(),
|
operation: AuditTrailMapping.jobexported(),
|
||||||
@@ -175,7 +180,43 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
});
|
});
|
||||||
history("/manage/accounting/receivables");
|
history("/manage/accounting/receivables");
|
||||||
};
|
};
|
||||||
const handleRrExportResult = (payload) => handleExportSuccess(payload);
|
|
||||||
|
// STEP 1 result (RO created) – DO NOT navigate; wait for cashiering
|
||||||
|
const handleRrExportResult = () => {
|
||||||
|
// Be defensive: if the server didn't already set the banner yet, make it obvious
|
||||||
|
setRrCashierPending(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."
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
notification.info({
|
||||||
|
message: "Reynolds RO created",
|
||||||
|
description:
|
||||||
|
"Complete cashiering in Reynolds, then click Finished/Close to finalize and mark this export complete.",
|
||||||
|
duration: 8
|
||||||
|
});
|
||||||
|
// No routing here — we remain on the page for step 2
|
||||||
|
};
|
||||||
|
|
||||||
|
// NEW: cashier step required (after create, before finalize)
|
||||||
|
const handleCashieringRequired = (payload) => {
|
||||||
|
setRrCashierPending(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.",
|
||||||
|
meta: { payload }
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
wsssocket.on("connect", handleConnect);
|
wsssocket.on("connect", handleConnect);
|
||||||
wsssocket.on("reconnect", handleReconnect);
|
wsssocket.on("reconnect", handleReconnect);
|
||||||
@@ -188,6 +229,9 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
wsssocket.on("export-success", handleExportSuccess);
|
wsssocket.on("export-success", handleExportSuccess);
|
||||||
wsssocket.on("export-failed", handleExportFailed);
|
wsssocket.on("export-failed", handleExportFailed);
|
||||||
|
|
||||||
|
// NEW
|
||||||
|
wsssocket.on("rr-cashiering-required", handleCashieringRequired);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
wsssocket.off("connect", handleConnect);
|
wsssocket.off("connect", handleConnect);
|
||||||
wsssocket.off("reconnect", handleReconnect);
|
wsssocket.off("reconnect", handleReconnect);
|
||||||
@@ -198,6 +242,8 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
|
|
||||||
wsssocket.off("export-success", handleExportSuccess);
|
wsssocket.off("export-success", handleExportSuccess);
|
||||||
wsssocket.off("export-failed", handleExportFailed);
|
wsssocket.off("export-failed", handleExportFailed);
|
||||||
|
|
||||||
|
wsssocket.off("rr-cashiering-required", handleCashieringRequired);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,6 +306,20 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
}
|
}
|
||||||
}, [dms, Fortellis?.treatment, logLevel, history, insertAuditTrail, notification, t, wsssocket]);
|
}, [dms, Fortellis?.treatment, logLevel, history, insertAuditTrail, notification, t, wsssocket]);
|
||||||
|
|
||||||
|
// NEW: finalize button callback—emit finalize event
|
||||||
|
const handleRrCashierFinished = () => {
|
||||||
|
if (!jobId) return;
|
||||||
|
wsssocket.emit("rr-finalize-repair-order", { jobId }, (ack) => {
|
||||||
|
if (ack?.ok) {
|
||||||
|
// success path handled by export-success listener
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ack?.error) {
|
||||||
|
notification.error({ message: ack.error });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) return <LoadingSpinner />;
|
if (loading) return <LoadingSpinner />;
|
||||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
|
|
||||||
@@ -299,7 +359,15 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
<DmsPostForm socket={activeSocket} job={data?.jobs_by_pk} logsRef={logsRef} />
|
<DmsPostForm socket={activeSocket} job={data?.jobs_by_pk} logsRef={logsRef} />
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<DmsCustomerSelector jobid={jobId} rrOpenRoLimit={rrOpenRoLimit} onRrOpenRoFinished={clearRrOpenRoLimit} />
|
{/* NEW props for two-step RR flow banners */}
|
||||||
|
<DmsCustomerSelector
|
||||||
|
jobid={jobId}
|
||||||
|
rrOpenRoLimit={rrOpenRoLimit}
|
||||||
|
onRrOpenRoFinished={clearRrOpenRoLimit}
|
||||||
|
rrCashierPending={rrCashierPending}
|
||||||
|
onRrCashierFinished={handleRrCashierFinished}
|
||||||
|
bodyshop={bodyshop}
|
||||||
|
/>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<div ref={logsRef}>
|
<div ref={logsRef}>
|
||||||
<Card
|
<Card
|
||||||
|
|||||||
@@ -3,7 +3,70 @@ const { RRClient } = require("./lib/index.cjs");
|
|||||||
const { getRRConfigFromBodyshop } = require("./rr-config");
|
const { getRRConfigFromBodyshop } = require("./rr-config");
|
||||||
const RRLogger = require("./rr-logger");
|
const RRLogger = require("./rr-logger");
|
||||||
|
|
||||||
// Build client + opts from bodyshop
|
const COUNTRY_MAP = {
|
||||||
|
US: "US",
|
||||||
|
USA: "US",
|
||||||
|
"UNITED STATES": "US",
|
||||||
|
CA: "CA",
|
||||||
|
CAN: "CA",
|
||||||
|
CANADA: "CA"
|
||||||
|
};
|
||||||
|
|
||||||
|
function toCountry2(v) {
|
||||||
|
const s = String(v || "")
|
||||||
|
.trim()
|
||||||
|
.toUpperCase();
|
||||||
|
if (!s) return "US"; // sane default
|
||||||
|
if (COUNTRY_MAP[s]) return COUNTRY_MAP[s];
|
||||||
|
// fallbacks: prefer 2-char; last resort: take first 2
|
||||||
|
return s.length === 2 ? s : s.slice(0, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePhone(num) {
|
||||||
|
const d = String(num || "").replace(/\D/g, "");
|
||||||
|
const n = d.length === 11 && d.startsWith("1") ? d.slice(1) : d;
|
||||||
|
return n.slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePostal(pc, country) {
|
||||||
|
const s = String(pc || "").trim();
|
||||||
|
if (country === "US") return s.replace(/[^0-9]/g, "").slice(0, 5);
|
||||||
|
if (country === "CA") return s.toUpperCase().replace(/\s+/g, "").slice(0, 6);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeRRCustomerPayload(payload = {}) {
|
||||||
|
const out = { ...payload };
|
||||||
|
|
||||||
|
out.addresses = (payload.addresses || []).map((a) => {
|
||||||
|
const country = toCountry2(a.country);
|
||||||
|
return {
|
||||||
|
...a,
|
||||||
|
country,
|
||||||
|
state: String(a.state || "")
|
||||||
|
.toUpperCase()
|
||||||
|
.slice(0, 2),
|
||||||
|
postalCode: normalizePostal(a.postalCode, country)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
out.phones = (payload.phones || []).map((p) => ({
|
||||||
|
...p,
|
||||||
|
number: normalizePhone(p.number)
|
||||||
|
}));
|
||||||
|
|
||||||
|
// trim names defensively (RR has various max lengths by site config)
|
||||||
|
if (out.firstName) out.firstName = String(out.firstName).trim().slice(0, 30);
|
||||||
|
if (out.lastName) out.lastName = String(out.lastName).trim().slice(0, 30);
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an RR client + common opts from a bodyshop row
|
||||||
|
* @param bodyshop
|
||||||
|
* @returns {{client: *, opts: {routing: {dealerNumber: *, storeNumber: *, areaNumber: *}, envelope: {sender: {component: string, task: string, referenceId: string, creator: string, senderName: string}}}}}
|
||||||
|
*/
|
||||||
function buildClientAndOpts(bodyshop) {
|
function buildClientAndOpts(bodyshop) {
|
||||||
const cfg = getRRConfigFromBodyshop(bodyshop);
|
const cfg = getRRConfigFromBodyshop(bodyshop);
|
||||||
const client = new RRClient({
|
const client = new RRClient({
|
||||||
@@ -29,19 +92,29 @@ function buildClientAndOpts(bodyshop) {
|
|||||||
return { client, opts };
|
return { client, opts };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip all non-digit characters from a string
|
||||||
|
* @param s
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
function digitsOnly(s) {
|
function digitsOnly(s) {
|
||||||
return String(s || "").replace(/\D/g, "");
|
return String(s || "").replace(/\D/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a new array with only unique values from the input array
|
||||||
|
* @param arr
|
||||||
|
* @returns {any[]}
|
||||||
|
*/
|
||||||
function uniq(arr) {
|
function uniq(arr) {
|
||||||
return Array.from(new Set(arr));
|
return Array.from(new Set(arr));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a payload that matches the RR client expectations for insert/update:
|
* Build RR customer payload from job.ownr_* fields, with optional overrides.
|
||||||
* - ibFlag: 'I' (individual) or 'B' (business). If we have a first name, default to 'I', else 'B' if company present.
|
* @param job
|
||||||
* - Must include lastName OR customerName.
|
* @param overrides
|
||||||
* - addresses[] / phones[] / emails[] per the library’s toView() contract.
|
* @returns {{ibFlag: string, firstName, lastName, customerName, createdBy, customerType, addresses: [{type, line1: *, line2, city, state, postalCode, country}], phones: {number: *}[], emails: [{address: string}]}}
|
||||||
*/
|
*/
|
||||||
function buildCustomerPayloadFromJob(job, overrides = {}) {
|
function buildCustomerPayloadFromJob(job, overrides = {}) {
|
||||||
// Pull ONLY from job.ownr_* fields (no job.customer.*)
|
// Pull ONLY from job.ownr_* fields (no job.customer.*)
|
||||||
@@ -114,7 +187,8 @@ async function createRRCustomer({ bodyshop, job, overrides = {}, socket }) {
|
|||||||
|
|
||||||
let res;
|
let res;
|
||||||
try {
|
try {
|
||||||
res = await client.insertCustomer(payload, opts);
|
const safePayload = sanitizeRRCustomerPayload(payload);
|
||||||
|
res = await client.insertCustomer(safePayload, opts);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log("error", "RR insertCustomer transport error", { message: e?.message, stack: e?.stack, payload });
|
log("error", "RR insertCustomer transport error", { message: e?.message, stack: e?.stack, payload });
|
||||||
throw e;
|
throw e;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// server/rr/rr-job-export.js
|
|
||||||
const { buildRRRepairOrderPayload } = require("./rr-job-helpers");
|
const { buildRRRepairOrderPayload } = require("./rr-job-helpers");
|
||||||
const { buildClientAndOpts } = require("./rr-lookup");
|
const { buildClientAndOpts } = require("./rr-lookup");
|
||||||
const { ensureRRServiceVehicle } = require("./rr-service-vehicles");
|
const { ensureRRServiceVehicle } = require("./rr-service-vehicles");
|
||||||
@@ -8,6 +7,9 @@ const RRLogger = require("./rr-logger");
|
|||||||
* Orchestrate an RR export (assumes custNo already resolved):
|
* Orchestrate an RR export (assumes custNo already resolved):
|
||||||
* - Ensure service vehicle (create flows)
|
* - Ensure service vehicle (create flows)
|
||||||
* - Create or update the Repair Order
|
* - Create or update the Repair Order
|
||||||
|
*
|
||||||
|
* NOTE: This function performs the create/update step and returns the RO data.
|
||||||
|
* "Mark exported" is handled later by the finalize step after cashiering.
|
||||||
*/
|
*/
|
||||||
async function exportJobToRR(args) {
|
async function exportJobToRR(args) {
|
||||||
const { bodyshop, job, advisorNo, selectedCustomer, existing, socket } = args || {};
|
const { bodyshop, job, advisorNo, selectedCustomer, existing, socket } = args || {};
|
||||||
@@ -31,14 +33,15 @@ async function exportJobToRR(args) {
|
|||||||
sender: {
|
sender: {
|
||||||
...(opts?.envelope?.sender || {}),
|
...(opts?.envelope?.sender || {}),
|
||||||
task: "BSMRO",
|
task: "BSMRO",
|
||||||
referenceId: existing?.dmsRepairOrderId ? "Update" : "Insert"
|
// If we have an existing RO number we'll be updating, otherwise inserting
|
||||||
|
referenceId: existing?.roNo || existing?.dmsRoNo || existing?.dmsRepairOrderId ? "Update" : "Insert"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ensure service vehicle for create flows (best-effort)
|
// Ensure service vehicle for create flows (best-effort)
|
||||||
let svId = null;
|
let svId = null;
|
||||||
if (!existing?.dmsRepairOrderId) {
|
if (!(existing?.roNo || existing?.dmsRoNo || existing?.dmsRepairOrderId)) {
|
||||||
try {
|
try {
|
||||||
const svRes = await ensureRRServiceVehicle({
|
const svRes = await ensureRRServiceVehicle({
|
||||||
bodyshop,
|
bodyshop,
|
||||||
@@ -54,19 +57,33 @@ async function exportJobToRR(args) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build RO payload (now includes DeptType/departmentType + variants)
|
// Build RO payload for create/update
|
||||||
const payload = buildRRRepairOrderPayload({
|
const payload = buildRRRepairOrderPayload({
|
||||||
job,
|
job,
|
||||||
selectedCustomer: { customerNo: String(selected), custNo: String(selected) },
|
selectedCustomer: { customerNo: String(selected), custNo: String(selected) },
|
||||||
advisorNo: String(advisorNo)
|
advisorNo: String(advisorNo)
|
||||||
});
|
});
|
||||||
|
|
||||||
const rrRes = existing?.dmsRepairOrderId
|
// Canonical update key is "roNo" (prefer DMS RO number); accept fallbacks from "existing"
|
||||||
? await client.updateRepairOrder({ ...payload, dmsRepairOrderId: existing.dmsRepairOrderId }, finalOpts)
|
const roNoForUpdate = existing?.roNo || existing?.dmsRoNo || existing?.dmsRepairOrderId || null;
|
||||||
|
|
||||||
|
const rrRes = roNoForUpdate
|
||||||
|
? await client.updateRepairOrder({ ...payload, roNo: String(roNoForUpdate) }, finalOpts) // ✅ use roNo on update
|
||||||
: await client.createRepairOrder(payload, finalOpts);
|
: await client.createRepairOrder(payload, finalOpts);
|
||||||
|
|
||||||
const data = rrRes?.data || null;
|
const data = rrRes?.data || null;
|
||||||
const roStatus = data?.roStatus || null;
|
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;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: rrRes?.success === true || roStatus?.status === "Success",
|
success: rrRes?.success === true || roStatus?.status === "Success",
|
||||||
data,
|
data,
|
||||||
@@ -75,8 +92,85 @@ async function exportJobToRR(args) {
|
|||||||
xml: rrRes?.xml,
|
xml: rrRes?.xml,
|
||||||
parsed: rrRes?.parsed,
|
parsed: rrRes?.parsed,
|
||||||
customerNo: String(selected),
|
customerNo: String(selected),
|
||||||
svId
|
svId,
|
||||||
|
roNo
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { exportJobToRR };
|
/**
|
||||||
|
* Finalize an RR Repair Order by sending finalUpdate: "Y".
|
||||||
|
* The caller should pass the canonical `roNo` if available (prefer DMS RO #).
|
||||||
|
* If not provided, we *safely* fall back to the external (Outsd) RO number.
|
||||||
|
*/
|
||||||
|
async function finalizeRRRepairOrder(args) {
|
||||||
|
const { bodyshop, job, advisorNo, customerNo, roNo, vin, socket } = args || {};
|
||||||
|
const log = RRLogger(socket, { ns: "rr-finalize" });
|
||||||
|
|
||||||
|
if (!bodyshop) throw new Error("finalizeRRRepairOrder: bodyshop is required");
|
||||||
|
if (!job) throw new Error("finalizeRRRepairOrder: job is required");
|
||||||
|
if (!advisorNo) throw new Error("finalizeRRRepairOrder: advisorNo is required");
|
||||||
|
if (!customerNo) throw new Error("finalizeRRRepairOrder: customerNo is required");
|
||||||
|
|
||||||
|
// The external (Outsd) RO is our deterministic fallback and correlation id.
|
||||||
|
const externalRo = job?.ro_number ?? job?.id;
|
||||||
|
if (externalRo == null) throw new Error("finalizeRRRepairOrder: outsdRoNo (job.ro_number/id) is required");
|
||||||
|
|
||||||
|
// Prefer DMS RO for update; fall back to external when DMS RO isn't known
|
||||||
|
const roNoToSend = roNo ? String(roNo) : String(externalRo);
|
||||||
|
|
||||||
|
const { client, opts } = buildClientAndOpts(bodyshop);
|
||||||
|
const finalOpts = {
|
||||||
|
...opts,
|
||||||
|
envelope: {
|
||||||
|
...(opts?.envelope || {}),
|
||||||
|
sender: {
|
||||||
|
...(opts?.envelope?.sender || {}),
|
||||||
|
task: "BSMRO",
|
||||||
|
referenceId: "Update"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanVin =
|
||||||
|
(job?.v_vin || vin || "")
|
||||||
|
.toString()
|
||||||
|
.replace(/[^A-Za-z0-9]/g, "")
|
||||||
|
.toUpperCase()
|
||||||
|
.slice(0, 17) || undefined;
|
||||||
|
|
||||||
|
// IMPORTANT: include "roNo" on updates (RR requires it). Also send outsdRoNo for correlation.
|
||||||
|
const payload = {
|
||||||
|
roNo: roNoToSend, // ✅ REQUIRED BY RR on update
|
||||||
|
outsdRoNo: String(externalRo),
|
||||||
|
finalUpdate: "Y",
|
||||||
|
departmentType: "B",
|
||||||
|
customerNo: String(customerNo),
|
||||||
|
advisorNo: String(advisorNo),
|
||||||
|
vin: cleanVin,
|
||||||
|
mileageIn: job?.kmin,
|
||||||
|
mileageOut: job?.kmout,
|
||||||
|
estimate: { estimateType: "Final" }
|
||||||
|
};
|
||||||
|
|
||||||
|
log("info", "RR finalize updateRepairOrder", {
|
||||||
|
roNo: roNoToSend,
|
||||||
|
outsdRoNo: String(externalRo),
|
||||||
|
customerNo: String(customerNo),
|
||||||
|
advisorNo: String(advisorNo)
|
||||||
|
});
|
||||||
|
|
||||||
|
const rrRes = await client.updateRepairOrder(payload, finalOpts);
|
||||||
|
const data = rrRes?.data || null;
|
||||||
|
const roStatus = data?.roStatus || null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: rrRes?.success === true || roStatus?.status === "Success",
|
||||||
|
data,
|
||||||
|
roStatus,
|
||||||
|
statusBlocks: rrRes?.statusBlocks || [],
|
||||||
|
xml: rrRes?.xml,
|
||||||
|
parsed: rrRes?.parsed
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { exportJobToRR, finalizeRRRepairOrder };
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const CreateRRLogEvent = require("./rr-logger-event");
|
const CreateRRLogEvent = require("./rr-logger-event");
|
||||||
const { rrCombinedSearch, rrGetAdvisors, buildClientAndOpts } = require("./rr-lookup");
|
const { rrCombinedSearch, rrGetAdvisors, buildClientAndOpts } = require("./rr-lookup");
|
||||||
const { QueryJobData } = require("./rr-job-helpers");
|
const { QueryJobData } = require("./rr-job-helpers");
|
||||||
const { exportJobToRR } = require("./rr-job-export");
|
const { exportJobToRR, finalizeRRRepairOrder } = require("./rr-job-export");
|
||||||
const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
|
const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
|
||||||
const { createRRCustomer } = require("./rr-customers");
|
const { createRRCustomer } = require("./rr-customers");
|
||||||
const { ensureRRServiceVehicle } = require("./rr-service-vehicles");
|
const { ensureRRServiceVehicle } = require("./rr-service-vehicles");
|
||||||
@@ -317,7 +317,7 @@ function registerRREvents({ socket, redisHelpers }) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ================= Fortellis-style two-step export =================
|
// ================= Fortellis-style two-step export (RR only) =================
|
||||||
// 1) Stage export -> search (Full Name + VIN) -> emit rr-select-customer
|
// 1) Stage export -> search (Full Name + VIN) -> emit rr-select-customer
|
||||||
socket.on("rr-export-job", async ({ jobid, jobId, txEnvelope } = {}) => {
|
socket.on("rr-export-job", async ({ jobid, jobId, txEnvelope } = {}) => {
|
||||||
const rid = resolveJobId(jobid || jobId, { jobId, jobid }, null);
|
const rid = resolveJobId(jobid || jobId, { jobId, jobid }, null);
|
||||||
@@ -385,7 +385,7 @@ function registerRREvents({ socket, redisHelpers }) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2) Selection (or create) -> ensure vehicle -> export
|
// 2) Selection (or create) -> ensure vehicle -> CREATE RO (do not mark exported)
|
||||||
socket.on("rr-selected-customer", async ({ jobid, jobId, selectedCustomerId, custNo, create } = {}, ack) => {
|
socket.on("rr-selected-customer", async ({ jobid, jobId, selectedCustomerId, custNo, create } = {}, ack) => {
|
||||||
const rid = resolveJobId(jobid || jobId, { jobid, jobId }, null);
|
const rid = resolveJobId(jobid || jobId, { jobid, jobId }, null);
|
||||||
let bodyshop = null;
|
let bodyshop = null;
|
||||||
@@ -515,7 +515,6 @@ function registerRREvents({ socket, redisHelpers }) {
|
|||||||
client,
|
client,
|
||||||
routing,
|
routing,
|
||||||
bodyshop,
|
bodyshop,
|
||||||
// Normalize for any internal checks:
|
|
||||||
selectedCustomerNo: effectiveCustNo,
|
selectedCustomerNo: effectiveCustNo,
|
||||||
custNo: effectiveCustNo,
|
custNo: effectiveCustNo,
|
||||||
customerNo: effectiveCustNo,
|
customerNo: effectiveCustNo,
|
||||||
@@ -532,7 +531,6 @@ function registerRREvents({ socket, redisHelpers }) {
|
|||||||
const advisorNo = readAdvisorNo({ txEnvelope }, cachedAdvisor);
|
const advisorNo = readAdvisorNo({ txEnvelope }, cachedAdvisor);
|
||||||
if (!advisorNo) {
|
if (!advisorNo) {
|
||||||
CreateRRLogEvent(socket, "ERROR", `Advisor is required (advisorNo)`);
|
CreateRRLogEvent(socket, "ERROR", `Advisor is required (advisorNo)`);
|
||||||
// Failure log (no advisor)
|
|
||||||
await insertRRFailedExportLog({
|
await insertRRFailedExportLog({
|
||||||
socket,
|
socket,
|
||||||
jobId: rid,
|
jobId: rid,
|
||||||
@@ -552,8 +550,8 @@ function registerRREvents({ socket, redisHelpers }) {
|
|||||||
defaultRRTTL
|
defaultRRTTL
|
||||||
);
|
);
|
||||||
|
|
||||||
// Export
|
// CREATE/UPDATE (first step only)
|
||||||
CreateRRLogEvent(socket, "DEBUG", `{4} Performing RR export`);
|
CreateRRLogEvent(socket, "DEBUG", `{4} Performing RR create/update (step 1)`);
|
||||||
const result = await exportJobToRR({
|
const result = await exportJobToRR({
|
||||||
bodyshop,
|
bodyshop,
|
||||||
job,
|
job,
|
||||||
@@ -563,22 +561,60 @@ function registerRREvents({ socket, redisHelpers }) {
|
|||||||
socket
|
socket
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.success) {
|
// Cache raw export result + pending RO number for finalize
|
||||||
CreateRRLogEvent(socket, "DEBUG", `{5} Export success`, { roStatus: result.roStatus });
|
await redisHelpers.setSessionTransactionData(
|
||||||
|
socket.id,
|
||||||
|
ns,
|
||||||
|
RRCacheEnums.ExportResult,
|
||||||
|
result || {},
|
||||||
|
defaultRRTTL
|
||||||
|
);
|
||||||
|
|
||||||
// ✅ Mark exported + success log (with metadata)
|
if (result?.success) {
|
||||||
await markRRExportSuccess({
|
const data = result?.data || {};
|
||||||
socket,
|
|
||||||
jobId: rid,
|
// Prefer explicit return from export function; then fall back to fields
|
||||||
job,
|
const dmsRoNo =
|
||||||
bodyshop,
|
result?.roNo ?? data?.dmsRoNo ?? data?.DMSRoNo ?? data?.roStatus?.dmsRoNo ?? data?.roStatus?.DMSRoNo ?? null;
|
||||||
result
|
|
||||||
|
const outsdRoNo =
|
||||||
|
data?.outsdRoNo ??
|
||||||
|
data?.OutsdRoNo ??
|
||||||
|
data?.roStatus?.outsdRoNo ??
|
||||||
|
data?.roStatus?.OutsdRoNo ??
|
||||||
|
job?.ro_number ??
|
||||||
|
job?.id ??
|
||||||
|
null;
|
||||||
|
|
||||||
|
await redisHelpers.setSessionTransactionData(
|
||||||
|
socket.id,
|
||||||
|
ns,
|
||||||
|
RRCacheEnums.PendingRO,
|
||||||
|
{
|
||||||
|
outsdRoNo,
|
||||||
|
dmsRoNo,
|
||||||
|
customerNo: String(effectiveCustNo),
|
||||||
|
advisorNo: String(advisorNo),
|
||||||
|
vin: job?.v_vin || null
|
||||||
|
},
|
||||||
|
defaultRRTTL
|
||||||
|
);
|
||||||
|
|
||||||
|
CreateRRLogEvent(socket, "INFO", `{5} RO created. Waiting for cashiering.`, {
|
||||||
|
dmsRoNo: dmsRoNo || null,
|
||||||
|
outsdRoNo: outsdRoNo || null
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.emit("export-success", { vendor: "rr", jobId: rid, roStatus: result.roStatus });
|
// Tell FE to prompt for "Finished/Close"
|
||||||
ack?.({ ok: true, result });
|
socket.emit("rr-cashiering-required", { jobId: rid, dmsRoNo, outsdRoNo });
|
||||||
|
|
||||||
|
// Still emit info result if you want
|
||||||
|
socket.emit("rr-export-job:result", { jobId: rid, bodyshopId: bodyshop?.id, result });
|
||||||
|
|
||||||
|
// ACK but indicate it's pending finalize
|
||||||
|
ack?.({ ok: true, pendingFinalize: true, dmsRoNo, outsdRoNo, result });
|
||||||
} else {
|
} else {
|
||||||
// NEW: classify vendor status for a friendly FE message
|
// classify & fail (no finalize)
|
||||||
const vendorStatusCode = Number(
|
const vendorStatusCode = Number(
|
||||||
result?.roStatus?.statusCode ?? result?.roStatus?.StatusCode ?? result?.statusBlocks?.transaction?.statusCode
|
result?.roStatus?.statusCode ?? result?.roStatus?.StatusCode ?? result?.statusBlocks?.transaction?.statusCode
|
||||||
);
|
);
|
||||||
@@ -587,12 +623,11 @@ function registerRREvents({ socket, redisHelpers }) {
|
|||||||
message: result?.roStatus?.message ?? result?.roStatus?.Message ?? result?.error ?? "RR export failed"
|
message: result?.roStatus?.message ?? result?.roStatus?.Message ?? result?.error ?? "RR export failed"
|
||||||
});
|
});
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "ERROR", `Export failed`, {
|
CreateRRLogEvent(socket, "ERROR", `Export failed (step 1)`, {
|
||||||
roStatus: result?.roStatus,
|
roStatus: result?.roStatus,
|
||||||
classification: cls
|
classification: cls
|
||||||
});
|
});
|
||||||
|
|
||||||
// ❌ Failure log (with classification + bits of response)
|
|
||||||
await insertRRFailedExportLog({
|
await insertRRFailedExportLog({
|
||||||
socket,
|
socket,
|
||||||
jobId: rid,
|
jobId: rid,
|
||||||
@@ -609,8 +644,6 @@ function registerRREvents({ socket, redisHelpers }) {
|
|||||||
error: cls?.friendlyMessage || result?.error || "RR export failed",
|
error: cls?.friendlyMessage || result?.error || "RR export failed",
|
||||||
...cls
|
...cls
|
||||||
});
|
});
|
||||||
// Optional: a user-focused channel if you want to show inline banners
|
|
||||||
socket.emit("rr-user-notice", { jobId: rid, ...cls });
|
|
||||||
|
|
||||||
ack?.({
|
ack?.({
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -619,15 +652,6 @@ function registerRREvents({ socket, redisHelpers }) {
|
|||||||
classification: cls
|
classification: cls
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await redisHelpers.setSessionTransactionData(
|
|
||||||
socket.id,
|
|
||||||
ns,
|
|
||||||
RRCacheEnums.ExportResult,
|
|
||||||
result || {},
|
|
||||||
defaultRRTTL
|
|
||||||
);
|
|
||||||
socket.emit("rr-export-job:result", { jobId: rid, bodyshopId: bodyshop?.id, result });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const cls = classifyRRVendorError(error);
|
const cls = classifyRRVendorError(error);
|
||||||
|
|
||||||
@@ -640,9 +664,7 @@ function registerRREvents({ socket, redisHelpers }) {
|
|||||||
jobid: rid
|
jobid: rid
|
||||||
});
|
});
|
||||||
|
|
||||||
// ❌ Failure log for thrown error path
|
|
||||||
try {
|
try {
|
||||||
// Load bodyshop/job if not loaded yet (best-effort)
|
|
||||||
if (!bodyshop || !job) {
|
if (!bodyshop || !job) {
|
||||||
const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
|
const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
|
||||||
bodyshop = bodyshop || (await getBodyshopForSocket({ bodyshopId, socket }));
|
bodyshop = bodyshop || (await getBodyshopForSocket({ bodyshopId, socket }));
|
||||||
@@ -651,7 +673,7 @@ function registerRREvents({ socket, redisHelpers }) {
|
|||||||
(await redisHelpers.getSessionTransactionData(socket.id, getTransactionType(rid), RRCacheEnums.JobData));
|
(await redisHelpers.getSessionTransactionData(socket.id, getTransactionType(rid), RRCacheEnums.JobData));
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
await insertRRFailedExportLog({
|
await insertRRFailedExportLog({
|
||||||
@@ -670,10 +692,155 @@ function registerRREvents({ socket, redisHelpers }) {
|
|||||||
error: error.message,
|
error: error.message,
|
||||||
...cls
|
...cls
|
||||||
});
|
});
|
||||||
// Optional UX hook for inline banners/toasts
|
|
||||||
socket.emit("rr-user-notice", { jobId: rid, ...cls });
|
socket.emit("rr-user-notice", { jobId: rid, ...cls });
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
ack?.({ ok: false, error: cls.friendlyMessage || error.message, classification: cls });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3) Finalize -> updateRepairOrder(finalUpdate: "Y") -> mark exported
|
||||||
|
socket.on("rr-finalize-repair-order", async ({ jobid, jobId } = {}, ack) => {
|
||||||
|
const rid = resolveJobId(jobid || jobId, { jobid, jobId }, null);
|
||||||
|
let bodyshop = null;
|
||||||
|
let job = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!rid) throw new Error("jobid required for finalize");
|
||||||
|
|
||||||
|
const ns = getTransactionType(rid);
|
||||||
|
const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
|
||||||
|
bodyshop = await getBodyshopForSocket({ bodyshopId, socket });
|
||||||
|
|
||||||
|
job = await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.JobData);
|
||||||
|
if (!job) job = await QueryJobData({ redisHelpers }, rid);
|
||||||
|
|
||||||
|
const pending = await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.PendingRO);
|
||||||
|
const advisorNo = await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.AdvisorNo);
|
||||||
|
const selectedCustomerNo = await redisHelpers.getSessionTransactionData(
|
||||||
|
socket.id,
|
||||||
|
ns,
|
||||||
|
RRCacheEnums.SelectedCustomer
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!advisorNo) throw new Error("Advisor missing in session");
|
||||||
|
if (!selectedCustomerNo) throw new Error("Customer number missing in session");
|
||||||
|
|
||||||
|
// Prefer cached outsdRoNo; fall back to our deterministic external number
|
||||||
|
const outsdRoNo = pending?.outsdRoNo ?? job?.ro_number ?? job?.id ?? null;
|
||||||
|
// Prefer DMS RO for update, but finalize() will safely fall back to Outsd if missing
|
||||||
|
const dmsRoNo = pending?.dmsRoNo ?? pending?.roNo ?? null;
|
||||||
|
|
||||||
|
CreateRRLogEvent(socket, "DEBUG", `{6} Finalizing RR RO`, {
|
||||||
|
jobId: rid,
|
||||||
|
outsdRoNo,
|
||||||
|
dmsRoNo,
|
||||||
|
advisorNo,
|
||||||
|
customerNo: selectedCustomerNo
|
||||||
|
});
|
||||||
|
|
||||||
|
const finalizeResult = await finalizeRRRepairOrder({
|
||||||
|
bodyshop,
|
||||||
|
job,
|
||||||
|
advisorNo: String(advisorNo),
|
||||||
|
customerNo: String(selectedCustomerNo),
|
||||||
|
roNo: dmsRoNo, // ✅ RR requires roNo; finalize() will fall back to outsdRoNo if this is absent
|
||||||
|
vin: pending?.vin,
|
||||||
|
socket
|
||||||
|
});
|
||||||
|
|
||||||
|
if (finalizeResult?.success) {
|
||||||
|
CreateRRLogEvent(socket, "INFO", `{7} Finalize success; marking exported`, { dmsRoNo, outsdRoNo });
|
||||||
|
|
||||||
|
// ✅ Mark exported + success log
|
||||||
|
await markRRExportSuccess({
|
||||||
|
socket,
|
||||||
|
jobId: rid,
|
||||||
|
job,
|
||||||
|
bodyshop,
|
||||||
|
result: finalizeResult
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean pending key
|
||||||
|
try {
|
||||||
|
await redisHelpers.setSessionTransactionData(socket.id, ns, RRCacheEnums.PendingRO, null, 1);
|
||||||
|
} catch {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.emit("export-success", { vendor: "rr", jobId: rid, roStatus: finalizeResult?.roStatus });
|
||||||
|
ack?.({ ok: true, result: finalizeResult });
|
||||||
|
} else {
|
||||||
|
const vendorStatusCode = Number(
|
||||||
|
finalizeResult?.roStatus?.statusCode ??
|
||||||
|
finalizeResult?.roStatus?.StatusCode ??
|
||||||
|
finalizeResult?.statusBlocks?.transaction?.statusCode
|
||||||
|
);
|
||||||
|
const cls = classifyRRVendorError({
|
||||||
|
code: vendorStatusCode,
|
||||||
|
message:
|
||||||
|
finalizeResult?.roStatus?.message ??
|
||||||
|
finalizeResult?.roStatus?.Message ??
|
||||||
|
finalizeResult?.error ??
|
||||||
|
"RR finalize failed"
|
||||||
|
});
|
||||||
|
|
||||||
|
await insertRRFailedExportLog({
|
||||||
|
socket,
|
||||||
|
jobId: rid,
|
||||||
|
job,
|
||||||
|
bodyshop,
|
||||||
|
error: new Error(cls.friendlyMessage || finalizeResult?.error || "RR finalize failed"),
|
||||||
|
classification: cls,
|
||||||
|
result: finalizeResult
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.emit("export-failed", {
|
||||||
|
vendor: "rr",
|
||||||
|
jobId: rid,
|
||||||
|
error: cls?.friendlyMessage || finalizeResult?.error || "RR finalize failed",
|
||||||
|
...cls
|
||||||
|
});
|
||||||
|
ack?.({ ok: false, error: cls.friendlyMessage || "RR finalize failed", classification: cls });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const cls = classifyRRVendorError(error);
|
||||||
|
CreateRRLogEvent(socket, "ERROR", `Error during RR finalize`, {
|
||||||
|
error: error.message,
|
||||||
|
vendorStatusCode: cls.vendorStatusCode,
|
||||||
|
code: cls.errorCode,
|
||||||
|
friendly: cls.friendlyMessage,
|
||||||
|
stack: error.stack,
|
||||||
|
jobid: rid
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!bodyshop || !job) {
|
||||||
|
const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
|
||||||
|
bodyshop = bodyshop || (await getBodyshopForSocket({ bodyshopId, socket }));
|
||||||
|
job =
|
||||||
|
job ||
|
||||||
|
(await redisHelpers.getSessionTransactionData(socket.id, getTransactionType(rid), RRCacheEnums.JobData));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
await insertRRFailedExportLog({
|
||||||
|
socket,
|
||||||
|
jobId: rid,
|
||||||
|
job,
|
||||||
|
bodyshop,
|
||||||
|
error,
|
||||||
|
classification: cls
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
socket.emit("export-failed", { vendor: "rr", jobId: rid, error: error.message, ...cls });
|
||||||
|
} catch {
|
||||||
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
ack?.({ ok: false, error: cls.friendlyMessage || error.message, classification: cls });
|
ack?.({ ok: false, error: cls.friendlyMessage || error.message, classification: cls });
|
||||||
|
|||||||
@@ -70,31 +70,6 @@ const normalizeCustomerCandidates = (res, { ownersSet = null } = {}) => {
|
|||||||
const country = chosen?.Country ?? chosen?.CountryCode ?? chosen?.country ?? undefined;
|
const country = chosen?.Country ?? chosen?.CountryCode ?? chosen?.country ?? undefined;
|
||||||
const county = chosen?.County ?? chosen?.county ?? undefined; // << added
|
const county = chosen?.County ?? chosen?.county ?? undefined; // << added
|
||||||
|
|
||||||
// instrumentation (kept minimal; County is now expected)
|
|
||||||
if ((process.env.RR_DEBUG_ADDR ?? "1") !== "0") {
|
|
||||||
const allowed = new Set([
|
|
||||||
"Type",
|
|
||||||
"Addr1",
|
|
||||||
"AddressLine1",
|
|
||||||
"Line1",
|
|
||||||
"Street1",
|
|
||||||
"Addr2",
|
|
||||||
"AddressLine2",
|
|
||||||
"Line2",
|
|
||||||
"Street2",
|
|
||||||
"City",
|
|
||||||
"State",
|
|
||||||
"StateOrProvince",
|
|
||||||
"Zip",
|
|
||||||
"PostalCode",
|
|
||||||
"Country",
|
|
||||||
"CountryCode",
|
|
||||||
"County"
|
|
||||||
]);
|
|
||||||
const unknown = Object.keys(chosen || {}).filter((k) => !allowed.has(k));
|
|
||||||
if (unknown.length) console.log("[RR:normCandidates] Unexpected address keys seen:", unknown);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!line1 && !city && !state && !postalCode && !country && !county) return null;
|
if (!line1 && !city && !state && !postalCode && !country && !county) return null;
|
||||||
return { line1, line2, city, state, postalCode, country, county };
|
return { line1, line2, city, state, postalCode, country, county };
|
||||||
};
|
};
|
||||||
@@ -113,7 +88,7 @@ const normalizeCustomerCandidates = (res, { ownersSet = null } = {}) => {
|
|||||||
|
|
||||||
const address = pickAddr(nci?.Address);
|
const address = pickAddr(nci?.Address);
|
||||||
|
|
||||||
// NEW: fallback to NameRecId when no ServVehicle/CustomerNo exists (e.g., pure name search)
|
// fallback to NameRecId when no ServVehicle/CustomerNo exists (e.g., pure name search)
|
||||||
const nameRecIdRaw = nci?.NameId?.NameRecId;
|
const nameRecIdRaw = nci?.NameId?.NameRecId;
|
||||||
const nameRecId = nameRecIdRaw != null ? String(nameRecIdRaw).trim() : "";
|
const nameRecId = nameRecIdRaw != null ? String(nameRecIdRaw).trim() : "";
|
||||||
|
|
||||||
@@ -136,14 +111,13 @@ const normalizeCustomerCandidates = (res, { ownersSet = null } = {}) => {
|
|||||||
out.push(item);
|
out.push(item);
|
||||||
}
|
}
|
||||||
} else if (nameRecId) {
|
} else if (nameRecId) {
|
||||||
// Use NameRecId as the identifier; this is what the RR "name" search provides
|
// Use NameRecId as the identifier
|
||||||
const cno = nameRecId;
|
const cno = nameRecId;
|
||||||
const item = {
|
const item = {
|
||||||
custNo: cno,
|
custNo: cno,
|
||||||
name: name || `Customer ${cno}`,
|
name: name || `Customer ${cno}`,
|
||||||
address: address || undefined
|
address: address || undefined
|
||||||
};
|
};
|
||||||
// owner flag cannot be inferred without a VIN owner set
|
|
||||||
out.push(item);
|
out.push(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,7 +160,7 @@ const readAdvisorNo = (payload, cached) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache enum keys for RR session transaction data
|
* Cache enum keys for RR session transaction data
|
||||||
* @type {{txEnvelope: string, JobData: string, SelectedCustomer: string, AdvisorNo: string, VINCandidates: string, SelectedVin: string, ExportResult: string}}
|
* @type {{txEnvelope: string, JobData: string, SelectedCustomer: string, AdvisorNo: string, VINCandidates: string, SelectedVin: string, ExportResult: string, PendingRO: string}}
|
||||||
*/
|
*/
|
||||||
const RRCacheEnums = {
|
const RRCacheEnums = {
|
||||||
txEnvelope: "RR.txEnvelope",
|
txEnvelope: "RR.txEnvelope",
|
||||||
@@ -195,7 +169,8 @@ const RRCacheEnums = {
|
|||||||
AdvisorNo: "RR.AdvisorNo",
|
AdvisorNo: "RR.AdvisorNo",
|
||||||
VINCandidates: "RR.VINCandidates",
|
VINCandidates: "RR.VINCandidates",
|
||||||
SelectedVin: "RR.SelectedVin",
|
SelectedVin: "RR.SelectedVin",
|
||||||
ExportResult: "RR.ExportResult"
|
ExportResult: "RR.ExportResult",
|
||||||
|
PendingRO: "RR.PendingRO" // NEW: cache created RO to finalize later
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user