Files
bodyshop/server/rr/rr-job-helpers.js

197 lines
6.1 KiB
JavaScript

const client = require("../graphql-client/graphql-client").client;
const { GET_JOB_BY_PK } = require("../graphql-client/queries");
// ---------- Internals ----------
function digitsOnly(s) {
return String(s || "").replace(/[^\d]/g, "");
}
function pickJobId(ctx, explicitId) {
return explicitId || ctx?.job?.id || ctx?.payload?.job?.id || ctx?.payload?.jobId || ctx?.jobId || null;
}
function safeVin(job) {
// Your schema exposes v_vin on jobs (no vehicle_vin root field).
return (job?.v_vin && String(job.v_vin).trim()) || null;
}
// Combined search helpers expect array-like blocks
function blocksFromCombinedSearchResult(res) {
const data = Array.isArray(res?.data) ? res.data : Array.isArray(res) ? res : [];
return Array.isArray(data) ? data : [];
}
// ---------- Public API ----------
/**
* Fetch a job by id using the shared Hasura GraphQL client.
* Resolution order:
* 1) ctx.job
* 2) ctx.payload.job
* 3) ctx.payload.jobId / ctx.jobId / explicit jobId
*/
async function QueryJobData(ctx = {}, jobId) {
if (ctx?.job) return ctx.job;
if (ctx?.payload?.job) return ctx.payload.job;
const id = pickJobId(ctx, jobId);
if (!id) throw new Error("QueryJobData: jobId required (none found in ctx or args)");
try {
const res = await client.request(GET_JOB_BY_PK, { id });
const job = res?.jobs_by_pk;
if (!job) throw new Error(`Job ${id} not found`);
return job;
} catch (e) {
const msg = e?.response?.errors?.[0]?.message || e.message || "unknown";
throw new Error(`QueryJobData failed: ${msg}`);
}
}
/**
* Build minimal RR RO payload (keys match RR client expectations).
* - Requires advisor number and customer number.
* - We provide BOTH "customerNo" and "custNo" (and BOTH "advisorNo" and "advNo")
* to be compatible with the compiled RR CJS lib which currently requires
* "customerNo (or CustNo)".
*/
function buildRRRepairOrderPayload({ job, selectedCustomer, advisorNo }) {
// Resolve customerNo from object or primitive; accept multiple incoming shapes
const customerNo = selectedCustomer?.customerNo ? String(selectedCustomer?.customerNo).trim() : null;
if (!customerNo) throw new Error("No RR customer selected (customerNo/CustNo missing)");
// Advisor number (accepts advisorNo string, map to both keys)
const adv = advisorNo != null && String(advisorNo).trim() !== "" ? String(advisorNo).trim() : null;
if (!adv) throw new Error("advisorNo is required for RR export");
// Clean/normalize VIN if present
const vinRaw = job?.v_vin;
const vin =
typeof vinRaw === "string"
? vinRaw
.replace(/[^A-Za-z0-9]/g, "")
.toUpperCase()
.slice(0, 17) || undefined
: undefined;
// Pick a stable external RO number
const ro = job?.ro_number != null ? job.ro_number : job?.id != null ? job.id : null;
if (ro == null) throw new Error("Missing repair order identifier (ro_number/job_number/id)");
// Provide superset of keys for maximum compatibility with the RR client
return {
repairOrderNumber: String(ro),
deptType: "B",
vin,
customerNo: String(customerNo),
advisorNo: adv
};
}
/**
* Derive a vehicle search payload from a job.
* Prefers VIN; otherwise tries a plate, else null.
*/
function makeVehicleSearchPayloadFromJob(job) {
const vin = safeVin(job);
if (vin) return { kind: "vin", vin };
const plate = job?.plate_no;
if (plate) return { kind: "license", license: String(plate).trim() };
return null;
}
/**
* Derive a customer search payload from a job.
* Prefers phone (digits), then last name/company, then VIN.
*/
function makeCustomerSearchPayloadFromJob(job) {
const phone = job?.ownr_ph1;
const d = digitsOnly(phone);
if (d.length >= 7) return { kind: "phone", phone: d };
const lastName = job?.ownr_ln;
const company = job?.ownr_co_nm;
const lnOrCompany = lastName || company;
if (lnOrCompany) return { kind: "name", name: { name: String(lnOrCompany).trim() } };
const vin = safeVin(job);
if (vin) return { kind: "vin", vin };
return null;
}
/**
* Normalize candidate customers from a RR combined search response.
*/
function normalizeCustomerCandidates(res) {
const blocks = blocksFromCombinedSearchResult(res);
const out = [];
for (const blk of blocks) {
const serv = Array.isArray(blk?.ServVehicle) ? blk.ServVehicle : [];
const custNos = serv.map((sv) => sv?.VehicleServInfo?.CustomerNo).filter(Boolean);
const nci = blk?.NameContactId;
const ind = nci?.NameId?.IndName;
const bus = nci?.NameId?.BusName;
const personal = [ind?.FName, ind?.LName].filter(Boolean).join(" ").trim();
const company = bus?.CompanyName;
const name = (personal || company || "").trim();
for (const custNo of custNos) {
out.push({ custNo, name: name || `Customer ${custNo}`, _blk: blk });
}
}
const seen = new Set();
return out.filter((c) => {
if (!c.custNo || seen.has(c.custNo)) return false;
seen.add(c.custNo);
return true;
});
}
/**
* Normalize candidate vehicles from a RR combined search response.
*/
function normalizeVehicleCandidates(res) {
const blocks = blocksFromCombinedSearchResult(res);
console.log("Normalized vehicle Candiadates!!!!!!!!!!!!!!!!!!!!!");
console.dir({ res, blocks }, { depth: null });
const out = [];
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;
if (!vin) continue;
const year = v?.VehicleYr || v?.ModelYear || v?.Year;
const make = v?.VehicleMake || v?.MakeName || v?.Make;
const model = v?.MdlNo || v?.ModelDesc || v?.Model;
const label = [year, make, model, vin].filter(Boolean).join(" ");
out.push({ vin, year, make, model, label, _blk: blk });
}
}
const seen = new Set();
return out.filter((v) => {
if (!v.vin || seen.has(v.vin)) return false;
seen.add(v.vin);
return true;
});
}
module.exports = {
QueryJobData,
buildRRRepairOrderPayload,
makeCustomerSearchPayloadFromJob,
makeVehicleSearchPayloadFromJob,
normalizeCustomerCandidates,
normalizeVehicleCandidates
};