feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Checkpoint
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
// server/rr/rr-customers.js
|
// 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 { RRClient } = require("./lib/index.cjs");
|
||||||
const { getRRConfigFromBodyshop } = require("./rr-config");
|
const { getRRConfigFromBodyshop } = require("./rr-config");
|
||||||
@@ -15,13 +15,16 @@ function buildClientAndOpts(bodyshop) {
|
|||||||
timeoutMs: cfg.timeoutMs,
|
timeoutMs: cfg.timeoutMs,
|
||||||
retries: cfg.retries
|
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 = {
|
const opts = {
|
||||||
routing: cfg.routing,
|
routing: cfg.routing,
|
||||||
envelope: {
|
envelope: {
|
||||||
sender: {
|
sender: {
|
||||||
component: "Rome",
|
component: "Rome",
|
||||||
task: "CVC",
|
task: "CU",
|
||||||
referenceId: "CreateCustomer",
|
referenceId: "Insert",
|
||||||
creator: "RCI",
|
creator: "RCI",
|
||||||
senderName: "RCI"
|
senderName: "RCI"
|
||||||
}
|
}
|
||||||
@@ -50,7 +53,7 @@ function buildCustomerPayloadFromJob(job, overrides = {}) {
|
|||||||
"";
|
"";
|
||||||
|
|
||||||
const payload = {
|
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,
|
firstName: firstName || undefined,
|
||||||
lastName: lastName || undefined,
|
lastName: lastName || undefined,
|
||||||
companyName: company || undefined,
|
companyName: company || undefined,
|
||||||
@@ -71,7 +74,8 @@ function buildCustomerPayloadFromJob(job, overrides = {}) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a customer in RR and return { custNo, raw }.
|
* 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 }) {
|
async function createRRCustomer({ bodyshop, job, overrides = {}, socket }) {
|
||||||
const log = RRLogger(socket, { ns: "rr" });
|
const log = RRLogger(socket, { ns: "rr" });
|
||||||
@@ -79,19 +83,20 @@ async function createRRCustomer({ bodyshop, job, overrides = {}, socket }) {
|
|||||||
const payload = buildCustomerPayloadFromJob(job, overrides);
|
const payload = buildCustomerPayloadFromJob(job, overrides);
|
||||||
|
|
||||||
let res;
|
let res;
|
||||||
// Try common method names; your lib exposes one of these.
|
try {
|
||||||
if (typeof client.createCustomer === "function") {
|
|
||||||
res = await client.createCustomer(payload, opts);
|
|
||||||
} else if (typeof client.insertCustomer === "function") {
|
|
||||||
res = await client.insertCustomer(payload, opts);
|
res = await client.insertCustomer(payload, opts);
|
||||||
} else if (client.customers && typeof client.customers.create === "function") {
|
} catch (e) {
|
||||||
res = await client.customers.create(payload, opts);
|
log("error", "RR insertCustomer transport error", { message: e?.message, stack: e?.stack });
|
||||||
} else {
|
throw e;
|
||||||
throw new Error("RR customer create operation not found in client");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = res?.data ?? res;
|
const data = res?.data ?? res; // be tolerant to shapes
|
||||||
const custNo =
|
const trx = res?.statusBlocks?.transaction;
|
||||||
|
|
||||||
|
// Primary: map dmsRecKey -> custNo
|
||||||
|
let custNo =
|
||||||
|
data?.dmsRecKey ??
|
||||||
|
// legacy fallbacks (if shapes ever change)
|
||||||
data?.custNo ??
|
data?.custNo ??
|
||||||
data?.CustNo ??
|
data?.CustNo ??
|
||||||
data?.customerNo ??
|
data?.customerNo ??
|
||||||
@@ -100,10 +105,23 @@ async function createRRCustomer({ bodyshop, job, overrides = {}, socket }) {
|
|||||||
data?.Customer?.CustNo;
|
data?.Customer?.CustNo;
|
||||||
|
|
||||||
if (!custNo) {
|
if (!custNo) {
|
||||||
log("error", "RR create customer returned no custNo", { data });
|
log("error", "RR insertCustomer returned no dmsRecKey/custNo", {
|
||||||
throw new Error("RR create customer returned no 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 };
|
return { custNo, raw: data };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,78 +1,174 @@
|
|||||||
const { GraphQLClient } = require("graphql-request");
|
// server/rr/rr-job-helpers.js
|
||||||
const queries = require("../graphql-client/queries");
|
// 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.
|
* Fetch a job by id using the shared Hasura GraphQL client.
|
||||||
* Supports { socket } (GraphQL) and/or { redisHelpers } (cache/fetch).
|
* Resolution order:
|
||||||
|
* 1) ctx.job
|
||||||
|
* 2) ctx.payload.job
|
||||||
|
* 3) ctx.payload.jobId / ctx.jobId / explicit jobId
|
||||||
*/
|
*/
|
||||||
async function QueryJobData(ctx = {}, 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) {
|
try {
|
||||||
if (typeof redisHelpers.getJobFromCache === "function") {
|
const res = await client.request(GET_JOB_BY_PK, { id });
|
||||||
try {
|
const job = res?.jobs_by_pk;
|
||||||
const hit = await redisHelpers.getJobFromCache(jobId);
|
if (!job) throw new Error(`Job ${id} not found`);
|
||||||
if (hit) return hit;
|
return job;
|
||||||
} catch {
|
} catch (e) {
|
||||||
//
|
const msg = e?.response?.errors?.[0]?.message || e.message || "unknown";
|
||||||
}
|
throw new Error(`QueryJobData failed: ${msg}`);
|
||||||
}
|
|
||||||
if (typeof redisHelpers.fetchJobById === "function") {
|
|
||||||
const full = await redisHelpers.fetchJobById(jobId);
|
|
||||||
if (full) return full;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
* Build minimal RR RO payload (keys match your RR client’s expectations).
|
||||||
* Prefers selectedVehicle.vin (if present) over job VIN.
|
* 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 =
|
const custNo =
|
||||||
selectedCustomer?.custNo ||
|
(selectedCustomer && (selectedCustomer.custNo || selectedCustomer.customerNo)) ||
|
||||||
selectedCustomer?.customerNo ||
|
(typeof selectedCustomer === "string" || typeof selectedCustomer === "number" ? String(selectedCustomer) : null);
|
||||||
selectedCustomer?.CustNo ||
|
|
||||||
selectedCustomer?.CustomerNo;
|
|
||||||
|
|
||||||
if (!custNo) throw new Error("No RR customer selected (custNo missing)");
|
if (!custNo) throw new Error("No RR customer selected (custNo missing)");
|
||||||
|
|
||||||
const vin = selectedVehicle?.vin || job?.vehicle?.vin || job?.v_vin || job?.vehicle_vin;
|
const vin = safeVin(job);
|
||||||
|
// For RR create flows, VIN is typically required; leave null allowed if you gate earlier in your flow.
|
||||||
if (!vin) throw new Error("No VIN available (select or create a vehicle)");
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
repairOrderNumber: String(job?.job_number || job?.id),
|
repairOrderNumber: String(job?.ro_number || job?.job_number || job?.id),
|
||||||
deptType: "B",
|
deptType: "B",
|
||||||
vin,
|
vin: vin || undefined,
|
||||||
custNo,
|
custNo,
|
||||||
advNo: advisorNo || undefined
|
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 = {
|
module.exports = {
|
||||||
QueryJobData,
|
QueryJobData,
|
||||||
buildRRRepairOrderPayload
|
buildRRRepairOrderPayload,
|
||||||
|
makeCustomerSearchPayloadFromJob,
|
||||||
|
makeVehicleSearchPayloadFromJob,
|
||||||
|
normalizeCustomerCandidates,
|
||||||
|
normalizeVehicleCandidates
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user