feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Checkpoint
This commit is contained in:
@@ -1,488 +1,257 @@
|
||||
/**
|
||||
* RR (Reynolds & Reynolds) helper module
|
||||
* -----------------------------------------------------------------------------
|
||||
* Responsibilities
|
||||
* - Load env (.env.{NODE_ENV})
|
||||
* - Provide token retrieval with simple Redis-backed caching
|
||||
* - Normalized HTTP caller (MakeRRCall) with request-id + idempotency key
|
||||
* - URL constructor w/ path + query params
|
||||
* - Optional delayed/batch polling stub (DelayedCallback)
|
||||
* - Central action registry (RRActions) with prod/uat base URLs (PLACEHOLDERS)
|
||||
* - Common cache enums + TTL + transaction-type helper (parity with Fortellis)
|
||||
*
|
||||
* What’s missing / TODOs to make this “real” (per RR/Rome PDFs you provided):
|
||||
* - Implement the actual RR auth/token flow inside getRRToken()
|
||||
* - Replace all RRActions URLs with the final endpoints from the RR spec
|
||||
* - Confirm final header names (e.g., X-Request-Id, Idempotency-Key)
|
||||
* - If RR uses async “batch/status/result”, adapt DelayedCallback() to spec
|
||||
* - Confirm success/error envelope and centralize in rr-error.js
|
||||
* @file rr-helpers.js
|
||||
* @description Core helper functions for Reynolds & Reynolds integration.
|
||||
* Handles XML rendering, SOAP communication, and configuration merging.
|
||||
*/
|
||||
|
||||
const fs = require("fs/promises");
|
||||
const path = require("path");
|
||||
require("dotenv").config({
|
||||
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
||||
});
|
||||
const mustache = require("mustache");
|
||||
const axios = require("axios");
|
||||
const { v4: uuidv4 } = require("uuid");
|
||||
const { RR_SOAP_HEADERS, RR_ACTIONS, getBaseRRConfig } = require("./rr-constants");
|
||||
const RRLogger = require("./rr-logger");
|
||||
const { client } = require("../graphql-client/graphql-client");
|
||||
const { GET_BODYSHOP_BY_ID } = require("../graphql-client/queries");
|
||||
|
||||
const uuid = require("uuid").v4;
|
||||
const AxiosLib = require("axios").default;
|
||||
const axios = AxiosLib.create();
|
||||
const axiosCurlirize = require("axios-curlirize").default;
|
||||
/* ------------------------------------------------------------------------------------------------
|
||||
* Configuration
|
||||
* ----------------------------------------------------------------------------------------------*/
|
||||
|
||||
const logger = require("../utils/logger");
|
||||
const { RrApiError } = require("./rr-error");
|
||||
|
||||
// Emit curl equivalents for dev troubleshooting (safe to disable in prod)
|
||||
axiosCurlirize(axios, (result /*, err */) => {
|
||||
/**
|
||||
* Loads the rr_configuration JSON for a given bodyshop directly from the database.
|
||||
* Dealer-level settings only. Platform/secret defaults come from getBaseRRConfig().
|
||||
* @param {string} bodyshopId
|
||||
* @returns {Promise<object>} rr_configuration
|
||||
*/
|
||||
async function getDealerConfig(bodyshopId) {
|
||||
try {
|
||||
const { command } = result;
|
||||
// Pipe to your centralized logger if preferred:
|
||||
// logger.log("rr-axios-curl", "DEBUG", "api", null, { command });
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
console.log("*** rr axios (curl):", command);
|
||||
}
|
||||
} catch {
|
||||
// Best-effort only
|
||||
}
|
||||
});
|
||||
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
|
||||
/**
|
||||
* Transaction key namespace (mirrors Fortellis' getTransactionType)
|
||||
* Used to partition per-job Redis session hashes.
|
||||
*/
|
||||
const getTransactionType = (jobid) => `rr:${jobid}`;
|
||||
|
||||
/**
|
||||
* Default per-transaction TTL for RR data cached in Redis (seconds).
|
||||
* Keep parity with the Fortellis helper to avoid drift.
|
||||
*/
|
||||
const defaultRRTTL = 60 * 60; // 1 hour
|
||||
|
||||
/**
|
||||
* Namespaced keys stored under each transaction hash (parity with Fortellis)
|
||||
* These are referenced across rr-job-export.js (and friends).
|
||||
*/
|
||||
const RRCacheEnums = {
|
||||
txEnvelope: "txEnvelope",
|
||||
DMSBatchTxn: "DMSBatchTxn",
|
||||
SubscriptionMeta: "SubscriptionMeta", // kept for parity; not used yet for RR
|
||||
DepartmentId: "DepartmentId", // kept for parity; not used yet for RR
|
||||
JobData: "JobData",
|
||||
DMSVid: "DMSVid",
|
||||
DMSVeh: "DMSVeh",
|
||||
DMSVehCustomer: "DMSVehCustomer",
|
||||
DMSCustList: "DMSCustList",
|
||||
DMSCust: "DMSCust",
|
||||
selectedCustomerId: "selectedCustomerId",
|
||||
DMSTransHeader: "DMSTransHeader",
|
||||
transWips: "transWips",
|
||||
DmsBatchTxnPost: "DmsBatchTxnPost",
|
||||
DMSVehHistory: "DMSVehHistory"
|
||||
};
|
||||
|
||||
/**
|
||||
* Provider-level token cache.
|
||||
* We reuse redisHelpers.setSessionData/getSessionData with a synthetic "socketId"
|
||||
* so we don’t need direct access to the Redis client here.
|
||||
*/
|
||||
const RR_PROVIDER_TOKEN_BUCKET = "rr:provider-token"; // becomes key: "socket:rr:provider-token"
|
||||
const RR_PROVIDER_TOKEN_FIELD = "token";
|
||||
|
||||
/**
|
||||
* Fetch an RR access token.
|
||||
* TODO: Implement the *actual* RR auth flow per the spec (client credentials
|
||||
* or whatever RCI requires). This stub uses an env or a fixed dev token.
|
||||
*
|
||||
* @param {Object} deps
|
||||
* @param {Object} deps.redisHelpers - Your redisHelpers API
|
||||
* @returns {Promise<string>} accessToken
|
||||
*/
|
||||
async function getRRToken({ redisHelpers }) {
|
||||
try {
|
||||
// Try the cache first
|
||||
const cached = await redisHelpers.getSessionData(RR_PROVIDER_TOKEN_BUCKET, RR_PROVIDER_TOKEN_FIELD);
|
||||
if (cached?.accessToken && cached?.expiresAt && Date.now() < cached.expiresAt - 5000) {
|
||||
return cached.accessToken;
|
||||
}
|
||||
|
||||
// TODO: Replace with real RR auth call. For now, fallback to env.
|
||||
const accessToken = process.env.RR_FAKE_TOKEN || "rr-dev-token";
|
||||
// Artificial ~55m expiry (adjust to actual token TTL)
|
||||
const expiresAt = Date.now() + 55 * 60 * 1000;
|
||||
|
||||
await redisHelpers.setSessionData(
|
||||
RR_PROVIDER_TOKEN_BUCKET,
|
||||
RR_PROVIDER_TOKEN_FIELD,
|
||||
{ accessToken, expiresAt },
|
||||
60 * 60 // TTL safety net
|
||||
);
|
||||
|
||||
return accessToken;
|
||||
} catch (error) {
|
||||
logger.log("rr-get-token-error", "ERROR", "api", "rr", {
|
||||
message: error?.message,
|
||||
stack: error?.stack
|
||||
});
|
||||
// Keep local dev moving even if cache errors
|
||||
return process.env.RR_FAKE_TOKEN || "rr-dev-token";
|
||||
const result = await client.request(GET_BODYSHOP_BY_ID, { id: bodyshopId });
|
||||
const cfg = result?.bodyshops_by_pk?.rr_configuration || {};
|
||||
return cfg;
|
||||
} catch (err) {
|
||||
console.error(`[RR] Failed to load rr_configuration for bodyshop ${bodyshopId}:`, err.message);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a full URL including optional path segment and query params.
|
||||
* Matches the function signature used elsewhere in the codebase.
|
||||
*
|
||||
* @param {Object} args
|
||||
* @param {string} args.url - base URL (may or may not end with "/")
|
||||
* @param {string} [args.pathParams] - string appended to URL (no leading slash)
|
||||
* @param {Array<[string,string]>} [args.requestSearchParams] - pairs converted to query params
|
||||
* @returns {string}
|
||||
* Helper to retrieve combined configuration (env + dealer) for calls.
|
||||
* NOTE: This does not hit Redis. DB only (dealer overrides) + env secrets.
|
||||
* @param {object} socket - Either a real socket or an Express req carrying bodyshopId on .bodyshopId
|
||||
* @returns {Promise<object>} configuration
|
||||
*/
|
||||
function constructFullUrl({ url, pathParams = "", requestSearchParams = [] }) {
|
||||
// normalize: ensure exactly one trailing slash on base
|
||||
url = url.replace(/\/+$/, "/");
|
||||
const fullPath = pathParams ? `${url}${pathParams}` : url;
|
||||
const query = new URLSearchParams(requestSearchParams).toString();
|
||||
return query ? `${fullPath}?${query}` : fullPath;
|
||||
async function resolveRRConfig(socket) {
|
||||
const bodyshopId = socket?.bodyshopId || socket?.user?.bodyshopid;
|
||||
const dealerCfg = bodyshopId ? await getDealerConfig(bodyshopId) : {};
|
||||
return { ...getBaseRRConfig(), ...dealerCfg };
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------
|
||||
* Template rendering
|
||||
* ----------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Loads and renders a Mustache XML template with provided data.
|
||||
* @param {string} templateName - Name of XML file under server/rr/xml-templates/ (without .xml)
|
||||
* @param {object} data - Template substitution object
|
||||
* @returns {Promise<string>} Rendered XML string
|
||||
*/
|
||||
async function renderXmlTemplate(templateName, data) {
|
||||
const templatePath = path.join(__dirname, "xml-templates", `${templateName}.xml`);
|
||||
const xmlTemplate = await fs.readFile(templatePath, "utf8");
|
||||
return mustache.render(xmlTemplate, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional delayed/batch polling flow (placeholder).
|
||||
* If RR returns a "check later" envelope, use this to poll until "complete".
|
||||
* Adjust the header names and result shapes once you have the real spec.
|
||||
*
|
||||
* @param {Object} args
|
||||
* @param {Object} args.delayMeta - body returned from initial RR call with status link(s)
|
||||
* @param {string} args.access_token - token to reuse for polling
|
||||
* @param {string} args.reqId - correlation id
|
||||
* @returns {Promise<any>}
|
||||
* Build a SOAP envelope with a rendered header + body.
|
||||
* Header comes from xml-templates/_EnvelopeHeader.xml.
|
||||
* @param {string} renderedBodyXml
|
||||
* @param {object} headerVars - values for header mustache
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function DelayedCallback({ delayMeta, access_token, reqId }) {
|
||||
// Stub example — adapt to RR if they do a batch/status-result pattern
|
||||
for (let attempt = 0; attempt < 5; attempt++) {
|
||||
await sleep((delayMeta?.checkStatusAfterSeconds || 2) * 1000);
|
||||
async function buildSoapEnvelopeWithHeader(renderedBodyXml, headerVars) {
|
||||
const headerXml = await renderXmlTemplate("_EnvelopeHeader", headerVars);
|
||||
|
||||
const statusUrl = delayMeta?._links?.status?.href;
|
||||
if (!statusUrl) {
|
||||
return { error: "No status URL provided by RR batch envelope." };
|
||||
}
|
||||
return `
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:rr="http://reynoldsandrey.com/">
|
||||
<soapenv:Header>
|
||||
${headerXml}
|
||||
</soapenv:Header>
|
||||
<soapenv:Body>
|
||||
${renderedBodyXml}
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>
|
||||
`.trim();
|
||||
}
|
||||
|
||||
const statusResult = await axios.get(statusUrl, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
"X-Request-Id": reqId
|
||||
}
|
||||
});
|
||||
/* ------------------------------------------------------------------------------------------------
|
||||
* Core SOAP caller
|
||||
* ----------------------------------------------------------------------------------------------*/
|
||||
|
||||
if (statusResult?.data?.status === "complete") {
|
||||
const resultUrl = statusResult?.data?._links?.result?.href;
|
||||
if (!resultUrl) return statusResult.data;
|
||||
const batchResult = await axios.get(resultUrl, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
"X-Request-Id": reqId
|
||||
}
|
||||
});
|
||||
return batchResult.data;
|
||||
}
|
||||
/**
|
||||
* Compute the full URL and SOAPAction for a given action spec.
|
||||
* Allows either:
|
||||
* - action: a key into RR_ACTIONS (e.g. "GetAdvisors")
|
||||
* - action: a raw URL/spec
|
||||
*/
|
||||
function resolveActionTarget(action, baseUrl) {
|
||||
if (typeof action === "string" && RR_ACTIONS[action]) {
|
||||
const spec = RR_ACTIONS[action];
|
||||
const soapAction = spec.soapAction || spec.action || action;
|
||||
const cleanedBase = (spec.baseUrl || baseUrl || "").replace(/\/+$/, "");
|
||||
const url = spec.url || (soapAction ? `${cleanedBase}/${soapAction}` : cleanedBase);
|
||||
return { url, soapAction };
|
||||
}
|
||||
return { error: "Batch result still not complete after max attempts." };
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
if (action && typeof action === "object" && (action.url || action.soapAction || action.action)) {
|
||||
const soapAction = action.soapAction || action.action || "";
|
||||
const cleanedBase = (action.baseUrl || baseUrl || "").replace(/\/+$/, "");
|
||||
const url = action.url || (soapAction ? `${cleanedBase}/${soapAction}` : cleanedBase);
|
||||
return { url, soapAction };
|
||||
}
|
||||
|
||||
if (typeof action === "string") {
|
||||
return { url: action, soapAction: "" };
|
||||
}
|
||||
|
||||
throw new Error("Invalid RR action. Must be a known RR_ACTIONS key, an action spec, or a URL string.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Core caller. Mirrors Fortellis' MakeFortellisCall shape so we can reuse flow.
|
||||
* Constructs and sends a SOAP call to the Reynolds & Reynolds endpoint.
|
||||
*
|
||||
* @param {Object} args
|
||||
* @param {string} args.apiName - logical name (used in logs/errors)
|
||||
* @param {string} args.url - base endpoint
|
||||
* @param {Object} [args.headers] - extra headers to send
|
||||
* @param {Object} [args.body] - POST/PUT body
|
||||
* @param {"get"|"post"|"put"|"delete"} [args.type="post"]
|
||||
* @param {boolean} [args.debug=true]
|
||||
* @param {string} [args.requestPathParams] - path segment to append to url
|
||||
* @param {Array<[string,string]>} [args.requestSearchParams=[]] - tuples of [key, val] for query params
|
||||
* @param {string|number} [args.jobid] - used for logger correlation (optional)
|
||||
* @param {Object} args.redisHelpers - your redisHelpers api (for token cache)
|
||||
* @param {Object} [args.socket] - pass-through so we can pull user/email if needed
|
||||
* @returns {Promise<any>}
|
||||
* Body can be one of:
|
||||
* - string (already-rendered XML body)
|
||||
* - { template: "TemplateName", data: {...} } to render server/rr/xml-templates/TemplateName.xml
|
||||
*
|
||||
* @param {object} params
|
||||
* @param {string|object} params.action - RR action key (RR_ACTIONS) or a raw URL/spec
|
||||
* @param {string|{template:string,data:object}} params.body - Rendered XML or template descriptor
|
||||
* @param {object} params.socket - The socket or req object for context (used to resolve config & logging)
|
||||
* @param {object} [params.redisHelpers]
|
||||
* @param {string|number} [params.jobid]
|
||||
* @param {object} [params.dealerConfig]
|
||||
* @param {number} [params.retries=1]
|
||||
* @returns {Promise<string>} Raw SOAP response text
|
||||
*/
|
||||
async function MakeRRCall({
|
||||
apiName,
|
||||
url,
|
||||
headers = {},
|
||||
body = {},
|
||||
type = "post",
|
||||
debug = true,
|
||||
requestPathParams,
|
||||
requestSearchParams = [],
|
||||
action,
|
||||
body,
|
||||
socket,
|
||||
// redisHelpers,
|
||||
jobid,
|
||||
redisHelpers,
|
||||
socket
|
||||
dealerConfig,
|
||||
retries = 1
|
||||
}) {
|
||||
const fullUrl = constructFullUrl({ url, pathParams: requestPathParams, requestSearchParams });
|
||||
const reqId = uuid();
|
||||
const idempotencyKey = uuid();
|
||||
const access_token = await getRRToken({ redisHelpers });
|
||||
const correlationId = uuidv4();
|
||||
|
||||
if (debug) {
|
||||
logger.log("rr-call", "DEBUG", socket?.user?.email, null, {
|
||||
apiName,
|
||||
type,
|
||||
url: fullUrl,
|
||||
jobid,
|
||||
reqId,
|
||||
body: safeLogJson(body)
|
||||
});
|
||||
const effectiveConfig = dealerConfig || (await resolveRRConfig(socket));
|
||||
const { url, soapAction } = resolveActionTarget(action, effectiveConfig.baseUrl);
|
||||
|
||||
// Render body if given by template descriptor
|
||||
let renderedBody = body;
|
||||
if (body && typeof body === "object" && body.template) {
|
||||
renderedBody = await renderXmlTemplate(body.template, body.data || {});
|
||||
}
|
||||
|
||||
try {
|
||||
const baseHeaders = {
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
"X-Request-Id": reqId,
|
||||
"Idempotency-Key": idempotencyKey,
|
||||
...headers
|
||||
};
|
||||
// Build header vars (from env + rr_configuration)
|
||||
const headerVars = {
|
||||
PPSysId: effectiveConfig.ppsysid || process.env.RR_PPSYSID || process.env.RR_PP_SYS_ID || process.env.RR_PP_SYSID,
|
||||
DealerNumber: effectiveConfig.dealer_number || effectiveConfig.dealer_id || process.env.RR_DEALER_NUMBER,
|
||||
StoreNumber: effectiveConfig.store_number || process.env.RR_STORE_NUMBER,
|
||||
BranchNumber: effectiveConfig.branch_number || process.env.RR_BRANCH_NUMBER,
|
||||
Username: effectiveConfig.username || process.env.RR_API_USER || process.env.RR_USERNAME,
|
||||
Password: effectiveConfig.password || process.env.RR_API_PASS || process.env.RR_PASSWORD,
|
||||
CorrelationId: correlationId
|
||||
};
|
||||
|
||||
let resp;
|
||||
switch ((type || "post").toLowerCase()) {
|
||||
case "get":
|
||||
resp = await axios.get(fullUrl, { headers: baseHeaders });
|
||||
break;
|
||||
case "put":
|
||||
resp = await axios.put(fullUrl, body, { headers: baseHeaders });
|
||||
break;
|
||||
case "delete":
|
||||
// Some APIs require body with DELETE; axios supports { data } for that
|
||||
resp = await axios.delete(fullUrl, { headers: baseHeaders, data: body });
|
||||
break;
|
||||
case "post":
|
||||
default:
|
||||
resp = await axios.post(fullUrl, body, { headers: baseHeaders });
|
||||
break;
|
||||
}
|
||||
// Build full SOAP envelope with proper header
|
||||
const soapEnvelope = await buildSoapEnvelopeWithHeader(renderedBody, headerVars);
|
||||
|
||||
if (debug) {
|
||||
logger.log("rr-response", "DEBUG", socket?.user?.email, null, {
|
||||
apiName,
|
||||
reqId,
|
||||
data: safeLogJson(resp?.data)
|
||||
});
|
||||
}
|
||||
RRLogger(socket, "info", `RR → ${soapAction || "SOAP"} request`, {
|
||||
jobid,
|
||||
url,
|
||||
correlationId
|
||||
});
|
||||
|
||||
// If RR returns a "check later" envelope, route through DelayedCallback
|
||||
if (resp?.data?.checkStatusAfterSeconds) {
|
||||
const delayed = await DelayedCallback({
|
||||
delayMeta: resp.data,
|
||||
access_token,
|
||||
reqId
|
||||
});
|
||||
return delayed;
|
||||
}
|
||||
const headers = {
|
||||
...RR_SOAP_HEADERS,
|
||||
SOAPAction: soapAction,
|
||||
"Content-Type": "text/xml; charset=utf-8",
|
||||
"X-Request-Id": correlationId
|
||||
};
|
||||
|
||||
return resp?.data;
|
||||
} catch (error) {
|
||||
// Handle 429 backoff hint (simple single-retry stub)
|
||||
if (error?.response?.status === 429) {
|
||||
const retryAfter = Number(error.response.headers?.["retry-after"] || 1);
|
||||
await sleep(retryAfter * 1000);
|
||||
return MakeRRCall({
|
||||
apiName,
|
||||
url,
|
||||
let attempt = 0;
|
||||
while (attempt <= retries) {
|
||||
attempt += 1;
|
||||
try {
|
||||
const response = await axios.post(url, soapEnvelope, {
|
||||
headers,
|
||||
body,
|
||||
type,
|
||||
debug,
|
||||
requestPathParams,
|
||||
requestSearchParams,
|
||||
jobid,
|
||||
redisHelpers,
|
||||
socket
|
||||
timeout: effectiveConfig.timeout || 30000,
|
||||
responseType: "text",
|
||||
validateStatus: () => true
|
||||
});
|
||||
|
||||
const text = response.data;
|
||||
|
||||
if (response.status >= 400) {
|
||||
RRLogger(socket, "error", `RR HTTP ${response.status} on ${soapAction || url}`, {
|
||||
status: response.status,
|
||||
jobid,
|
||||
correlationId,
|
||||
snippet: text?.slice?.(0, 512)
|
||||
});
|
||||
|
||||
if (response.status >= 500 && attempt <= retries) {
|
||||
RRLogger(socket, "warn", `RR transient HTTP error; retrying (${attempt}/${retries})`, {
|
||||
correlationId
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Error(`RR HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
RRLogger(socket, "debug", `RR ← ${soapAction || "SOAP"} response`, {
|
||||
jobid,
|
||||
correlationId,
|
||||
bytes: Buffer.byteLength(text || "", "utf8")
|
||||
});
|
||||
|
||||
return text;
|
||||
} catch (err) {
|
||||
const transient = /ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|socket hang up|network error/i.test(
|
||||
err?.message || ""
|
||||
);
|
||||
if (transient && attempt <= retries) {
|
||||
RRLogger(socket, "warn", `RR transient network error; retrying (${attempt}/${retries})`, {
|
||||
error: err.message,
|
||||
correlationId
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
RRLogger(socket, "error", `RR ${soapAction || "SOAP"} failed`, {
|
||||
error: err.message,
|
||||
jobid,
|
||||
correlationId
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
|
||||
const errPayload = {
|
||||
reqId,
|
||||
url: fullUrl,
|
||||
apiName,
|
||||
errorData: error?.response?.data,
|
||||
status: error?.response?.status,
|
||||
statusText: error?.response?.statusText
|
||||
};
|
||||
|
||||
// Log and throw a typed error (consistent with Fortellis helpers)
|
||||
logger.log("rr-call-error", "ERROR", socket?.user?.email, null, {
|
||||
...errPayload,
|
||||
message: error?.message,
|
||||
stack: error?.stack
|
||||
});
|
||||
|
||||
throw new RrApiError(`RR API call failed for ${apiName}: ${error?.message}`, errPayload);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Central action registry.
|
||||
* TODO: Replace ALL URLs with real RR endpoints from the Rome/RR specs.
|
||||
* You can later split into domain-specific registries if it grows large.
|
||||
*/
|
||||
const RRActions = {
|
||||
// Vehicles
|
||||
GetVehicleId: {
|
||||
apiName: "RR Get Vehicle Id",
|
||||
url: isProduction
|
||||
? "https://rr.example.com/service-vehicle-mgmt/v1/vehicle-ids/" // append VIN
|
||||
: "https://rr-uat.example.com/service-vehicle-mgmt/v1/vehicle-ids/",
|
||||
type: "get"
|
||||
},
|
||||
ReadVehicle: {
|
||||
apiName: "RR Read Vehicle",
|
||||
url: isProduction
|
||||
? "https://rr.example.com/service-vehicle-mgmt/v1/" // append vehicleId
|
||||
: "https://rr-uat.example.com/service-vehicle-mgmt/v1/",
|
||||
type: "get"
|
||||
},
|
||||
InsertVehicle: {
|
||||
apiName: "RR Insert Service Vehicle",
|
||||
url: isProduction
|
||||
? "https://rr.example.com/service-vehicle-mgmt/v1/"
|
||||
: "https://rr-uat.example.com/service-vehicle-mgmt/v1/",
|
||||
type: "post"
|
||||
},
|
||||
UpdateVehicle: {
|
||||
apiName: "RR Update Service Vehicle",
|
||||
url: isProduction
|
||||
? "https://rr.example.com/service-vehicle-mgmt/v1/"
|
||||
: "https://rr-uat.example.com/service-vehicle-mgmt/v1/",
|
||||
type: "put"
|
||||
},
|
||||
/* ------------------------------------------------------------------------------------------------
|
||||
* Exports
|
||||
* ----------------------------------------------------------------------------------------------*/
|
||||
|
||||
// Customers
|
||||
CreateCustomer: {
|
||||
apiName: "RR Create Customer",
|
||||
url: isProduction ? "https://rr.example.com/customer/v1/" : "https://rr-uat.example.com/customer/v1/",
|
||||
type: "post"
|
||||
},
|
||||
UpdateCustomer: {
|
||||
apiName: "RR Update Customer",
|
||||
url: isProduction
|
||||
? "https://rr.example.com/customer/v1/" // append /{id} if required by spec
|
||||
: "https://rr-uat.example.com/customer/v1/",
|
||||
type: "put"
|
||||
},
|
||||
ReadCustomer: {
|
||||
apiName: "RR Read Customer",
|
||||
url: isProduction
|
||||
? "https://rr.example.com/customer/v1/" // append /{id}
|
||||
: "https://rr-uat.example.com/customer/v1/",
|
||||
type: "get"
|
||||
},
|
||||
SearchCustomer: {
|
||||
apiName: "RR Query Customer By Name",
|
||||
url: isProduction ? "https://rr.example.com/customer/v1/search" : "https://rr-uat.example.com/customer/v1/search",
|
||||
type: "get"
|
||||
},
|
||||
|
||||
// Combined search (customer + vehicle)
|
||||
CombinedSearch: {
|
||||
apiName: "RR Combined Search (Customer + Vehicle)",
|
||||
url: isProduction
|
||||
? "https://rr.example.com/search/v1/customer-vehicle"
|
||||
: "https://rr-uat.example.com/search/v1/customer-vehicle",
|
||||
type: "get"
|
||||
},
|
||||
|
||||
// Advisors
|
||||
GetAdvisors: {
|
||||
apiName: "RR Get Advisors",
|
||||
url: isProduction ? "https://rr.example.com/advisors/v1" : "https://rr-uat.example.com/advisors/v1",
|
||||
type: "get"
|
||||
},
|
||||
|
||||
// Parts
|
||||
GetParts: {
|
||||
apiName: "RR Get Parts",
|
||||
url: isProduction ? "https://rr.example.com/parts/v1" : "https://rr-uat.example.com/parts/v1",
|
||||
type: "get"
|
||||
},
|
||||
|
||||
// GL / WIP (mirroring your existing flows; endpoints are placeholders)
|
||||
StartWip: {
|
||||
apiName: "RR Start WIP",
|
||||
url: isProduction ? "https://rr.example.com/glpost/v1/startWIP" : "https://rr-uat.example.com/glpost/v1/startWIP",
|
||||
type: "post"
|
||||
},
|
||||
TranBatchWip: {
|
||||
apiName: "RR Trans Batch WIP",
|
||||
url: isProduction
|
||||
? "https://rr.example.com/glpost/v1/transBatchWIP"
|
||||
: "https://rr-uat.example.com/glpost/v1/transBatchWIP",
|
||||
type: "post"
|
||||
},
|
||||
PostBatchWip: {
|
||||
apiName: "RR Post Batch WIP",
|
||||
url: isProduction
|
||||
? "https://rr.example.com/glpost/v1/postBatchWIP"
|
||||
: "https://rr-uat.example.com/glpost/v1/postBatchWIP",
|
||||
type: "post"
|
||||
},
|
||||
QueryErrorWip: {
|
||||
apiName: "RR Query Error WIP",
|
||||
url: isProduction ? "https://rr.example.com/glpost/v1/errWIP" : "https://rr-uat.example.com/glpost/v1/errWIP",
|
||||
type: "get"
|
||||
},
|
||||
|
||||
// Service history (header insert)
|
||||
ServiceHistoryInsert: {
|
||||
apiName: "RR Service Vehicle History Insert",
|
||||
url: isProduction
|
||||
? "https://rr.example.com/service-vehicle-history-mgmt/v1/"
|
||||
: "https://rr-uat.example.com/service-vehicle-history-mgmt/v1/",
|
||||
type: "post"
|
||||
},
|
||||
|
||||
// Repair Orders
|
||||
CreateRepairOrder: {
|
||||
apiName: "RR Create Repair Order",
|
||||
url: isProduction ? "https://rr.example.com/repair-orders/v1" : "https://rr-uat.example.com/repair-orders/v1",
|
||||
type: "post"
|
||||
},
|
||||
UpdateRepairOrder: {
|
||||
apiName: "RR Update Repair Order",
|
||||
url: isProduction
|
||||
? "https://rr.example.com/repair-orders/v1/" // append /{id} if required
|
||||
: "https://rr-uat.example.com/repair-orders/v1/",
|
||||
type: "put"
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Safe JSON logger helper to avoid huge payloads/recursive structures in logs.
|
||||
*/
|
||||
function safeLogJson(data) {
|
||||
try {
|
||||
const text = JSON.stringify(data);
|
||||
// cap to ~5k for logs
|
||||
return text.length > 5000 ? `${text.slice(0, 5000)}… [truncated]` : text;
|
||||
} catch {
|
||||
return "[unserializable]";
|
||||
}
|
||||
}
|
||||
const RRActions = RR_ACTIONS;
|
||||
|
||||
module.exports = {
|
||||
// core helpers
|
||||
MakeRRCall,
|
||||
RRActions,
|
||||
getRRToken,
|
||||
constructFullUrl,
|
||||
DelayedCallback,
|
||||
|
||||
// parity exports required by other RR modules
|
||||
getTransactionType,
|
||||
defaultRRTTL,
|
||||
RRCacheEnums
|
||||
getDealerConfig,
|
||||
renderXmlTemplate,
|
||||
resolveRRConfig,
|
||||
RRActions
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user