diff --git a/server/rr/rr-customers.js b/server/rr/rr-customers.js index 724827eb4..6e7a90aa3 100644 --- a/server/rr/rr-customers.js +++ b/server/rr/rr-customers.js @@ -1,5 +1,5 @@ // server/rr/rr-customers.js -// Minimal RR customer create helper +// Minimal RR customer create helper (maps dmsRecKey -> custNo for callers) const { RRClient } = require("./lib/index.cjs"); const { getRRConfigFromBodyshop } = require("./rr-config"); @@ -15,13 +15,16 @@ function buildClientAndOpts(bodyshop) { timeoutMs: cfg.timeoutMs, retries: cfg.retries }); + + // For customer INSERT, the STAR envelope typically uses Task="CU" and ReferenceId="Insert". + // Routing (dealer/store/area) is provided via opts.routing and applied by the lib. const opts = { routing: cfg.routing, envelope: { sender: { component: "Rome", - task: "CVC", - referenceId: "CreateCustomer", + task: "CU", + referenceId: "Insert", creator: "RCI", senderName: "RCI" } @@ -50,7 +53,7 @@ function buildCustomerPayloadFromJob(job, overrides = {}) { ""; const payload = { - // These keys follow the RR client’s customers op conventions (the lib normalizes case) + // These keys follow the RR client’s conventions; the lib normalizes case internally. firstName: firstName || undefined, lastName: lastName || undefined, companyName: company || undefined, @@ -71,7 +74,8 @@ function buildCustomerPayloadFromJob(job, overrides = {}) { /** * Create a customer in RR and return { custNo, raw }. - * Tries common op names to stay compatible with the generated client. + * NOTE: The library returns { data: { dmsRecKey, status, statusCode }, statusBlocks, ... }. + * We map data.dmsRecKey -> custNo for compatibility with existing callers. */ async function createRRCustomer({ bodyshop, job, overrides = {}, socket }) { const log = RRLogger(socket, { ns: "rr" }); @@ -79,19 +83,20 @@ async function createRRCustomer({ bodyshop, job, overrides = {}, socket }) { const payload = buildCustomerPayloadFromJob(job, overrides); let res; - // Try common method names; your lib exposes one of these. - if (typeof client.createCustomer === "function") { - res = await client.createCustomer(payload, opts); - } else if (typeof client.insertCustomer === "function") { + try { res = await client.insertCustomer(payload, opts); - } else if (client.customers && typeof client.customers.create === "function") { - res = await client.customers.create(payload, opts); - } else { - throw new Error("RR customer create operation not found in client"); + } catch (e) { + log("error", "RR insertCustomer transport error", { message: e?.message, stack: e?.stack }); + throw e; } - const data = res?.data ?? res; - const custNo = + const data = res?.data ?? res; // be tolerant to shapes + const trx = res?.statusBlocks?.transaction; + + // Primary: map dmsRecKey -> custNo + let custNo = + data?.dmsRecKey ?? + // legacy fallbacks (if shapes ever change) data?.custNo ?? data?.CustNo ?? data?.customerNo ?? @@ -100,10 +105,23 @@ async function createRRCustomer({ bodyshop, job, overrides = {}, socket }) { data?.Customer?.CustNo; if (!custNo) { - log("error", "RR create customer returned no custNo", { data }); - throw new Error("RR create customer returned no custNo"); + log("error", "RR insertCustomer returned no dmsRecKey/custNo", { + status: trx?.status, + statusCode: trx?.statusCode, + message: trx?.message, + data + }); + throw new Error( + `RR insertCustomer returned no dmsRecKey (status=${trx?.status ?? "?"} code=${trx?.statusCode ?? "?"}${ + trx?.message ? ` msg=${trx.message}` : "" + })` + ); } + // Normalize to string for safety + custNo = String(custNo); + + // Preserve existing return shape so callers don’t need changes return { custNo, raw: data }; } diff --git a/server/rr/rr-job-helpers.js b/server/rr/rr-job-helpers.js index 11dbdd8cc..efac1e333 100644 --- a/server/rr/rr-job-helpers.js +++ b/server/rr/rr-job-helpers.js @@ -1,78 +1,174 @@ -const { GraphQLClient } = require("graphql-request"); -const queries = require("../graphql-client/queries"); +// server/rr/rr-job-helpers.js +// Utilities to fetch and map job data into RR payloads using the shared Hasura client. + +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 ---------- /** - * Query job + related entities. - * Supports { socket } (GraphQL) and/or { redisHelpers } (cache/fetch). + * 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 (!jobId) throw new Error("jobId required"); + if (ctx?.job) return ctx.job; + if (ctx?.payload?.job) return ctx.payload.job; - const { redisHelpers, socket } = ctx; + const id = pickJobId(ctx, jobId); + if (!id) throw new Error("QueryJobData: jobId required (none found in ctx or args)"); - if (redisHelpers) { - if (typeof redisHelpers.getJobFromCache === "function") { - try { - const hit = await redisHelpers.getJobFromCache(jobId); - if (hit) return hit; - } catch { - // - } - } - if (typeof redisHelpers.fetchJobById === "function") { - const full = await redisHelpers.fetchJobById(jobId); - if (full) return full; - } + 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}`); } - - if (socket) { - const endpoint = process.env.GRAPHQL_ENDPOINT; - if (!endpoint) throw new Error("GRAPHQL_ENDPOINT not configured"); - - const token = (socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token); - - if (!token) throw new Error("Missing bearer token on socket for GraphQL fetch"); - - const client = new GraphQLClient(endpoint, {}); - const resp = await client - .setHeaders({ Authorization: `Bearer ${token}` }) - .request(queries.QUERY_JOBS_FOR_CDK_EXPORT, { id: jobId }); - - const job = resp?.jobs_by_pk; - if (job) return job; - throw new Error("QueryJobData: job not found via GraphQL"); - } - - throw new Error("QueryJobData: no available method to load job (need socket or redisHelpers)"); } /** - * Build RR create/update RO payload. - * Prefers selectedVehicle.vin (if present) over job VIN. + * Build minimal RR RO payload (keys match your RR client’s expectations). + * Uses fields that exist in your schema (v_vin, ro_number, owner fields, etc). */ -function buildRRRepairOrderPayload({ job, selectedCustomer, selectedVehicle, advisorNo }) { +function buildRRRepairOrderPayload({ job, selectedCustomer, advisorNo }) { const custNo = - selectedCustomer?.custNo || - selectedCustomer?.customerNo || - selectedCustomer?.CustNo || - selectedCustomer?.CustomerNo; + (selectedCustomer && (selectedCustomer.custNo || selectedCustomer.customerNo)) || + (typeof selectedCustomer === "string" || typeof selectedCustomer === "number" ? String(selectedCustomer) : null); if (!custNo) throw new Error("No RR customer selected (custNo missing)"); - const vin = selectedVehicle?.vin || job?.vehicle?.vin || job?.v_vin || job?.vehicle_vin; - - if (!vin) throw new Error("No VIN available (select or create a vehicle)"); - + const vin = safeVin(job); + // For RR create flows, VIN is typically required; leave null allowed if you gate earlier in your flow. return { - repairOrderNumber: String(job?.job_number || job?.id), + repairOrderNumber: String(job?.ro_number || job?.job_number || job?.id), deptType: "B", - vin, + vin: vin || undefined, custNo, advNo: advisorNo || undefined }; } +/** + * 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); + 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 + buildRRRepairOrderPayload, + makeCustomerSearchPayloadFromJob, + makeVehicleSearchPayloadFromJob, + normalizeCustomerCandidates, + normalizeVehicleCandidates };