254 lines
9.0 KiB
JavaScript
254 lines
9.0 KiB
JavaScript
// server/rr/rr-service-vehicles.js
|
||
const { RRClient } = require("./lib/index.cjs");
|
||
const { getRRConfigFromBodyshop } = require("./rr-config");
|
||
const RRLogger = require("./rr-logger");
|
||
const { ownersFromVinBlocks, getTransactionType, defaultRRTTL, RRCacheEnums } = require("../rr/rr-utils");
|
||
|
||
function buildClientAndOpts(bodyshop) {
|
||
const cfg = getRRConfigFromBodyshop(bodyshop);
|
||
const client = new RRClient({
|
||
baseUrl: cfg.baseUrl,
|
||
username: cfg.username,
|
||
password: cfg.password,
|
||
timeoutMs: cfg.timeoutMs,
|
||
retries: cfg.retries
|
||
});
|
||
const opts = {
|
||
routing: cfg.routing,
|
||
envelope: {
|
||
sender: {
|
||
component: "Rome",
|
||
task: "SV", // default for insertServiceVehicle
|
||
referenceId: "Insert",
|
||
creator: "RCI",
|
||
senderName: "RCI"
|
||
}
|
||
}
|
||
};
|
||
return { client, opts };
|
||
}
|
||
|
||
function buildServiceVehiclePayload({ job, custNo, overrides = {} }) {
|
||
// Helpers
|
||
const isBlank = (v) => v == null || (typeof v === "string" && v.trim() === "");
|
||
const toStr = (v) => (v == null ? undefined : String(v));
|
||
const toNum = (v) => {
|
||
const n = Number(v);
|
||
return Number.isFinite(n) ? n : undefined;
|
||
};
|
||
const dropEmptyDeep = (obj) => {
|
||
if (Array.isArray(obj)) {
|
||
const arr = obj
|
||
.map(dropEmptyDeep)
|
||
.filter(
|
||
(v) =>
|
||
!(
|
||
v == null ||
|
||
(typeof v === "object" && !Array.isArray(v) && Object.keys(v).length === 0) ||
|
||
(typeof v === "string" && v.trim() === "")
|
||
)
|
||
);
|
||
return arr.length ? arr : undefined;
|
||
}
|
||
if (obj && typeof obj === "object") {
|
||
const out = {};
|
||
for (const [k, v] of Object.entries(obj)) {
|
||
const vv = dropEmptyDeep(v);
|
||
if (!(vv == null || (typeof vv === "string" && vv.trim() === ""))) out[k] = vv;
|
||
}
|
||
return Object.keys(out).length ? out : undefined;
|
||
}
|
||
return obj;
|
||
};
|
||
|
||
// Derive odometer + units from job
|
||
let odometer = undefined;
|
||
let odometerUnits = undefined;
|
||
|
||
if (!isBlank(job?.kmout)) {
|
||
odometer = toNum(job.kmout);
|
||
odometerUnits = "KM";
|
||
} else if (!isBlank(job?.kmin)) {
|
||
odometer = toNum(job.kmin);
|
||
odometerUnits = "KM";
|
||
} else if (!isBlank(job?.odometer)) {
|
||
odometer = toNum(job.odometer);
|
||
// if you know this is miles in your dataset, set "MI"; otherwise omit units
|
||
}
|
||
|
||
// Map common vehicle descriptors
|
||
const year = overrides.year ?? job?.v_model_yr;
|
||
const modelDesc = overrides.modelDesc ?? job?.v_model_desc;
|
||
const makeDesc = overrides.makeDesc ?? job?.v_make_desc; // not in the spec list, used only to help build `carline`
|
||
const extClrDesc = overrides.extClrDesc ?? job?.v_color;
|
||
const intClrDesc = overrides.intClrDesc ?? job?.v_int_color;
|
||
const trimDesc = overrides.trimDesc ?? job?.v_trim;
|
||
const bodyStyle = overrides.bodyStyle ?? job?.v_body_style;
|
||
const engineDesc = overrides.engineDesc ?? job?.v_engine_desc ?? job?.engine_desc;
|
||
const transDesc = overrides.transDesc ?? job?.v_trans_desc ?? job?.trans_desc;
|
||
|
||
// Carline: prefer explicit field; otherwise derive from make+model when both are present
|
||
const carline =
|
||
overrides.carline ??
|
||
job?.v_carline ??
|
||
(makeDesc && modelDesc ? `${String(makeDesc).trim()} ${String(modelDesc).trim()}` : undefined);
|
||
|
||
// Advisor (NameRecId) if you have it handy (often provided via txEnvelope or cache) – pass in overrides.advisorNameRecId
|
||
const advisorNameRecId = overrides.advisorNameRecId;
|
||
|
||
// Optional warranty fields (pass through via overrides.*)
|
||
const vehExtWarranty = (() => {
|
||
const contractNumber = overrides?.vehExtWarranty?.contractNumber ?? job?.warranty_contract_no;
|
||
const expirationDate = overrides?.vehExtWarranty?.expirationDate ?? job?.warranty_exp_date;
|
||
const expirationMileage = overrides?.vehExtWarranty?.expirationMileage ?? job?.warranty_exp_mileage;
|
||
const candidate = {
|
||
contractNumber: isBlank(contractNumber) ? undefined : String(contractNumber),
|
||
expirationDate: isBlank(expirationDate) ? undefined : String(expirationDate),
|
||
expirationMileage: isBlank(expirationMileage) ? undefined : String(expirationMileage)
|
||
};
|
||
const any = Object.values(candidate).some((v) => !isBlank(v));
|
||
return any ? candidate : undefined;
|
||
})();
|
||
|
||
// Sales/team/in-service/mileage optional fields
|
||
const salesmanNo = overrides.salesmanNo ?? job?.salesman_no;
|
||
const inServiceDate = overrides.inServiceDate ?? job?.in_service_date;
|
||
const svMileage = overrides.mileage ?? odometer; // mirror root odometer if you want
|
||
const teamCode = overrides.teamCode ?? job?.team_code;
|
||
|
||
// Build raw payload (include everything we can; drop empties after)
|
||
const raw = {
|
||
// Required
|
||
vin: toStr(overrides.vin ?? job?.v_vin),
|
||
|
||
// Optional attributes
|
||
modelDesc: toStr(modelDesc),
|
||
carline: toStr(carline),
|
||
extClrDesc: toStr(extClrDesc),
|
||
intClrDesc: toStr(intClrDesc),
|
||
trimDesc: toStr(trimDesc),
|
||
bodyStyle: toStr(bodyStyle),
|
||
engineDesc: toStr(engineDesc),
|
||
transDesc: toStr(transDesc),
|
||
|
||
// Optional elements
|
||
year: toStr(year),
|
||
odometer,
|
||
odometerUnits: toStr(overrides.odometerUnits ?? odometerUnits),
|
||
|
||
// VehicleDetail
|
||
vehicleDetail: {
|
||
licNo: toStr(overrides.licNo ?? job?.plate_no)
|
||
},
|
||
|
||
// VehicleServInfo (CustomerNo is required)
|
||
vehicleServInfo: {
|
||
customerNo: toStr(custNo),
|
||
salesmanNo: toStr(salesmanNo),
|
||
inServiceDate: toStr(inServiceDate),
|
||
mileage: svMileage,
|
||
teamCode: toStr(teamCode),
|
||
vehExtWarranty,
|
||
advisor: advisorNameRecId ? { contactInfo: { nameRecId: toStr(advisorNameRecId) } } : undefined
|
||
}
|
||
};
|
||
|
||
const cleaned = dropEmptyDeep(raw);
|
||
if (!cleaned?.vin) throw new Error("buildServiceVehiclePayload: VIN is required");
|
||
if (!cleaned?.vehicleServInfo?.customerNo) throw new Error("buildServiceVehiclePayload: customerNo is required");
|
||
|
||
return cleaned;
|
||
}
|
||
|
||
/**
|
||
* Ensure a Service Vehicle (for job.v_vin) exists and is associated to the given custNo.
|
||
* - Prefer cached VIN blocks saved during step {2} under RR.VINCandidates
|
||
* - If no cache, do a single combinedSearch by VIN, then cache it
|
||
* - If not owned, insert service vehicle to associate to selected customer
|
||
*/
|
||
async function ensureRRServiceVehicle({ bodyshop, custNo, job, overrides = {}, socket, redisHelpers }) {
|
||
const log = RRLogger(socket);
|
||
const vin = job?.v_vin && String(job.v_vin).trim();
|
||
if (!vin) {
|
||
log("warn", "ensureRRServiceVehicle: no VIN on job; nothing to ensure", { jobId: job?.id });
|
||
return { ensured: false, reason: "no-vin" };
|
||
}
|
||
const custNoStr = String(custNo);
|
||
|
||
const { client, opts } = buildClientAndOpts(bodyshop);
|
||
|
||
// 1) Try cached VIN combined-search blocks from step {2}
|
||
let blocks = null;
|
||
if (redisHelpers && socket?.id && job?.id) {
|
||
try {
|
||
blocks = await redisHelpers.getSessionTransactionData(
|
||
socket.id,
|
||
getTransactionType(job.id),
|
||
RRCacheEnums.VINCandidates
|
||
);
|
||
} catch {
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
// 2) If no cached blocks, do one combinedSearch (VIN) and cache it
|
||
if (!Array.isArray(blocks) || blocks.length === 0) {
|
||
try {
|
||
const precheckRes = await client.combinedSearch(
|
||
{ vin, maxRecs: 50, kind: "vin" },
|
||
{
|
||
...opts,
|
||
envelope: {
|
||
...opts.envelope,
|
||
sender: { ...opts.envelope.sender, task: "CVC", referenceId: "Query" }
|
||
}
|
||
}
|
||
);
|
||
|
||
// Normalize to "blocks" shape (either .data or array)
|
||
blocks = Array.isArray(precheckRes?.data) ? precheckRes.data : Array.isArray(precheckRes) ? precheckRes : [];
|
||
|
||
if (redisHelpers && socket?.id && job?.id) {
|
||
try {
|
||
await redisHelpers.setSessionTransactionData(
|
||
socket.id,
|
||
getTransactionType(job.id),
|
||
RRCacheEnums.VINCandidates,
|
||
blocks,
|
||
defaultRRTTL
|
||
);
|
||
} catch {
|
||
//
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// Non-fatal: proceed to insert
|
||
log("warn", "RR combinedSearch(VIN) precheck failed; will proceed to insert", { vin, err: e?.message });
|
||
blocks = [];
|
||
}
|
||
}
|
||
|
||
// 3) Check ownership from blocks (strict)
|
||
const ownersSet = ownersFromVinBlocks(blocks, vin);
|
||
if (ownersSet.has(custNoStr)) {
|
||
log("info", "ensureRRServiceVehicle: already owned", { vin, custNo: custNoStr });
|
||
return { ensured: true, alreadyOwned: true };
|
||
}
|
||
|
||
// 4) Not owned → insert/associate service vehicle to selected customer
|
||
const payload = buildServiceVehiclePayload({ job, custNo: custNoStr, overrides });
|
||
const res = await client.insertServiceVehicle(payload, opts);
|
||
const data = res?.data ?? res;
|
||
|
||
// Normalize a simple id for later (RR often returns a DMS key)
|
||
const svId = data?.svId ?? data?.serviceVehicleId ?? data?.dmsRecKey ?? data?.DMSRecKey;
|
||
if (!svId) {
|
||
log("warn", "RR insert service vehicle returned no id", { vin, custNo: custNoStr, data });
|
||
} else {
|
||
log("info", "ensureRRServiceVehicle: inserted service vehicle", { vin, custNo: custNoStr, svId });
|
||
}
|
||
return { ensured: true, inserted: true, svId, raw: data };
|
||
}
|
||
|
||
module.exports = { ensureRRServiceVehicle, buildServiceVehiclePayload, buildClientAndOpts };
|