339 lines
8.9 KiB
JavaScript
339 lines
8.9 KiB
JavaScript
const { buildClientAndOpts, rrCombinedSearch } = require("./rr-lookup");
|
|
const CreateRRLogEvent = require("./rr-logger-event");
|
|
const { withRRRequestXml } = require("./rr-log-xml");
|
|
/**
|
|
* 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() : "";
|
|
};
|
|
|
|
/**
|
|
* Simple length sanitizer for outbound strings
|
|
* Returns undefined if value is null/undefined/empty after trim.
|
|
*/
|
|
const sanitizeLength = (value, maxLen) => {
|
|
if (value == null) return undefined;
|
|
let s = String(value).trim();
|
|
if (!s) return undefined;
|
|
if (maxLen && s.length > maxLen) {
|
|
s = s.slice(0, maxLen);
|
|
}
|
|
return s;
|
|
};
|
|
|
|
/**
|
|
* 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",
|
|
withRRRequestXml(combinedSearchResponse, { 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",
|
|
withRRRequestXml(e, {
|
|
vin: vinStr,
|
|
error: e?.message
|
|
})
|
|
);
|
|
}
|
|
|
|
// Vendor says: MODEL DESCRIPTION HAS MAXIMUM LENGTH OF 20
|
|
const rawModelDesc = job?.v_model_desc;
|
|
const safeModelDesc = sanitizeLength(rawModelDesc, 20);
|
|
if (rawModelDesc && safeModelDesc && rawModelDesc.trim() !== safeModelDesc) {
|
|
CreateRRLogEvent(socket, "warn", "{SV} Truncated model description to 20 chars", {
|
|
original: rawModelDesc,
|
|
truncated: safeModelDesc
|
|
});
|
|
}
|
|
|
|
const insertPayload = {
|
|
vin: vinStr.toUpperCase(), // "1FDWX34Y28EB01395"
|
|
|
|
// 2-character make code (from v_make_desc → known mapping)
|
|
vehicleMake: deriveMakeCode(job?.v_make_desc), // → "FR" for Ford
|
|
year: job?.v_model_yr || undefined,
|
|
|
|
// Model description (RR: max length 20)
|
|
modelDesc: safeModelDesc,
|
|
|
|
// Model number / carline / other optional fields
|
|
mdlNo: undefined,
|
|
carline: undefined,
|
|
extClrDesc: sanitizeLength(job?.v_color, 30), // safe, configurable if vendor complains
|
|
accentClr: undefined,
|
|
aircond: undefined,
|
|
pwrstr: undefined,
|
|
transm: undefined,
|
|
turbo: undefined,
|
|
engineConfig: undefined,
|
|
trim: undefined,
|
|
|
|
// License plate
|
|
licNo: sanitizeLength(license ? String(license) : undefined, 20),
|
|
|
|
customerNo: custNoStr,
|
|
stockId: sanitizeLength(job?.ro_number, 20), // RO as stock#, truncated for safety
|
|
vehicleServInfo: {
|
|
customerNo: custNoStr, // REQUIRED — this is what toServiceVehicleView() validates
|
|
salesmanNo: undefined,
|
|
inServiceDate: undefined,
|
|
productionDate: undefined,
|
|
modelMaintCode: undefined,
|
|
teamCode: undefined,
|
|
vehExtWarranty: undefined,
|
|
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", withRRRequestXml(res, { 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",
|
|
withRRRequestXml(e, {
|
|
message: e?.message,
|
|
code: e?.code,
|
|
status: e?.meta?.status || e?.status
|
|
})
|
|
);
|
|
|
|
throw e;
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
ensureRRServiceVehicle
|
|
};
|