Files
bodyshop/server/rr/rr-service-vehicles.js

320 lines
9.2 KiB
JavaScript

const { buildClientAndOpts, rrCombinedSearch } = require("./rr-lookup");
const CreateRRLogEvent = require("./rr-logger-event");
/**
* Pick and normalize VIN from inputs
* @param vin
* @param job
* @returns {string}
*/
const pickVin = ({ vin, job }) => {
const v = vin || job?.v_vin || job?.vehicle?.vin || job?.vin || job?.vehicleVin || null;
if (!v) return "";
return String(v)
.replace(/[^A-Za-z0-9]/g, "")
.toUpperCase()
.slice(0, 17);
};
/**
* Pick and normalize customer number from inputs
* @param selectedCustomerNo
* @param custNo
* @param customerNo
* @returns {string|string}
*/
const pickCustNo = ({ selectedCustomerNo, custNo, customerNo }) => {
const c = selectedCustomerNo ?? custNo ?? customerNo ?? null;
return c != null ? String(c).trim() : "";
};
/**
* Extract owner customer numbers from combined search results
* @param res
* @param wantedVin
* @returns {Set<any>}
*/
const ownersFromCombined = (res, wantedVin) => {
const blocks = Array.isArray(res?.data) ? res.data : [];
const owners = new Set();
for (const blk of blocks) {
const serv = Array.isArray(blk?.ServVehicle) ? blk.ServVehicle : [];
for (const sv of serv) {
const v = sv?.Vehicle || {};
const vin = (v?.Vin || v?.VIN || v?.vin || "").toString().toUpperCase();
const cust = sv?.VehicleServInfo?.CustomerNo;
if (vin && cust && (!wantedVin || vin === wantedVin)) {
owners.add(String(cust));
}
}
}
return owners;
};
/**
* Determine if error indicates "already exists"
* @param e
* @returns {boolean}
*/
const isAlreadyExistsError = (e) => {
if (!e) return false;
if (e.code === 300) return true;
const msg = (e.message || "").toUpperCase();
return msg.includes("ALREADY EXISTS") || msg.includes("VEHICLE ALREADY EXISTS");
};
function deriveMakeCode(makeDesc) {
if (!makeDesc) return "FR"; // safe default
const map = {
Ford: "FO",
FORD: "FO",
Lincoln: "LN",
Mercury: "MY",
Chevrolet: "CH",
GMC: "GM",
Cadillac: "CA",
Buick: "BU",
Pontiac: "PO",
Oldsmobile: "OL",
Saturn: "SA",
Dodge: "DO",
Chrysler: "CH",
Jeep: "JE",
Plymouth: "PL",
Toyota: "TY",
Lexus: "LX",
Honda: "HO",
Acura: "AC",
Nissan: "NI",
Infiniti: "IN",
Hyundai: "HY",
Kia: "KI",
Subaru: "SU",
Mazda: "MZ",
Volkswagen: "VW",
Audi: "AU",
BMW: "BM",
Mercedes: "MB",
"Mercedes-Benz": "MB",
Volvo: "VO",
Ram: "RA" // post-2010
};
return map[makeDesc.trim().toUpperCase()] || "FR"; // TODO Default set
}
/**
* Ensure/create a Service Vehicle in RR for the given VIN + customer.
* Args may contain:
* - client, routing (prebuilt) OR bodyshop (we'll build a client)
* - vin OR job (we'll derive vin from job)
* - selectedCustomerNo / custNo / customerNo
* - license (optional)
* - socket (for logging)
*
* Returns: { created:boolean, exists:boolean, vin, customerNo, svId?, status? }
*/
const ensureRRServiceVehicle = async (args = {}) => {
const {
client: inClient,
routing: inRouting,
bodyshop,
job,
vin,
selectedCustomerNo,
custNo,
customerNo,
license,
socket
} = args;
// Build/derive essentials
let client = inClient;
let routing = inRouting;
if (!client || !routing) {
if (!bodyshop) {
throw new Error("ensureRRServiceVehicle: either (client & routing) or bodyshop is required");
}
const built = buildClientAndOpts(bodyshop);
client = built.client;
routing = built.opts?.routing;
}
const vinStr = pickVin({ vin, job });
const custNoStr = pickCustNo({ selectedCustomerNo, custNo, customerNo });
if (!vinStr) throw new Error("ensureRRServiceVehicle: vin required");
if (!custNoStr) throw new Error("ensureRRServiceVehicle: customerNo required");
// --- Preflight: does VIN already exist (and under whom)? ---
try {
let owners = new Set();
if (bodyshop) {
const combinedSearchResponse = await rrCombinedSearch(bodyshop, { kind: "vin", vin: vinStr, maxResults: 50 });
CreateRRLogEvent(socket, "silly", "{SV} Preflight combined search by VIN: raw response", {
response: combinedSearchResponse
});
owners = ownersFromCombined(combinedSearchResponse, vinStr);
}
// Short-circuit: VIN exists anywhere -> don't try to insert
if (owners.size > 0) {
const ownedBySame = owners.has(custNoStr);
CreateRRLogEvent(socket, ownedBySame ? "info" : "warn", "{SV} VIN already present in RR; skipping insert", {
vin: vinStr,
selectedCustomerNo: custNoStr,
owners: Array.from(owners)
});
return {
created: false,
exists: true,
vin: vinStr,
customerNo: custNoStr,
ownedBySame
};
}
} catch (e) {
// Preflight shouldn't be fatal; log and continue to insert (idempotency will still be handled)
CreateRRLogEvent(socket, "WARN", "{SV} VIN preflight lookup failed; continuing to insert", {
vin: vinStr,
error: e?.message
});
}
// --- Attempt insert (idempotent) ---
// IMPORTANT: The current RR lib build validates `vehicleServInfo.customerNo`.
// To be future-proof, we also include top-level `customerNo`.
const insertPayload = {
// === Core Vehicle Identity (MANDATORY for success) ===
vin: vinStr.toUpperCase(), // "1FDWX34Y28EB01395"
// Required: 2-character make code (from v_make_desc → known mapping)
vehicleMake: deriveMakeCode(job.v_make_desc), // → "FR" for Ford
// Required: 2-digit year (last 2 digits of v_model_yr)
year: job?.v_model_yr || undefined,
// Required: Model number — fallback strategy per ERA behavior
// Most Ford trucks use "T" = Truck. Some systems accept actual code.
// CAN BE (P)assenger , (T)ruck, (O)ther
mdlNo: undefined,
// === Descriptive Fields (highly recommended) ===
modelDesc: job?.v_model_desc?.trim() || undefined, // "F-350 SD"
carline: job?.v_model_desc?.trim() || undefined, // Series line
extClrDesc: job?.v_color?.trim() || undefined, // "Red"
// Optional but helpful
accentClr: undefined,
// === VehicleDetail Flags (CRITICAL — cause silent fails or error 303 if missing) ===
aircond: undefined, // "Y", // Nearly all modern vehicles have A/C
pwrstr: undefined, // "Y", // Power steering = yes on 99% of vehicles post-1990
transm: undefined, // "A", // Default to Automatic — change to "M" only if known manual
turbo: undefined, //"N", // 2008 F-350 6.4L Power Stroke has turbo, but field is optional
engineConfig: undefined, //"V8", // or "6.4L Diesel" — optional but nice
trim: undefined, //"XLT", // You don't have this — safe to omit or guess
// License plate
licNo: license ? String(license).trim() : undefined,
// === VehicleServInfo (attributes on the element) ===
customerNo: custNoStr, // fallback (some builds read this)
stockId: job.ro_number || undefined, // Use RO as stock# — common pattern
vehicleServInfo: {
customerNo: custNoStr, // REQUIRED — this is what toServiceVehicleView() validates
// Optional but increases success rate
salesmanNo: undefined, // You don't have advisor yet — omit
inServiceDate: undefined,
// Optional — safe to include if you want
productionDate: undefined,
modelMaintCode: undefined,
teamCode: undefined,
// Extended warranty — omit unless you sell contracts
vehExtWarranty: undefined,
// Advisor — omit unless you know who the service advisor is
advisor: undefined
}
};
const insertOpts = {
routing,
envelope: {
sender: {
component: "Rome",
task: "SV",
referenceId: "Insert",
creator: "RCI",
senderName: "RCI"
}
}
};
CreateRRLogEvent(socket, "info", "{SV} Inserting service vehicle", {
vin: vinStr,
selectedCustomerNo: custNoStr,
payloadShape: Object.keys(insertPayload).filter((k) => insertPayload[k] != null)
});
try {
const res = await client.insertServiceVehicle(insertPayload, insertOpts);
CreateRRLogEvent(socket, "silly", "{SV} insertServiceVehicle: raw response", { res });
const data = res?.data ?? {};
const svId = data?.dmsRecKey || data?.svId || undefined;
CreateRRLogEvent(socket, "info", "{SV} insertServiceVehicle: success", {
vin: vinStr,
customerNo: custNoStr,
svId
});
return {
created: true,
exists: false,
vin: vinStr,
customerNo: custNoStr,
svId,
status: res?.statusBlocks?.transaction
};
} catch (e) {
if (isAlreadyExistsError(e)) {
// Treat as idempotent success
CreateRRLogEvent(socket, "warn", "{SV} insertServiceVehicle: already exists; treating as success", {
vin: vinStr,
customerNo: custNoStr,
code: e?.code,
status: e?.meta?.status || e?.status
});
return {
created: false,
exists: true,
vin: vinStr,
customerNo: custNoStr,
status: e?.meta?.status || e?.status
};
}
CreateRRLogEvent(socket, "error", "{SV} insertServiceVehicle: failure", {
message: e?.message,
code: e?.code,
status: e?.meta?.status || e?.status
});
throw e;
}
};
module.exports = {
ensureRRServiceVehicle
};