feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Expanded Logs / Formatting change
This commit is contained in:
@@ -1,45 +1,33 @@
|
||||
// server/rr/rr-config.js
|
||||
// Build RR client configuration from bodyshop settings or env
|
||||
|
||||
function requireString(v, name) {
|
||||
/**
|
||||
* Ensure a value is a non-empty string, else throw
|
||||
* @param v
|
||||
* @param name
|
||||
* @returns {string}
|
||||
*/
|
||||
const requireString = (v, name) => {
|
||||
const s = (v ?? "").toString().trim();
|
||||
if (!s || s.toLowerCase() === "undefined" || s.toLowerCase() === "null") {
|
||||
throw new Error(`RR config missing: ${name}`);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract RR connection + routing from a bodyshop record (preferred)
|
||||
* Falls back to process.env for any missing bits.
|
||||
*
|
||||
* Bodyshop fields expected:
|
||||
* - rr_dealerid -> dealerNumber
|
||||
* - rr_configuration: { storeNumber, branchNumber } -> storeNumber, areaNumber
|
||||
*
|
||||
* Env fallbacks:
|
||||
* RR_BASE_URL, RR_USERNAME, RR_PASSWORD,
|
||||
* RR_DEALER_NUMBER, RR_STORE_NUMBER, RR_BRANCH_NUMBER
|
||||
* Get RR config from bodyshop record, with env fallbacks
|
||||
* @param bodyshop
|
||||
* @returns {{baseUrl: string, username: string, password: string, routing: {dealerNumber: string, storeNumber: string, areaNumber: string}, timeoutMs: number, retries: {max: number}}}
|
||||
*/
|
||||
function getRRConfigFromBodyshop(bodyshop) {
|
||||
const getRRConfigFromBodyshop = (bodyshop) => {
|
||||
const baseUrl = process.env.RR_BASE_URL;
|
||||
const username = process.env.RR_USERNAME;
|
||||
const password = process.env.RR_PASSWORD;
|
||||
|
||||
// NOTE: your schema uses rr_dealerid and rr_configuration JSON
|
||||
const dealerNumber = bodyshop?.rr_dealerid ?? process.env.RR_DEALER_NUMBER;
|
||||
|
||||
const bsCfg = bodyshop?.rr_configuration || {};
|
||||
const storeNumber =
|
||||
bsCfg?.storeNumber ??
|
||||
bodyshop?.rr_store_number ?? // legacy fallback if present
|
||||
process.env.RR_STORE_NUMBER;
|
||||
|
||||
const areaNumber =
|
||||
bsCfg?.branchNumber ??
|
||||
bsCfg?.areaNumber ?? // accept either key
|
||||
bodyshop?.rr_branch_number ?? // legacy fallback if present
|
||||
process.env.RR_BRANCH_NUMBER;
|
||||
const storeNumber = bsCfg?.storeNumber;
|
||||
const areaNumber = bsCfg?.branchNumber ?? bsCfg?.areaNumber;
|
||||
|
||||
return {
|
||||
baseUrl: requireString(baseUrl, "baseUrl"),
|
||||
@@ -54,6 +42,6 @@ function getRRConfigFromBodyshop(bodyshop) {
|
||||
timeoutMs: Number(process.env.RR_TIMEOUT_MS || 30000),
|
||||
retries: { max: Number(process.env.RR_RETRIES_MAX || 2) }
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { getRRConfigFromBodyshop };
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
// File: server/rr/rr-customers.js
|
||||
const { RRClient } = require("./lib/index.cjs");
|
||||
const { getRRConfigFromBodyshop } = require("./rr-config");
|
||||
const RRLogger = require("./rr-logger");
|
||||
|
||||
/**
|
||||
* Country code map for normalization
|
||||
* @type {{US: string, USA: string, "UNITED STATES": string, CA: string, CAN: string, CANADA: string}}
|
||||
*/
|
||||
const COUNTRY_MAP = {
|
||||
US: "US",
|
||||
USA: "US",
|
||||
@@ -12,7 +15,12 @@ const COUNTRY_MAP = {
|
||||
CANADA: "CA"
|
||||
};
|
||||
|
||||
function toCountry2(v) {
|
||||
/**
|
||||
* Normalize country input to 2-char code
|
||||
* @param v
|
||||
* @returns {*|string}
|
||||
*/
|
||||
const toCountry2 = (v) => {
|
||||
const s = String(v || "")
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
@@ -20,22 +28,38 @@ function toCountry2(v) {
|
||||
if (COUNTRY_MAP[s]) return COUNTRY_MAP[s];
|
||||
// fallbacks: prefer 2-char; last resort: take first 2
|
||||
return s.length === 2 ? s : s.slice(0, 2);
|
||||
}
|
||||
};
|
||||
|
||||
function normalizePhone(num) {
|
||||
/**
|
||||
* Normalize phone number to 10-digit string
|
||||
* @param num
|
||||
* @returns {string}
|
||||
*/
|
||||
const normalizePhone = (num) => {
|
||||
const d = String(num || "").replace(/\D/g, "");
|
||||
const n = d.length === 11 && d.startsWith("1") ? d.slice(1) : d;
|
||||
return n.slice(0, 10);
|
||||
}
|
||||
};
|
||||
|
||||
function normalizePostal(pc, country) {
|
||||
/**
|
||||
* Normalize postal code based on country
|
||||
* @param pc
|
||||
* @param country
|
||||
* @returns {string}
|
||||
*/
|
||||
const normalizePostal = (pc, country) => {
|
||||
const s = String(pc || "").trim();
|
||||
if (country === "US") return s.replace(/[^0-9]/g, "").slice(0, 5);
|
||||
if (country === "CA") return s.toUpperCase().replace(/\s+/g, "").slice(0, 6);
|
||||
return s;
|
||||
}
|
||||
};
|
||||
|
||||
function sanitizeRRCustomerPayload(payload = {}) {
|
||||
/**
|
||||
* Sanitize RR customer payload (addresses, phones, names)
|
||||
* @param payload
|
||||
* @returns {{}}
|
||||
*/
|
||||
const sanitizeRRCustomerPayload = (payload = {}) => {
|
||||
const out = { ...payload };
|
||||
|
||||
out.addresses = (payload.addresses || []).map((a) => {
|
||||
@@ -60,14 +84,14 @@ function sanitizeRRCustomerPayload(payload = {}) {
|
||||
if (out.lastName) out.lastName = String(out.lastName).trim().slice(0, 30);
|
||||
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Build an RR client + common opts from a bodyshop row
|
||||
* @param bodyshop
|
||||
* @returns {{client: *, opts: {routing: {dealerNumber: *, storeNumber: *, areaNumber: *}, envelope: {sender: {component: string, task: string, referenceId: string, creator: string, senderName: string}}}}}
|
||||
*/
|
||||
function buildClientAndOpts(bodyshop) {
|
||||
const buildClientAndOpts = (bodyshop) => {
|
||||
const cfg = getRRConfigFromBodyshop(bodyshop);
|
||||
const client = new RRClient({
|
||||
baseUrl: cfg.baseUrl,
|
||||
@@ -90,25 +114,25 @@ function buildClientAndOpts(bodyshop) {
|
||||
}
|
||||
};
|
||||
return { client, opts };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Strip all non-digit characters from a string
|
||||
* @param s
|
||||
* @returns {string}
|
||||
*/
|
||||
function digitsOnly(s) {
|
||||
const digitsOnly = (s) => {
|
||||
return String(s || "").replace(/\D/g, "");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a new array with only unique values from the input array
|
||||
* @param arr
|
||||
* @returns {any[]}
|
||||
*/
|
||||
function uniq(arr) {
|
||||
const uniq = (arr) => {
|
||||
return Array.from(new Set(arr));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Build RR customer payload from job.ownr_* fields, with optional overrides.
|
||||
@@ -116,7 +140,7 @@ function uniq(arr) {
|
||||
* @param overrides
|
||||
* @returns {{ibFlag: string, firstName, lastName, customerName, createdBy, customerType, addresses: [{type, line1: *, line2, city, state, postalCode, country}], phones: {number: *}[], emails: [{address: string}]}}
|
||||
*/
|
||||
function buildCustomerPayloadFromJob(job, overrides = {}) {
|
||||
const buildCustomerPayloadFromJob = (job, overrides = {}) => {
|
||||
// Pull ONLY from job.ownr_* fields (no job.customer.*)
|
||||
const firstName = overrides.firstName ?? job?.ownr_fn ?? undefined;
|
||||
const lastName = overrides.lastName ?? job?.ownr_ln ?? undefined;
|
||||
@@ -174,13 +198,13 @@ function buildCustomerPayloadFromJob(job, overrides = {}) {
|
||||
|
||||
Object.keys(payload).forEach((k) => payload[k] === undefined && delete payload[k]);
|
||||
return payload;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a customer in RR and return { customerNo, raw }.
|
||||
* Maps data.dmsRecKey -> customerNo for compatibility with existing callers.
|
||||
*/
|
||||
async function createRRCustomer({ bodyshop, job, overrides = {}, socket }) {
|
||||
const createRRCustomer = async ({ bodyshop, job, overrides = {}, socket }) => {
|
||||
const log = RRLogger(socket, { ns: "rr" });
|
||||
const { client, opts } = buildClientAndOpts(bodyshop);
|
||||
const payload = buildCustomerPayloadFromJob(job, overrides);
|
||||
@@ -213,7 +237,7 @@ async function createRRCustomer({ bodyshop, job, overrides = {}, socket }) {
|
||||
}
|
||||
|
||||
return { customerNo: String(customerNo), raw: data };
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createRRCustomer
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// Map RR vendor/status failures into user-friendly messages the FE can show.
|
||||
|
||||
function parseVendorStatusCode(err) {
|
||||
/**
|
||||
* Parse vendor status code from various possible locations in the error object.
|
||||
* @param err
|
||||
* @returns {number|null}
|
||||
*/
|
||||
const parseVendorStatusCode = (err) => {
|
||||
// Prefer explicit numeric props when available
|
||||
const codeProp = err?.code ?? err?.statusCode ?? err?.meta?.status?.StatusCode ?? err?.status?.StatusCode;
|
||||
const num = Number(codeProp);
|
||||
@@ -9,14 +12,14 @@ function parseVendorStatusCode(err) {
|
||||
// Fallback: parse from message text (e.g., "... 507 CANNOT EXCEED ...")
|
||||
const m = String(err?.message || "").match(/\b(\d{3})\b/);
|
||||
return m ? Number(m[1]) : null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Classify RR vendor errors into a small set of stable codes/messages for the FE.
|
||||
* @param {any} err
|
||||
* @returns {{vendorStatusCode:number|null, errorCode:string, title:string, friendlyMessage:string, severity:'info'|'warning'|'error', canRetry:boolean}}
|
||||
*/
|
||||
function classifyRRVendorError(err) {
|
||||
const classifyRRVendorError = (err) => {
|
||||
const code = parseVendorStatusCode(err);
|
||||
const rawMsg = String(err?.meta?.status?.Message || err?.status?.Message || err?.message || "").toUpperCase();
|
||||
|
||||
@@ -43,6 +46,6 @@ function classifyRRVendorError(err) {
|
||||
severity: "error",
|
||||
canRetry: true
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { classifyRRVendorError };
|
||||
|
||||
@@ -3,12 +3,11 @@ const queries = require("../graphql-client/queries");
|
||||
const CreateRRLogEvent = require("./rr-logger-event");
|
||||
|
||||
/** Get bearer token from the socket (same approach used elsewhere) */
|
||||
function getAuthToken(socket) {
|
||||
return (socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token) || null;
|
||||
}
|
||||
const getAuthToken = (socket) =>
|
||||
(socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token) || null;
|
||||
|
||||
/** Compact metadata for RR */
|
||||
function buildRRExportMeta({ result, extra = {} }) {
|
||||
const buildRRExportMeta = ({ result, extra = {} }) => {
|
||||
const roStatus = result?.roStatus || result?.data?.roStatus || null;
|
||||
return {
|
||||
provider: "rr",
|
||||
@@ -25,10 +24,10 @@ function buildRRExportMeta({ result, extra = {} }) {
|
||||
parsed: result?.parsed,
|
||||
...extra
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/** Build a stringified JSON array for the `message` text column */
|
||||
function buildMessageJSONString({ error, classification, result, fallback }) {
|
||||
const buildMessageJSONString = ({ error, classification, result, fallback }) => {
|
||||
const msgs = [];
|
||||
|
||||
const clean = (v) => {
|
||||
@@ -63,13 +62,13 @@ function buildMessageJSONString({ error, classification, result, fallback }) {
|
||||
|
||||
const arr = msgs.length ? msgs : ["RR export failed"];
|
||||
return JSON.stringify(arr);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Success: mark job exported + (optionally) insert a success log.
|
||||
* Uses queries.MARK_JOB_EXPORTED (same shape as Fortellis/PBS).
|
||||
*/
|
||||
async function markRRExportSuccess({ socket, jobId, job, bodyshop, result, metaExtra = {} }) {
|
||||
const markRRExportSuccess = async ({ socket, jobId, job, bodyshop, result, metaExtra = {} }) => {
|
||||
const endpoint = process.env.GRAPHQL_ENDPOINT;
|
||||
if (!endpoint) throw new Error("GRAPHQL_ENDPOINT not configured");
|
||||
const token = getAuthToken(socket);
|
||||
@@ -114,13 +113,13 @@ async function markRRExportSuccess({ socket, jobId, job, bodyshop, result, metaE
|
||||
error: e?.message
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Failure: insert failure ExportsLog with `message` as JSON **string** (text column).
|
||||
* Uses queries.INSERT_EXPORT_LOG($logs: [exportlog_insert_input!]!).
|
||||
*/
|
||||
async function insertRRFailedExportLog({ socket, jobId, job, bodyshop, error, classification, result }) {
|
||||
const insertRRFailedExportLog = async ({ socket, jobId, job, bodyshop, error, classification, result }) => {
|
||||
const endpoint = process.env.GRAPHQL_ENDPOINT;
|
||||
if (!endpoint) throw new Error("GRAPHQL_ENDPOINT not configured");
|
||||
const token = getAuthToken(socket);
|
||||
@@ -163,7 +162,7 @@ async function insertRRFailedExportLog({ socket, jobId, job, bodyshop, error, cl
|
||||
error: e?.message
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
markRRExportSuccess,
|
||||
|
||||
@@ -4,14 +4,11 @@ const { ensureRRServiceVehicle } = require("./rr-service-vehicles");
|
||||
const RRLogger = require("./rr-logger");
|
||||
|
||||
/**
|
||||
* Orchestrate an RR export (assumes custNo already resolved):
|
||||
* - Ensure service vehicle (create flows)
|
||||
* - Create or update the Repair Order
|
||||
*
|
||||
* NOTE: This function performs the create/update step and returns the RO data.
|
||||
* "Mark exported" is handled later by the finalize step after cashiering.
|
||||
* Export a job to Reynolds & Reynolds as a Repair Order (create or update).
|
||||
* @param args
|
||||
* @returns {Promise<{success, data: *, roStatus: *, statusBlocks, xml: *, parsed: any, customerNo: string, svId: null, roNo: *}>}
|
||||
*/
|
||||
async function exportJobToRR(args) {
|
||||
const exportJobToRR = async (args) => {
|
||||
const { bodyshop, job, advisorNo, selectedCustomer, existing, socket } = args || {};
|
||||
const log = RRLogger(socket, { ns: "rr-export" });
|
||||
|
||||
@@ -95,14 +92,14 @@ async function exportJobToRR(args) {
|
||||
svId,
|
||||
roNo
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Finalize an RR Repair Order by sending finalUpdate: "Y".
|
||||
* The caller should pass the canonical `roNo` if available (prefer DMS RO #).
|
||||
* If not provided, we *safely* fall back to the external (Outsd) RO number.
|
||||
* @param args
|
||||
* @returns {Promise<{success, data: *, roStatus: *, statusBlocks, xml: *, parsed: any}>}
|
||||
*/
|
||||
async function finalizeRRRepairOrder(args) {
|
||||
const finalizeRRRepairOrder = async (args) => {
|
||||
const { bodyshop, job, advisorNo, customerNo, roNo, vin, socket } = args || {};
|
||||
const log = RRLogger(socket, { ns: "rr-finalize" });
|
||||
|
||||
@@ -171,6 +168,6 @@ async function finalizeRRRepairOrder(args) {
|
||||
xml: rrRes?.xml,
|
||||
parsed: rrRes?.parsed
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { exportJobToRR, finalizeRRRepairOrder };
|
||||
|
||||
@@ -1,29 +1,49 @@
|
||||
const client = require("../graphql-client/graphql-client").client;
|
||||
const { GET_JOB_BY_PK } = require("../graphql-client/queries");
|
||||
|
||||
// ---------- Internals ----------
|
||||
/**
|
||||
* Remove all non-digit characters from a string.
|
||||
* @param s
|
||||
* @returns {string}
|
||||
*/
|
||||
const digitsOnly = (s) => String(s || "").replace(/\D/g, "");
|
||||
|
||||
function digitsOnly(s) {
|
||||
return String(s || "").replace(/\D/g, "");
|
||||
}
|
||||
/**
|
||||
* Pick job ID from various possible locations.
|
||||
* @param ctx
|
||||
* @param explicitId
|
||||
* @returns {*|null}
|
||||
*/
|
||||
const pickJobId = (ctx, explicitId) =>
|
||||
explicitId || ctx?.job?.id || ctx?.payload?.job?.id || ctx?.payload?.jobId || ctx?.jobId || null;
|
||||
|
||||
function pickJobId(ctx, explicitId) {
|
||||
return explicitId || ctx?.job?.id || ctx?.payload?.job?.id || ctx?.payload?.jobId || ctx?.jobId || null;
|
||||
}
|
||||
/**
|
||||
* Safely get VIN from job object.
|
||||
* @param job
|
||||
* @returns {*|string|null}
|
||||
*/
|
||||
const safeVin = (job) => (job?.v_vin && String(job.v_vin).trim()) || null;
|
||||
|
||||
function safeVin(job) {
|
||||
return (job?.v_vin && String(job.v_vin).trim()) || null;
|
||||
}
|
||||
|
||||
// Combined search helpers expect array-like blocks
|
||||
function blocksFromCombinedSearchResult(res) {
|
||||
/**
|
||||
* Extract blocks array from combined search result.
|
||||
* @param res
|
||||
* @returns {any[]|*[]}
|
||||
*/
|
||||
const blocksFromCombinedSearchResult = (res) => {
|
||||
const data = Array.isArray(res?.data) ? res.data : Array.isArray(res) ? res : [];
|
||||
return Array.isArray(data) ? data : [];
|
||||
}
|
||||
};
|
||||
|
||||
// ---------- Public API ----------
|
||||
|
||||
async function QueryJobData(ctx = {}, jobId) {
|
||||
/**
|
||||
* Query job data by ID from GraphQL API.
|
||||
* @param ctx
|
||||
* @param jobId
|
||||
* @returns {Promise<*>}
|
||||
* @constructor
|
||||
*/
|
||||
const QueryJobData = async (ctx = {}, jobId) => {
|
||||
if (ctx?.job) return ctx.job;
|
||||
if (ctx?.payload?.job) return ctx.payload.job;
|
||||
|
||||
@@ -39,18 +59,16 @@ async function QueryJobData(ctx = {}, jobId) {
|
||||
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).
|
||||
* Provide ALL common variants so downstream ops accept them:
|
||||
* - RO number: outsdRoNo / OutsdRoNo / repairOrderNumber / RepairOrderNumber
|
||||
* - Dept: DeptType / departmentType / deptType
|
||||
* - VIN: Vin / vin
|
||||
* - Customer: CustNo / customerNo / custNo
|
||||
* - Advisor: AdvNo / AdvisorNo / advisorNo / advNo
|
||||
* Build RR Repair Order payload from job and customer data.
|
||||
* @param job
|
||||
* @param selectedCustomer
|
||||
* @param advisorNo
|
||||
* @returns {{outsdRoNo: string, repairOrderNumber: string, departmentType: string, vin: string, customerNo: string, advisorNo: string, mileageIn: *|null}}
|
||||
*/
|
||||
function buildRRRepairOrderPayload({ job, selectedCustomer, advisorNo }) {
|
||||
const buildRRRepairOrderPayload = ({ job, selectedCustomer, advisorNo }) => {
|
||||
const customerNo = selectedCustomer?.customerNo
|
||||
? String(selectedCustomer.customerNo).trim()
|
||||
: selectedCustomer?.custNo
|
||||
@@ -94,9 +112,14 @@ function buildRRRepairOrderPayload({ job, selectedCustomer, advisorNo }) {
|
||||
// ---- Mileage In (new) ----
|
||||
mileageIn
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
function makeVehicleSearchPayloadFromJob(job) {
|
||||
/**
|
||||
* Make vehicle search payload from job data
|
||||
* @param job
|
||||
* @returns {{kind: string, license: string}|null|{kind: string, vin: *|string}}
|
||||
*/
|
||||
const makeVehicleSearchPayloadFromJob = (job) => {
|
||||
const vin = safeVin(job);
|
||||
if (vin) return { kind: "vin", vin };
|
||||
|
||||
@@ -104,9 +127,14 @@ function makeVehicleSearchPayloadFromJob(job) {
|
||||
if (plate) return { kind: "license", license: String(plate).trim() };
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
function makeCustomerSearchPayloadFromJob(job) {
|
||||
/**
|
||||
* Make customer search payload from job data
|
||||
* @param job
|
||||
* @returns {{kind: string, vin: *|string}|{kind: string, name: {name: string}}|{kind: string, phone: string}|null}
|
||||
*/
|
||||
const makeCustomerSearchPayloadFromJob = (job) => {
|
||||
const phone = job?.ownr_ph1;
|
||||
const d = digitsOnly(phone);
|
||||
if (d.length >= 7) return { kind: "phone", phone: d };
|
||||
@@ -120,9 +148,14 @@ function makeCustomerSearchPayloadFromJob(job) {
|
||||
if (vin) return { kind: "vin", vin };
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
function normalizeCustomerCandidates(res) {
|
||||
/**
|
||||
* Normalize customer candidates from combined search result.
|
||||
* @param res
|
||||
* @returns {*[]}
|
||||
*/
|
||||
const normalizeCustomerCandidates = (res) => {
|
||||
const blocks = blocksFromCombinedSearchResult(res);
|
||||
const out = [];
|
||||
for (const blk of blocks) {
|
||||
@@ -146,9 +179,14 @@ function normalizeCustomerCandidates(res) {
|
||||
seen.add(c.custNo);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function normalizeVehicleCandidates(res) {
|
||||
/**
|
||||
* Normalize vehicle candidates from combined search result.
|
||||
* @param res
|
||||
* @returns {*[]}
|
||||
*/
|
||||
const normalizeVehicleCandidates = (res) => {
|
||||
const blocks = blocksFromCombinedSearchResult(res);
|
||||
const out = [];
|
||||
for (const blk of blocks) {
|
||||
@@ -170,7 +208,7 @@ function normalizeVehicleCandidates(res) {
|
||||
seen.add(v.vin);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
QueryJobData,
|
||||
|
||||
@@ -1,44 +1,75 @@
|
||||
// File: server/rr/rr-logger-event.js
|
||||
// Fortellis-style log helper for RR flows.
|
||||
// Usage: CreateRRLogEvent(socket, "DEBUG"|"INFO"|"WARN"|"ERROR", message, details?)
|
||||
const logger = require("../utils/logger");
|
||||
|
||||
const RRLogger = require("../rr/rr-logger");
|
||||
/**
|
||||
* Convert an Error object to a plain object for serialization.
|
||||
* @param err
|
||||
* @returns {{[p: string]: unknown, name: *, message: *, stack: *}|null}
|
||||
*/
|
||||
const toPlainError = (err) => {
|
||||
if (!err) return null;
|
||||
return {
|
||||
name: err.name,
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
...Object.fromEntries(Object.entries(err).filter(([k]) => !["stack"].includes(k)))
|
||||
};
|
||||
};
|
||||
|
||||
// Normalize level to upper + provide console + socket event with coherent payload
|
||||
function CreateRRLogEvent(socket, level = "DEBUG", message = "", details = {}) {
|
||||
const lvl = String(level || "DEBUG").toUpperCase();
|
||||
/**
|
||||
* Safely serialize meta information for logging.
|
||||
* @param meta
|
||||
* @returns {{note: string}|any|{[p: string]: *, name: *, message: *, stack: *}|null}
|
||||
*/
|
||||
const safeMeta = (meta) => {
|
||||
try {
|
||||
if (meta instanceof Error) return toPlainError(meta);
|
||||
if (meta && typeof meta === "object") {
|
||||
// JSON-safe clone w/ BigInt -> string
|
||||
return JSON.parse(JSON.stringify(meta, (_k, v) => (typeof v === "bigint" ? v.toString() : v)));
|
||||
}
|
||||
return meta ?? null;
|
||||
} catch {
|
||||
return { note: "meta not serializable" };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create and emit a Reynolds log event.
|
||||
* @param socket
|
||||
* @param level
|
||||
* @param message
|
||||
* @param meta
|
||||
* @returns {{timestamp: number, level: string, message: string|string, meta: {note: string}|*|{[p: string]: *, name: *, message: *, stack: *}|null}}
|
||||
* @constructor
|
||||
*/
|
||||
const CreateRRLogEvent = (socket, level = "INFO", message = "", meta = null) => {
|
||||
const ts = Date.now();
|
||||
const lvl = String(level || "INFO").toUpperCase();
|
||||
const msg = typeof message === "string" ? message : (message?.toString?.() ?? JSON.stringify(message));
|
||||
|
||||
// Console (uses existing RRLogger, which also emits "RR:LOG" to sockets for live tail)
|
||||
const payload = {
|
||||
timestamp: ts,
|
||||
level: lvl,
|
||||
message: msg,
|
||||
meta: safeMeta(meta)
|
||||
};
|
||||
|
||||
// Console
|
||||
try {
|
||||
const log = RRLogger(socket);
|
||||
const fn = lvl === "ERROR" ? "error" : lvl === "WARN" ? "warn" : lvl === "INFO" ? "info" : "debug";
|
||||
log(fn, message, { ts, ...safeJson(details) });
|
||||
const fn = logger?.logger?.[lvl.toLowerCase()] ?? logger?.logger?.info ?? console.log;
|
||||
fn(`[RR] ${new Date(ts).toISOString()} | ${lvl} | ${msg}`, payload.meta);
|
||||
} catch {
|
||||
/* ignore console/log failures */
|
||||
// ignore console failures
|
||||
}
|
||||
|
||||
// Structured RR event for FE debug panel (parity with Fortellis' CreateFortellisLogEvent)
|
||||
// socket.emit("fortellis-log-event", { level, message, txnDetails });
|
||||
// Socket
|
||||
try {
|
||||
socket?.emit?.("rr-log-event", {
|
||||
level: lvl,
|
||||
message,
|
||||
ts,
|
||||
txnDetails: details
|
||||
});
|
||||
socket?.emit?.("rr-log-event", payload);
|
||||
} catch {
|
||||
/* ignore socket emit failures */
|
||||
// ignore socket failures
|
||||
}
|
||||
}
|
||||
|
||||
// Best-effort ensure details are JSON-safe (avoid circular / BigInt)
|
||||
function safeJson(obj) {
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(obj ?? {}));
|
||||
} catch {
|
||||
return { _unsafe: true };
|
||||
}
|
||||
}
|
||||
return payload;
|
||||
};
|
||||
|
||||
module.exports = CreateRRLogEvent;
|
||||
|
||||
@@ -1,35 +1,32 @@
|
||||
// File: server/rr/rr-logger.js
|
||||
// Console logger for RR flows with safe JSON. No socket emission by default.
|
||||
|
||||
const baseLogger = require("../utils/logger");
|
||||
|
||||
function RRLogger(_socket, defaults = {}) {
|
||||
function safeSerialize(value) {
|
||||
try {
|
||||
const seen = new WeakSet();
|
||||
return JSON.stringify(value, (key, val) => {
|
||||
if (typeof val === "bigint") return val.toString();
|
||||
if (val instanceof Error) return { name: val.name, message: val.message, stack: val.stack };
|
||||
if (typeof val === "function") return undefined;
|
||||
if (typeof val === "object" && val !== null) {
|
||||
if (seen.has(val)) return "[Circular]";
|
||||
seen.add(val);
|
||||
if (val instanceof Date) return val.toISOString();
|
||||
if (val instanceof Map) return Object.fromEntries(val);
|
||||
if (val instanceof Set) return Array.from(val);
|
||||
if (typeof Buffer !== "undefined" && Buffer.isBuffer?.(val)) return `<Buffer len=${val.length}>`;
|
||||
}
|
||||
return val;
|
||||
});
|
||||
} catch {
|
||||
try {
|
||||
return String(value);
|
||||
} catch {
|
||||
return "[Unserializable]";
|
||||
const safeSerialize = (value) => {
|
||||
try {
|
||||
const seen = new WeakSet();
|
||||
return JSON.stringify(value, (key, val) => {
|
||||
if (typeof val === "bigint") return val.toString();
|
||||
if (val instanceof Error) return { name: val.name, message: val.message, stack: val.stack };
|
||||
if (typeof val === "function") return undefined;
|
||||
if (typeof val === "object" && val !== null) {
|
||||
if (seen.has(val)) return "[Circular]";
|
||||
seen.add(val);
|
||||
if (val instanceof Date) return val.toISOString();
|
||||
if (val instanceof Map) return Object.fromEntries(val);
|
||||
if (val instanceof Set) return Array.from(val);
|
||||
if (typeof Buffer !== "undefined" && Buffer.isBuffer?.(val)) return `<Buffer len=${val.length}>`;
|
||||
}
|
||||
return val;
|
||||
});
|
||||
} catch {
|
||||
try {
|
||||
return String(value);
|
||||
} catch {
|
||||
return "[Unserializable]";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const RRLogger = (_socket, defaults = {}) => {
|
||||
return function log(level = "info", message = "", ctx = {}) {
|
||||
const lvl = String(level || "info").toLowerCase();
|
||||
const iso = new Date().toISOString();
|
||||
@@ -48,9 +45,11 @@ function RRLogger(_socket, defaults = {}) {
|
||||
} catch {
|
||||
try {
|
||||
console.log(line);
|
||||
} catch {}
|
||||
} catch {
|
||||
// swallow
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = RRLogger;
|
||||
|
||||
@@ -4,7 +4,7 @@ const { getRRConfigFromBodyshop } = require("./rr-config");
|
||||
/**
|
||||
* Build an RR client + common opts from a bodyshop row
|
||||
*/
|
||||
function buildClientAndOpts(bodyshop) {
|
||||
const buildClientAndOpts = (bodyshop) => {
|
||||
const cfg = getRRConfigFromBodyshop(bodyshop);
|
||||
|
||||
const client = new RRClient({
|
||||
@@ -30,13 +30,13 @@ function buildClientAndOpts(bodyshop) {
|
||||
};
|
||||
|
||||
return { client, opts };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalize the combined-search arguments into the RR shape.
|
||||
* We infer `kind` if not provided, based on the first detectable field.
|
||||
*/
|
||||
function toCombinedSearchPayload(args = {}) {
|
||||
const toCombinedSearchPayload = (args = {}) => {
|
||||
const q = { ...args };
|
||||
|
||||
// Decide kind if not provided
|
||||
@@ -125,26 +125,26 @@ function toCombinedSearchPayload(args = {}) {
|
||||
if (q.license && payload.kind !== "license") payload.license = String(q.license).trim();
|
||||
|
||||
return payload;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Combined customer/service/vehicle search
|
||||
* @param bodyshop - bodyshop row (must include rr_dealerid & rr_configuration with store/branch)
|
||||
* @param args - search inputs (phone | license | vin | nameRecId | name | stkNo)
|
||||
*/
|
||||
async function rrCombinedSearch(bodyshop, args = {}) {
|
||||
const rrCombinedSearch = async (bodyshop, args = {}) => {
|
||||
const { client, opts } = buildClientAndOpts(bodyshop);
|
||||
const payload = toCombinedSearchPayload(args);
|
||||
const res = await client.combinedSearch(payload, opts);
|
||||
return res?.data ?? res; // lib returns { success, data, ... }
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Advisors lookup
|
||||
* @param bodyshop
|
||||
* @param args - { department: 'B'|'S'|'P'|string, advisorNumber?: string }
|
||||
*/
|
||||
async function rrGetAdvisors(bodyshop, args = {}) {
|
||||
const rrGetAdvisors = async (bodyshop, args = {}) => {
|
||||
const { client, opts } = buildClientAndOpts(bodyshop);
|
||||
// Accept either department or departmentType from FE
|
||||
const dep = String(args.department ?? args.departmentType ?? "").toUpperCase();
|
||||
@@ -158,7 +158,7 @@ async function rrGetAdvisors(bodyshop, args = {}) {
|
||||
|
||||
const res = await client.getAdvisors(payload, opts);
|
||||
return res?.data ?? res;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
rrCombinedSearch,
|
||||
|
||||
@@ -6,10 +6,7 @@ const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").defa
|
||||
const { createRRCustomer } = require("./rr-customers");
|
||||
const { ensureRRServiceVehicle } = require("./rr-service-vehicles");
|
||||
const { classifyRRVendorError } = require("./rr-errors");
|
||||
|
||||
// NEW: export logs (success/failure) parity with Fortellis/PBS
|
||||
const { markRRExportSuccess, insertRRFailedExportLog } = require("./rr-export-logs");
|
||||
|
||||
const {
|
||||
makeVehicleSearchPayloadFromJob,
|
||||
ownersFromVinBlocks,
|
||||
@@ -19,7 +16,6 @@ const {
|
||||
defaultRRTTL,
|
||||
RRCacheEnums
|
||||
} = require("./rr-utils");
|
||||
|
||||
const { GraphQLClient } = require("graphql-request");
|
||||
const queries = require("../graphql-client/queries");
|
||||
|
||||
@@ -36,9 +32,8 @@ const ADVISORS_CACHE_TTL = 7 * 24 * 60 * 60; // seconds
|
||||
* @param job
|
||||
* @returns {*|null}
|
||||
*/
|
||||
function resolveJobId(explicit, payload, job) {
|
||||
return explicit || payload?.jobId || payload?.jobid || job?.id || job?.jobId || job?.jobid || null;
|
||||
}
|
||||
const resolveJobId = (explicit, payload, job) =>
|
||||
explicit || payload?.jobId || payload?.jobid || job?.id || job?.jobId || job?.jobid || null;
|
||||
|
||||
/**
|
||||
* Resolve VIN from tx/job shapes
|
||||
@@ -46,18 +41,15 @@ function resolveJobId(explicit, payload, job) {
|
||||
* @param job
|
||||
* @returns {*|null}
|
||||
*/
|
||||
function resolveVin({ tx, job }) {
|
||||
// Prefer cached tx vin (if we made one), then common job shapes (v_vin for our schema)
|
||||
return tx?.jobData?.vin || job?.v_vin || null;
|
||||
}
|
||||
const resolveVin = ({ tx, job }) => tx?.jobData?.vin || job?.v_vin || null;
|
||||
|
||||
/**
|
||||
* Sort vehicle owners first in the list, preserving original order otherwise.
|
||||
* @param list
|
||||
* @returns {*}
|
||||
*/
|
||||
function sortVehicleOwnerFirst(list) {
|
||||
return list
|
||||
const sortVehicleOwnerFirst = (list) =>
|
||||
list
|
||||
.map((v, i) => ({ v, i }))
|
||||
.sort((a, b) => {
|
||||
const ao = a.v?.isVehicleOwner ? 1 : 0;
|
||||
@@ -66,15 +58,13 @@ function sortVehicleOwnerFirst(list) {
|
||||
return a.i - b.i;
|
||||
})
|
||||
.map(({ v }) => v);
|
||||
}
|
||||
|
||||
/**
|
||||
* NEW: merge candidates coming from multiple queries (name + vin) by custNo.
|
||||
* - keeps first non-empty name
|
||||
* - preserves/ORs vinOwner/isVehicleOwner
|
||||
* - keeps first non-empty address
|
||||
* Merge customer candidates by custNo, combining isVehicleOwner flags and filling missing fields.
|
||||
* @param items
|
||||
* @returns {any[]}
|
||||
*/
|
||||
function mergeByCustNo(items = []) {
|
||||
const mergeByCustNo = (items = []) => {
|
||||
const byId = new Map();
|
||||
for (const c of items) {
|
||||
const id = (c?.custNo || "").trim();
|
||||
@@ -93,7 +83,7 @@ function mergeByCustNo(items = []) {
|
||||
}
|
||||
}
|
||||
return Array.from(byId.values());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get session data or socket fallback
|
||||
@@ -101,7 +91,7 @@ function mergeByCustNo(items = []) {
|
||||
* @param socket
|
||||
* @returns {Promise<{bodyshopId: *, email: *, sess: null}>}
|
||||
*/
|
||||
async function getSessionOrSocket(redisHelpers, socket) {
|
||||
const getSessionOrSocket = async (redisHelpers, socket) => {
|
||||
let sess = null;
|
||||
try {
|
||||
sess = await redisHelpers.getSessionData(socket.id);
|
||||
@@ -112,7 +102,7 @@ async function getSessionOrSocket(redisHelpers, socket) {
|
||||
const email = sess?.email ?? socket.user?.email;
|
||||
if (!bodyshopId) throw new Error("No bodyshopId (session/socket)");
|
||||
return { bodyshopId, email, sess };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch bodyshop data for socket
|
||||
@@ -120,7 +110,7 @@ async function getSessionOrSocket(redisHelpers, socket) {
|
||||
* @param socket
|
||||
* @returns {Promise<{id: string, intellipay_config: {payment_map: {amex: string}}}|{id: string, intellipay_config: null}|*>}
|
||||
*/
|
||||
async function getBodyshopForSocket({ bodyshopId, socket }) {
|
||||
const getBodyshopForSocket = async ({ bodyshopId, 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);
|
||||
@@ -131,12 +121,16 @@ async function getBodyshopForSocket({ bodyshopId, socket }) {
|
||||
const bodyshop = res?.bodyshops_by_pk;
|
||||
if (!bodyshop) throw new Error(`Bodyshop not found: ${bodyshopId}`);
|
||||
return bodyshop;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Build advisors cache namespace + field (per bodyshop + routing + department)
|
||||
* Build advisors cache namespace and field
|
||||
* @param bodyshopId
|
||||
* @param routing
|
||||
* @param departmentType
|
||||
* @returns {{ns: string, field: string}}
|
||||
*/
|
||||
function buildAdvisorsCacheNS({ bodyshopId, routing, departmentType = "B" }) {
|
||||
const buildAdvisorsCacheNS = ({ bodyshopId, routing, departmentType = "B" }) => {
|
||||
const dealer = routing?.dealerNumber || "unknown";
|
||||
const store = routing?.storeNumber || "none";
|
||||
const area = routing?.areaNumber || "none";
|
||||
@@ -145,12 +139,17 @@ function buildAdvisorsCacheNS({ bodyshopId, routing, departmentType = "B" }) {
|
||||
ns: `rr:advisors:${bodyshopId}:${dealer}:${store}:${area}`,
|
||||
field: `dept:${dept}`
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* VIN + Full Name merge (export flow)
|
||||
* Run multi-query customer search (Full Name + VIN) and merge results.
|
||||
* @param bodyshop
|
||||
* @param job
|
||||
* @param socket
|
||||
* @param redisHelpers
|
||||
* @returns {Promise<*|*[]>}
|
||||
*/
|
||||
async function rrMultiCustomerSearch({ bodyshop, job, socket, redisHelpers }) {
|
||||
const rrMultiCustomerSearch = async ({ bodyshop, job, socket, redisHelpers }) => {
|
||||
const queriesList = [];
|
||||
|
||||
// 1) Full Name (preferred)
|
||||
@@ -207,11 +206,14 @@ async function rrMultiCustomerSearch({ bodyshop, job, socket, redisHelpers }) {
|
||||
const deduped = mergeByCustNo(merged);
|
||||
|
||||
return sortVehicleOwnerFirst(deduped);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------- register handlers ----------------
|
||||
function registerRREvents({ socket, redisHelpers }) {
|
||||
// ---------- Lookup passthrough ----------
|
||||
/**
|
||||
* Register RR socket events
|
||||
* @param socket
|
||||
* @param redisHelpers
|
||||
*/
|
||||
const registerRREvents = ({ socket, redisHelpers }) => {
|
||||
socket.on("rr-lookup-combined", async ({ jobid, params } = {}, cb) => {
|
||||
try {
|
||||
const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
|
||||
@@ -242,7 +244,6 @@ function registerRREvents({ socket, redisHelpers }) {
|
||||
}
|
||||
});
|
||||
|
||||
// ---------- Advisors (cached) ----------
|
||||
socket.on("rr-get-advisors", async (args = {}, ack) => {
|
||||
const refresh = !!args?.refresh;
|
||||
const requestedDept = (args?.departmentType || "B").toUpperCase();
|
||||
@@ -317,8 +318,6 @@ function registerRREvents({ socket, redisHelpers }) {
|
||||
}
|
||||
});
|
||||
|
||||
// ================= Fortellis-style two-step export (RR only) =================
|
||||
// 1) Stage export -> search (Full Name + VIN) -> emit rr-select-customer
|
||||
socket.on("rr-export-job", async ({ jobid, jobId, txEnvelope } = {}) => {
|
||||
const rid = resolveJobId(jobid || jobId, { jobId, jobid }, null);
|
||||
try {
|
||||
@@ -385,7 +384,6 @@ function registerRREvents({ socket, redisHelpers }) {
|
||||
}
|
||||
});
|
||||
|
||||
// 2) Selection (or create) -> ensure vehicle -> CREATE RO (do not mark exported)
|
||||
socket.on("rr-selected-customer", async ({ jobid, jobId, selectedCustomerId, custNo, create } = {}, ack) => {
|
||||
const rid = resolveJobId(jobid || jobId, { jobid, jobId }, null);
|
||||
let bodyshop = null;
|
||||
@@ -701,7 +699,6 @@ function registerRREvents({ socket, redisHelpers }) {
|
||||
}
|
||||
});
|
||||
|
||||
// 3) Finalize -> updateRepairOrder(finalUpdate: "Y") -> mark exported
|
||||
socket.on("rr-finalize-repair-order", async ({ jobid, jobId } = {}, ack) => {
|
||||
const rid = resolveJobId(jobid || jobId, { jobid, jobId }, null);
|
||||
let bodyshop = null;
|
||||
@@ -847,7 +844,6 @@ function registerRREvents({ socket, redisHelpers }) {
|
||||
}
|
||||
});
|
||||
|
||||
// ---------- Allocations (parity) ----------
|
||||
socket.on("rr-calculate-allocations", async (jobid, cb) => {
|
||||
try {
|
||||
CreateRRLogEvent(socket, "DEBUG", "rr-calculate-allocations: begin", { jobid });
|
||||
@@ -860,6 +856,6 @@ function registerRREvents({ socket, redisHelpers }) {
|
||||
cb?.({ ok: false, error: e.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = registerRREvents;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// File: server/rr/rr-service-vehicles.js
|
||||
// Idempotent Service Vehicle ensure: if VIN exists (owner match or not), don't fail.
|
||||
|
||||
const RRLogger = require("./rr-logger");
|
||||
const { buildClientAndOpts, rrCombinedSearch } = require("./rr-lookup");
|
||||
|
||||
// --- helpers ---
|
||||
function pickVin({ vin, job }) {
|
||||
/**
|
||||
* 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 "";
|
||||
@@ -13,14 +15,27 @@ function pickVin({ vin, job }) {
|
||||
.replace(/[^A-Za-z0-9]/g, "")
|
||||
.toUpperCase()
|
||||
.slice(0, 17);
|
||||
}
|
||||
};
|
||||
|
||||
function pickCustNo({ selectedCustomerNo, custNo, customerNo }) {
|
||||
/**
|
||||
* 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() : "";
|
||||
}
|
||||
};
|
||||
|
||||
function ownersFromCombined(res, wantedVin) {
|
||||
/**
|
||||
* 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 : Array.isArray(res) ? res : [];
|
||||
const owners = new Set();
|
||||
for (const blk of blocks) {
|
||||
@@ -35,14 +50,19 @@ function ownersFromCombined(res, wantedVin) {
|
||||
}
|
||||
}
|
||||
return owners;
|
||||
}
|
||||
};
|
||||
|
||||
function isAlreadyExistsError(e) {
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Ensure/create a Service Vehicle in RR for the given VIN + customer.
|
||||
@@ -56,7 +76,7 @@ function isAlreadyExistsError(e) {
|
||||
*
|
||||
* Returns: { created:boolean, exists:boolean, vin, customerNo, svId?, status? }
|
||||
*/
|
||||
async function ensureRRServiceVehicle(args = {}) {
|
||||
const ensureRRServiceVehicle = async (args = {}) => {
|
||||
const {
|
||||
client: inClient,
|
||||
routing: inRouting,
|
||||
@@ -199,7 +219,7 @@ async function ensureRRServiceVehicle(args = {}) {
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
ensureRRServiceVehicle
|
||||
|
||||
Reference in New Issue
Block a user