feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Checkpoint

This commit is contained in:
Dave
2025-11-04 15:18:08 -05:00
parent aa692d4d05
commit c60dfa4319
2 changed files with 196 additions and 138 deletions

View File

@@ -46,7 +46,7 @@ async function ensureRRServiceVehicle({ bodyshop, custNo, job, overrides = {}, s
// Optional: first try a combined query by VIN to detect existing SV // Optional: first try a combined query by VIN to detect existing SV
try { try {
const queryRes = await client.combinedSearch( const queryRes = await client.combinedSearch(
{ vin: job?.v_vin, maxRecs: 1 }, { vin: job?.v_vin, maxRecs: 1, kind: "vin" },
{ {
...opts, ...opts,
envelope: { envelope: {

View File

@@ -5,17 +5,29 @@ const { exportJobToRR } = require("../rr/rr-job-export");
const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default; const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
const { createRRCustomer } = require("../rr/rr-customers"); const { createRRCustomer } = require("../rr/rr-customers");
const { buildClientAndOpts } = require("../rr/rr-lookup");
const { GraphQLClient } = require("graphql-request"); const { GraphQLClient } = require("graphql-request");
const queries = require("../graphql-client/queries"); const queries = require("../graphql-client/queries");
// ---------------- utils ---------------- // Use the same namespacing/TTL approach as Fortellis
const { getTransactionType, defaultFortellisTTL } = require("../fortellis/fortellis-helpers");
// ---------------- cache keys (RR) ----------------
const RRCacheEnums = {
txEnvelope: "RR.txEnvelope",
JobData: "RR.JobData",
SelectedCustomer: "RR.SelectedCustomer",
AdvisorNo: "RR.AdvisorNo",
// Vehicle keys reserved for future multi-vehicle UX parity
VINCandidates: "RR.VINCandidates",
SelectedVin: "RR.SelectedVin",
ExportResult: "RR.ExportResult"
};
const defaultRRTTL = defaultFortellisTTL || 60 * 60; // fallback 1h
// ---------------- utils ----------------
function resolveJobId(explicit, payload, job) { function resolveJobId(explicit, payload, job) {
return explicit || payload?.jobId || payload?.jobid || job?.id || job?.jobId || job?.jobid || null; return explicit || payload?.jobId || payload?.jobid || job?.id || job?.jobId || job?.jobid || null;
} }
// ---- local helpers (avoid no-undef) ----
const digitsOnly = (s) => String(s || "").replace(/\D/g, ""); const digitsOnly = (s) => String(s || "").replace(/\D/g, "");
const makeVehicleSearchPayloadFromJob = (job) => { const makeVehicleSearchPayloadFromJob = (job) => {
@@ -42,8 +54,8 @@ const makeCustomerSearchPayloadFromJob = (job) => {
return null; return null;
}; };
// Normalize to the RR table shape expected by FE (custNo + name)
const normalizeCustomerCandidates = (res) => { const normalizeCustomerCandidates = (res) => {
// CombinedSearch may return array or { data: [...] }
const blocks = Array.isArray(res?.data) ? res.data : Array.isArray(res) ? res : []; const blocks = Array.isArray(res?.data) ? res.data : Array.isArray(res) ? res : [];
const out = []; const out = [];
for (const blk of blocks) { for (const blk of blocks) {
@@ -58,7 +70,7 @@ const normalizeCustomerCandidates = (res) => {
const name = (personal || company || "").trim(); const name = (personal || company || "").trim();
for (const custNo of custNos) { for (const custNo of custNos) {
out.push({ custNo, name: name || `Customer ${custNo}` }); out.push({ custNo: String(custNo), name: name || `Customer ${custNo}` });
} }
} }
const seen = new Set(); const seen = new Set();
@@ -99,32 +111,41 @@ async function getBodyshopForSocket({ bodyshopId, socket }) {
return bodyshop; return bodyshop;
} }
// ---------------- register handlers ---------------- function readAdvisorNo(payload, cached) {
const v =
(payload?.txEnvelope?.advisorNo != null && String(payload.txEnvelope.advisorNo)) ||
(payload?.advisorNo != null && String(payload.advisorNo)) ||
(payload?.advNo != null && String(payload.advNo)) ||
(cached != null && String(cached)) ||
null;
return v && v.trim() !== "" ? v : null;
}
function registerRREvents({ socket, redisHelpers }) { // ---------------- register handlers ----------------
// RRLogger returns a log(level, message, ctx) function function registerRREvents({ socket, redisHelpers /*, ioHelpers, logger*/ }) {
const log = RRLogger(socket); const log = RRLogger(socket);
// Lookups (mirrors Fortellis shape/flow) // --------- Lookups (customer search → open table) ---------
socket.on("rr-lookup-combined", async ({ jobid, params } = {}, cb) => { socket.on("rr-lookup-combined", async ({ jobid, params } = {}, cb) => {
try { try {
const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket); const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
const bodyshop = await getBodyshopForSocket({ bodyshopId, socket }); const bodyshop = await getBodyshopForSocket({ bodyshopId, socket });
const res = await rrCombinedSearch(bodyshop, params || {}); const res = await rrCombinedSearch(bodyshop, params || {});
const data = res?.data ?? res; const normalized = normalizeCustomerCandidates(res);
cb?.({ jobid: resolveJobId(jobid, { jobid }, null), data }); const rid = resolveJobId(jobid, { jobid }, null);
cb?.({ jobid: rid, data: normalized });
// Push to FE to open the table; keep payload as the raw array (FE maps columns itself) // FE expects { custNo, name }
socket.emit("rr-select-customer", Array.isArray(data) ? data : data?.customers || []); socket.emit("rr-select-customer", normalized);
} catch (e) { } catch (e) {
log("error", `RR combined lookup error: ${e.message}`, { jobid }); log("error", `RR combined lookup error: ${e.message}`, { jobid });
cb?.({ jobid, error: e.message }); cb?.({ jobid, error: e.message });
} }
}); });
// Advisors // --------- Advisors ---------
socket.on("rr-get-advisors", async (args = {}, ack) => { socket.on("rr-get-advisors", async (args = {}, ack) => {
try { try {
const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket); const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
@@ -138,7 +159,7 @@ function registerRREvents({ socket, redisHelpers }) {
} }
}); });
// Parts // --------- Parts ---------
socket.on("rr-get-parts", async (args = {}, ack) => { socket.on("rr-get-parts", async (args = {}, ack) => {
try { try {
const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket); const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
@@ -152,31 +173,54 @@ function registerRREvents({ socket, redisHelpers }) {
} }
}); });
// Persist customer selection (or flag create-new) // --------- Persist customer selection / create-intent (table-first UX) ---------
socket.on("rr-selected-customer", async (selected, ack) => { socket.on("rr-selected-customer", async ({ jobId, custNo, create } = {}, ack) => {
const ns = getTransactionType(jobId || "unknown");
try { try {
await getSessionOrSocket(redisHelpers, socket); const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
const tx = (await redisHelpers.getSessionTransactionData(socket.id)) || {}; const bodyshop = await getBodyshopForSocket({ bodyshopId, socket });
if (!jobId) throw new Error("jobId required");
// Signal create-new intent // If caller passed a selection, just persist it.
if (!selected || selected?.create === true || selected?.__new === true) { if (custNo && create !== true) {
await redisHelpers.setSessionTransactionData(socket.id, { ...tx, rrCreateCustomer: true }); await redisHelpers.setSessionTransactionData(
log("info", "rr-selected-customer:new-customer-intent"); socket.id,
socket.emit("rr-customer-create-required"); ns,
return ack?.({ ok: true, action: "create" }); RRCacheEnums.SelectedCustomer,
String(custNo),
defaultRRTTL
);
log("info", "rr-selected-customer", { jobId, custNo: String(custNo) });
return ack?.({ ok: true });
} }
await redisHelpers.setSessionTransactionData(socket.id, { ...tx, rrSelectedCustomer: selected }); // No custNo (or create: true) => create immediately from JobData (Fortellis parity)
log("info", "rr-selected-customer", { selected }); const job = await QueryJobData({ redisHelpers }, jobId);
ack?.({ ok: true }); const created = await createRRCustomer({ bodyshop, job, socket });
const newCustNo = String(
created?.custNo || created?.customerNo || created?.CustomerNo || created?.dmsRecKey || ""
);
if (!newCustNo) throw new Error("RR create customer returned no custNo");
await redisHelpers.setSessionTransactionData(
socket.id,
ns,
RRCacheEnums.SelectedCustomer,
newCustNo,
defaultRRTTL
);
log("info", "rr-create-customer:success", { jobId, custNo: newCustNo });
return ack?.({ ok: true, custNo: newCustNo });
} catch (err) { } catch (err) {
log("error", err?.message || "select customer failed", { err }); log("error", err?.message || "select/create customer failed", { err, jobId });
ack?.({ ok: false, error: err?.message || "select customer failed" }); return ack?.({ ok: false, error: err?.message || "select/create customer failed" });
} }
}); });
// Optional explicit create-customer from UI form // --------- Optional explicit create-customer (form) ---------
socket.on("rr-create-customer", async ({ jobId, fields } = {}, ack) => { socket.on("rr-create-customer", async ({ jobId, fields } = {}, ack) => {
const ns = getTransactionType(jobId || "unknown");
try { try {
const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket); const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
const bodyshop = await getBodyshopForSocket({ bodyshopId, socket }); const bodyshop = await getBodyshopForSocket({ bodyshopId, socket });
@@ -184,67 +228,82 @@ function registerRREvents({ socket, redisHelpers }) {
if (!jobId) throw new Error("jobId required"); if (!jobId) throw new Error("jobId required");
const job = await QueryJobData({ redisHelpers }, jobId); const job = await QueryJobData({ redisHelpers }, jobId);
const { custNo } = await createRRCustomer({ bodyshop, job, overrides: fields || {}, socket }); const created = await createRRCustomer({ bodyshop, job, overrides: fields || {}, socket });
const custNo = String(created?.custNo || created?.customerNo || created?.CustomerNo || created?.dmsRecKey || "");
if (!custNo) throw new Error("RR create customer returned no custNo");
const tx = (await redisHelpers.getSessionTransactionData(socket.id)) || {}; await redisHelpers.setSessionTransactionData(socket.id, ns, RRCacheEnums.SelectedCustomer, custNo, defaultRRTTL);
await redisHelpers.setSessionTransactionData(socket.id, { await redisHelpers.setSessionTransactionData(socket.id, ns, "RR.CreateCustomerIntent", false, defaultRRTTL);
...tx,
rrSelectedCustomer: { custNo },
rrCreateCustomer: false
});
log("info", "rr-create-customer:success", { custNo }); log("info", "rr-create-customer:success", { jobId, custNo });
socket.emit("rr-customer-created", { custNo }); socket.emit("rr-customer-created", { custNo });
ack?.({ ok: true, custNo }); ack?.({ ok: true, custNo });
} catch (err) { } catch (err) {
log("error", err?.message || "create customer failed", { err }); log("error", err?.message || "create customer failed", { err, jobId });
ack?.({ ok: false, error: err?.message || "create customer failed" }); ack?.({ ok: false, error: err?.message || "create customer failed" });
} }
}); });
// Vehicle selection helpers // --------- Vehicle selection hooks (reserved for future parity) ---------
socket.on("rr-selected-vehicle", async (selected, ack) => { socket.on("rr-selected-vehicle", async ({ jobId, vin } = {}, ack) => {
const ns = getTransactionType(jobId || "unknown");
try { try {
await getSessionOrSocket(redisHelpers, socket); await getSessionOrSocket(redisHelpers, socket);
if (!selected?.vin) throw new Error("selected vehicle must include vin"); if (!vin) throw new Error("vin required");
const tx = (await redisHelpers.getSessionTransactionData(socket.id)) || {}; await redisHelpers.setSessionTransactionData(socket.id, ns, RRCacheEnums.SelectedVin, String(vin), defaultRRTTL);
await redisHelpers.setSessionTransactionData(socket.id, { ...tx, rrSelectedVehicle: selected }); log("info", "rr-selected-vehicle", { jobId, vin: String(vin) });
log("info", "rr-selected-vehicle", { vin: selected.vin });
ack?.({ ok: true }); ack?.({ ok: true });
} catch (err) { } catch (err) {
log("error", err?.message || "select vehicle failed", { err }); log("error", err?.message || "select vehicle failed", { err, jobId });
ack?.({ ok: false, error: err?.message || "select vehicle failed" }); ack?.({ ok: false, error: err?.message || "select vehicle failed" });
} }
}); });
socket.on("rr-create-vehicle", async (vehicle, ack) => { socket.on("rr-create-vehicle", async ({ jobId, vehicle } = {}, ack) => {
const ns = getTransactionType(jobId || "unknown");
try { try {
await getSessionOrSocket(redisHelpers, socket); await getSessionOrSocket(redisHelpers, socket);
if (!vehicle?.vin) throw new Error("vehicle.vin required"); if (!vehicle?.vin) throw new Error("vehicle.vin required");
const tx = (await redisHelpers.getSessionTransactionData(socket.id)) || {}; await redisHelpers.setSessionTransactionData(
await redisHelpers.setSessionTransactionData(socket.id, { ...tx, rrSelectedVehicle: vehicle }); socket.id,
log("info", "rr-create-vehicle", { vin: vehicle.vin }); ns,
RRCacheEnums.SelectedVin,
String(vehicle.vin),
defaultRRTTL
);
log("info", "rr-create-vehicle", { jobId, vin: String(vehicle.vin) });
ack?.({ ok: true }); ack?.({ ok: true });
} catch (err) { } catch (err) {
log("error", err?.message || "create vehicle failed", { err }); log("error", err?.message || "create vehicle failed", { err, jobId });
ack?.({ ok: false, error: err?.message || "create vehicle failed" }); ack?.({ ok: false, error: err?.message || "create vehicle failed" });
} }
}); });
// --------- Main export (Fortellis-style staging) ---------
socket.on("rr-export-job", async (payload = {}) => { socket.on("rr-export-job", async (payload = {}) => {
const log = RRLogger(socket, { ns: "rr" }); const _log = RRLogger(socket, { ns: "rr" });
try { try {
// -------- 1) Resolve job -------- // 1) Resolve job
let job = payload.job || payload.txEnvelope?.job; let job = payload.job || payload.txEnvelope?.job;
const jobId = payload.jobId || payload.jobid || payload.txEnvelope?.jobId || job?.id; const jobId = payload.jobId || payload.jobid || payload.txEnvelope?.jobId || job?.id;
if (!job) { if (!job) {
if (!jobId) throw new Error("RR export: job or jobId required"); if (!jobId) throw new Error("RR export: job or jobId required");
job = await QueryJobData({ redisHelpers }, jobId); job = await QueryJobData({ redisHelpers }, jobId);
} }
const ns = getTransactionType(job.id);
// -------- 2) Resolve bodyshop (+ full row via GraphQL) -------- // Persist txEnvelope + job
await redisHelpers.setSessionTransactionData(
socket.id,
ns,
RRCacheEnums.txEnvelope,
payload.txEnvelope || {},
defaultRRTTL
);
await redisHelpers.setSessionTransactionData(socket.id, ns, RRCacheEnums.JobData, job, defaultRRTTL);
// 2) Resolve bodyshop
let bodyshopId = payload.bodyshopId || payload.bodyshopid || payload.bodyshopUUID || job?.bodyshop?.id; let bodyshopId = payload.bodyshopId || payload.bodyshopid || payload.bodyshopUUID || job?.bodyshop?.id;
if (!bodyshopId) { if (!bodyshopId) {
const sess = await getSessionOrSocket(redisHelpers, socket); const sess = await getSessionOrSocket(redisHelpers, socket);
@@ -257,109 +316,116 @@ function registerRREvents({ socket, redisHelpers }) {
? job.bodyshop ? job.bodyshop
: await getBodyshopForSocket({ bodyshopId, socket }); : await getBodyshopForSocket({ bodyshopId, socket });
// -------- 3) Resolve advisor number from the posting form -------- // 3) Resolve advisor number (from form or cache)
const tx = (await redisHelpers.getSessionTransactionData(socket.id)) || {}; const cachedAdvisor = await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.AdvisorNo);
const advisorNo = payload.advisorNo || payload.advNo || payload.txEnvelope?.advisorNo || tx.rrAdvisorNo || null; const advisorNo = readAdvisorNo(payload, cachedAdvisor);
if (!advisorNo) {
if (!advisorNo || String(advisorNo).trim() === "") {
socket.emit("export-failed", { vendor: "rr", jobId, error: "Advisor is required (advisorNo)." }); socket.emit("export-failed", { vendor: "rr", jobId, error: "Advisor is required (advisorNo)." });
return; return;
} }
// Persist for subsequent steps if useful await redisHelpers.setSessionTransactionData(
await redisHelpers.setSessionTransactionData(socket.id, { ...tx, rrAdvisorNo: String(advisorNo) }); socket.id,
ns,
RRCacheEnums.AdvisorNo,
String(advisorNo),
defaultRRTTL
);
// -------- 4) Resolve selected customer (payload → tx) -------- // 4) Resolve selected customer (payload → cache)
let selectedCustomer = null; let selectedCust = null;
// from payload
if (payload.selectedCustomer) { if (payload.selectedCustomer) {
if (typeof payload.selectedCustomer === "object" && payload.selectedCustomer.custNo) { if (typeof payload.selectedCustomer === "object" && payload.selectedCustomer.custNo) {
selectedCustomer = { custNo: String(payload.selectedCustomer.custNo) }; selectedCust = { custNo: String(payload.selectedCustomer.custNo) };
} else if (typeof payload.selectedCustomer === "string" || typeof payload.selectedCustomer === "number") { } else if (typeof payload.selectedCustomer === "string" || typeof payload.selectedCustomer === "number") {
selectedCustomer = { custNo: String(payload.selectedCustomer) }; selectedCust = { custNo: String(payload.selectedCustomer) };
}
if (selectedCust?.custNo) {
await redisHelpers.setSessionTransactionData(
socket.id,
ns,
RRCacheEnums.SelectedCustomer,
selectedCust.custNo,
defaultRRTTL
);
} }
} }
// from tx if (!selectedCust) {
if (!selectedCustomer && tx.rrSelectedCustomer) { const cached = await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.SelectedCustomer);
if (typeof tx.rrSelectedCustomer === "object" && tx.rrSelectedCustomer.custNo) { if (cached) selectedCust = { custNo: String(cached) };
selectedCustomer = { custNo: String(tx.rrSelectedCustomer.custNo) };
} else {
selectedCustomer = { custNo: String(tx.rrSelectedCustomer) };
}
} }
// Flags // Flags
const forceCreate = payload.forceCreate === true || tx.rrCreateCustomer === true; const forceCreate = payload.forceCreate === true;
const autoCreateOnNoMatch = payload.autoCreateOnNoMatch !== false; // default TRUE const autoCreateOnNoMatch = payload.autoCreateOnNoMatch !== false; // default TRUE
// -------- 5) If no selection & not "forceCreate", try auto-search first -------- // 5) If no selection & not "forceCreate", try auto-search
if (!selectedCustomer && !forceCreate) { if (!selectedCust && !forceCreate) {
const customerQuery = makeCustomerSearchPayloadFromJob(job); const customerQuery = makeCustomerSearchPayloadFromJob(job);
const vehicleQuery = makeVehicleSearchPayloadFromJob(job); const vehicleQuery = makeVehicleSearchPayloadFromJob(job);
const query = customerQuery || vehicleQuery; const query = customerQuery || vehicleQuery;
if (query) { if (query) {
log("info", "rr-export-job:customer-preselect-search", { query, jobId }); _log("info", "rr-export-job:customer-preselect-search", { query, jobId });
const searchRes = await rrCombinedSearch(bodyshop, query); const searchRes = await rrCombinedSearch(bodyshop, query);
const candidates = normalizeCustomerCandidates(searchRes); const candidates = normalizeCustomerCandidates(searchRes);
if (candidates.length === 1) { if (candidates.length === 1) {
// auto-pick single hit selectedCust = { custNo: String(candidates[0].custNo) };
selectedCustomer = { custNo: String(candidates[0].custNo) }; await redisHelpers.setSessionTransactionData(
await redisHelpers.setSessionTransactionData(socket.id, { socket.id,
...tx, ns,
rrSelectedCustomer: selectedCustomer.custNo, RRCacheEnums.SelectedCustomer,
rrCreateCustomer: false selectedCust.custNo,
}); defaultRRTTL
log("info", "rr-export-job:auto-selected-customer", { jobId, custNo: selectedCustomer.custNo }); );
_log("info", "rr-export-job:auto-selected-customer", { jobId, custNo: selectedCust.custNo });
} else if (candidates.length > 1) { } else if (candidates.length > 1) {
// multiple matches → ask UI to pick and STOP here // multiple matches → table and stop
const table = candidates.map((c) => ({ socket.emit("rr-select-customer", candidates); // FE expects [{custNo,name}]
CustomerId: c.custNo, socket.emit("rr-log-event", {
customerId: c.custNo, level: "info",
CustomerName: { FirstName: c.name || String(c.custNo), LastName: "" } message: "RR: customer selection required",
})); ts: Date.now()
socket.emit("rr-select-customer", table); });
socket.emit("rr-log-event", { level: "info", message: "RR: customer selection required", ts: Date.now() });
return; return;
} else { } else {
// 0 matches → auto-create by default
if (autoCreateOnNoMatch) { if (autoCreateOnNoMatch) {
const { createRRCustomer } = require("../rr/rr-customers");
const created = await createRRCustomer({ bodyshop, job, socket }); const created = await createRRCustomer({ bodyshop, job, socket });
const custNo = created?.custNo || created?.customerNo || created?.CustomerNo || created?.dmsRecKey; const custNo = created?.custNo || created?.customerNo || created?.CustomerNo || created?.dmsRecKey;
if (!custNo) throw new Error("RR create customer returned no custNo"); if (!custNo) throw new Error("RR create customer returned no custNo");
selectedCustomer = { custNo: String(custNo) }; selectedCust = { custNo: String(custNo) };
await redisHelpers.setSessionTransactionData(socket.id, { await redisHelpers.setSessionTransactionData(
...tx, socket.id,
rrSelectedCustomer: selectedCustomer.custNo, ns,
rrCreateCustomer: false RRCacheEnums.SelectedCustomer,
}); selectedCust.custNo,
log("info", "rr-export-job:auto-created-customer", { jobId, custNo: selectedCustomer.custNo }); defaultRRTTL
);
_log("info", "rr-export-job:auto-created-customer", { jobId, custNo: selectedCust.custNo });
} else { } else {
await redisHelpers.setSessionTransactionData(socket.id, { ...tx, rrCreateCustomer: true });
socket.emit("rr-customer-create-required"); socket.emit("rr-customer-create-required");
socket.emit("rr-log-event", { level: "info", message: "RR: create customer required", ts: Date.now() }); socket.emit("rr-log-event", { level: "info", message: "RR: create customer required", ts: Date.now() });
return; return;
} }
} }
} else { } else {
// no usable query → fall back to create or UI prompt // no usable query → create or prompt
if (autoCreateOnNoMatch) { if (autoCreateOnNoMatch) {
const { createRRCustomer } = require("../rr/rr-customers");
const created = await createRRCustomer({ bodyshop, job, socket }); const created = await createRRCustomer({ bodyshop, job, socket });
const custNo = created?.custNo || created?.customerNo || created?.CustomerNo || created?.dmsRecKey; const custNo = created?.custNo || created?.customerNo || created?.CustomerNo || created?.dmsRecKey;
if (!custNo) throw new Error("RR create customer returned no custNo"); if (!custNo) throw new Error("RR create customer returned no custNo");
selectedCustomer = { custNo: String(custNo) }; selectedCust = { custNo: String(custNo) };
await redisHelpers.setSessionTransactionData(socket.id, { await redisHelpers.setSessionTransactionData(
...tx, socket.id,
rrSelectedCustomer: selectedCustomer.custNo, ns,
rrCreateCustomer: false RRCacheEnums.SelectedCustomer,
}); selectedCust.custNo,
log("info", "rr-export-job:auto-created-customer(no-query)", { jobId, custNo: selectedCustomer.custNo }); defaultRRTTL
);
_log("info", "rr-export-job:auto-created-customer(no-query)", { jobId, custNo: selectedCust.custNo });
} else { } else {
await redisHelpers.setSessionTransactionData(socket.id, { ...tx, rrCreateCustomer: true });
socket.emit("rr-customer-create-required"); socket.emit("rr-customer-create-required");
socket.emit("rr-log-event", { level: "info", message: "RR: create customer required", ts: Date.now() }); socket.emit("rr-log-event", { level: "info", message: "RR: create customer required", ts: Date.now() });
return; return;
@@ -367,29 +433,14 @@ function registerRREvents({ socket, redisHelpers }) {
} }
} }
// -------- 6) If still not selected & creation is allowed, create now -------- if (!selectedCust?.custNo) throw new Error("RR export: selected customer missing custNo");
if (!selectedCustomer && (forceCreate || autoCreateOnNoMatch)) {
const { createRRCustomer } = require("../rr/rr-customers");
const created = await createRRCustomer({ bodyshop, job, socket });
const custNo = created?.custNo || created?.customerNo || created?.CustomerNo || created?.dmsRecKey;
if (!custNo) throw new Error("RR create customer returned no custNo");
selectedCustomer = { custNo: String(custNo) };
await redisHelpers.setSessionTransactionData(socket.id, {
...tx,
rrSelectedCustomer: selectedCustomer.custNo,
rrCreateCustomer: false
});
log("info", "rr-export-job:customer-created", { jobId, custNo: selectedCustomer.custNo });
}
if (!selectedCustomer?.custNo) throw new Error("RR export: selected customer missing custNo"); // 6) Perform export (ensure SV + create/update RO inside exportJobToRR)
// -------- 7) Perform export (ensure SV + create/update RO) --------
const result = await exportJobToRR({ const result = await exportJobToRR({
bodyshop, bodyshop,
job, job,
selectedCustomer, selectedCustomer: selectedCust,
advisorNo, advisorNo: String(advisorNo),
existing: payload.existing, existing: payload.existing,
socket socket
}); });
@@ -405,10 +456,17 @@ function registerRREvents({ socket, redisHelpers }) {
}); });
} }
await redisHelpers.setSessionTransactionData(
socket.id,
ns,
RRCacheEnums.ExportResult,
result || {},
defaultRRTTL
);
socket.emit("rr-export-job:result", { jobId, bodyshopId, result }); socket.emit("rr-export-job:result", { jobId, bodyshopId, result });
} catch (error) { } catch (error) {
const jobId = payload.jobId || payload.jobid || payload.txEnvelope?.jobId || payload?.job?.id; const jobId = payload.jobId || payload.jobid || payload.txEnvelope?.jobId || payload?.job?.id;
log("error", `Error during RR export: ${error.message}`, { jobId, stack: error.stack }); _log("error", `Error during RR export: ${error.message}`, { jobId, stack: error.stack });
try { try {
socket.emit("export-failed", { vendor: "rr", jobId, error: error.message }); socket.emit("export-failed", { vendor: "rr", jobId, error: error.message });
} catch { } catch {
@@ -417,7 +475,7 @@ function registerRREvents({ socket, redisHelpers }) {
} }
}); });
// Allocations (RR reuses CDK calculator) // --------- Allocations (reuse CDK calculator) ---------
socket.on("rr-calculate-allocations", async (jobid, cb) => { socket.on("rr-calculate-allocations", async (jobid, cb) => {
try { try {
const allocations = await CdkCalculateAllocations(socket, jobid); const allocations = await CdkCalculateAllocations(socket, jobid);