feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Checkpoint
This commit is contained in:
@@ -1,257 +1,319 @@
|
||||
/**
|
||||
* @file rr-helpers.js
|
||||
* @description Core helper functions for Reynolds & Reynolds integration.
|
||||
* Handles XML rendering, SOAP communication, and configuration merging.
|
||||
* STAR-only SOAP transport + template rendering for Reynolds & Reynolds (Rome/RCI).
|
||||
* - Renders Mustache STAR business templates (rey_*Req rooted with STAR ns)
|
||||
* - Builds STAR SOAP envelope (ProcessMessage/payload/content + ApplicationArea)
|
||||
* - Posts to RCI endpoint with STAR SOAPAction (full URI)
|
||||
* - Parses XML response (faults + STAR payload result)
|
||||
*/
|
||||
|
||||
const fs = require("fs/promises");
|
||||
const path = require("path");
|
||||
const mustache = require("mustache");
|
||||
const axios = require("axios");
|
||||
const { v4: uuidv4 } = require("uuid");
|
||||
const { RR_SOAP_HEADERS, RR_ACTIONS, getBaseRRConfig } = require("./rr-constants");
|
||||
const mustache = require("mustache");
|
||||
const { XMLParser } = require("fast-xml-parser");
|
||||
const RRLogger = require("./rr-logger");
|
||||
const { client } = require("../graphql-client/graphql-client");
|
||||
const { GET_BODYSHOP_BY_ID } = require("../graphql-client/queries");
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------
|
||||
* Configuration
|
||||
* ----------------------------------------------------------------------------------------------*/
|
||||
const { RR_ACTIONS, RR_SOAP_HEADERS, RR_STAR_SOAP_ACTION, RR_NS, getBaseRRConfig } = require("./rr-constants");
|
||||
const { RrApiError } = require("./rr-error");
|
||||
const xmlFormatter = require("xml-formatter");
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Remove XML decl, collapse inter-tag whitespace, strip empty lines,
|
||||
* then pretty-print. Safe for XML because we only touch whitespace
|
||||
* BETWEEN tags, not inside text nodes.
|
||||
/**
|
||||
* Collapse Mustache-induced whitespace and pretty print.
|
||||
* - strips XML decl (inner)
|
||||
* - removes lines that are only whitespace
|
||||
* - collapses inter-tag whitespace
|
||||
* - formats with consistent indentation
|
||||
*/
|
||||
async function getDealerConfig(bodyshopId) {
|
||||
try {
|
||||
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 {};
|
||||
}
|
||||
function prettyPrintXml(xml) {
|
||||
let s = xml;
|
||||
|
||||
// strip any inner XML declaration
|
||||
s = s.replace(/^\s*<\?xml[^>]*\?>\s*/i, "");
|
||||
|
||||
// remove lines that are only whitespace
|
||||
s = s.replace(/^[\t ]*(?:\r?\n)/gm, "");
|
||||
|
||||
// collapse whitespace strictly between tags (not inside text nodes)
|
||||
s = s.replace(/>\s+</g, "><");
|
||||
|
||||
// final pretty print
|
||||
return xmlFormatter(s, {
|
||||
indentation: " ",
|
||||
collapseContent: true, // keep short elements on one line
|
||||
lineSeparator: "\n",
|
||||
strictMode: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
async function resolveRRConfig(socket) {
|
||||
const bodyshopId = socket?.bodyshopId || socket?.user?.bodyshopid;
|
||||
const dealerCfg = bodyshopId ? await getDealerConfig(bodyshopId) : {};
|
||||
return { ...getBaseRRConfig(), ...dealerCfg };
|
||||
// ---------- Public action map (compat with rr-test.js) ----------
|
||||
const RRActions = Object.fromEntries(Object.entries(RR_ACTIONS).map(([k]) => [k, { action: k }]));
|
||||
|
||||
// ---------- Template cache ----------
|
||||
const templateCache = new Map();
|
||||
|
||||
async function loadTemplate(templateName) {
|
||||
if (templateCache.has(templateName)) return templateCache.get(templateName);
|
||||
const filePath = path.join(__dirname, "xml-templates", `${templateName}.xml`);
|
||||
const tpl = await fs.readFile(filePath, "utf8");
|
||||
templateCache.set(templateName, tpl);
|
||||
return tpl;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------
|
||||
* 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);
|
||||
const tpl = await loadTemplate(templateName);
|
||||
// Render and strip any XML declaration to keep a single root element for the BOD
|
||||
const rendered = mustache.render(tpl, data || {});
|
||||
return rendered.replace(/^\s*<\?xml[^>]*\?>\s*/i, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 buildSoapEnvelopeWithHeader(renderedBodyXml, headerVars) {
|
||||
const headerXml = await renderXmlTemplate("_EnvelopeHeader", headerVars);
|
||||
// ---------- Config resolution (STAR only) ----------
|
||||
async function resolveRRConfig(_socket, bodyshopConfig) {
|
||||
const envCfg = getBaseRRConfig();
|
||||
|
||||
return `
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:rr="http://reynoldsandrey.com/">
|
||||
if (bodyshopConfig && typeof bodyshopConfig === "object") {
|
||||
return {
|
||||
...envCfg,
|
||||
baseUrl: bodyshopConfig.baseUrl || envCfg.baseUrl,
|
||||
username: bodyshopConfig.username || envCfg.username,
|
||||
password: bodyshopConfig.password || envCfg.password,
|
||||
ppsysId: bodyshopConfig.ppsysId || envCfg.ppsysId,
|
||||
dealerNumber: bodyshopConfig.dealer_number || envCfg.dealerNumber,
|
||||
storeNumber: bodyshopConfig.store_number || envCfg.storeNumber,
|
||||
branchNumber: bodyshopConfig.branch_number || envCfg.branchNumber,
|
||||
wssePasswordType: bodyshopConfig.wssePasswordType || envCfg.wssePasswordType || "Text",
|
||||
timeout: envCfg.timeout
|
||||
};
|
||||
}
|
||||
return envCfg;
|
||||
}
|
||||
|
||||
// ---------- Response parsing ----------
|
||||
function parseRRResponse(xml) {
|
||||
const parser = new XMLParser({
|
||||
ignoreAttributes: false,
|
||||
removeNSPrefix: true
|
||||
});
|
||||
|
||||
const doc = parser.parse(xml);
|
||||
|
||||
// Envelope/Body
|
||||
const body =
|
||||
doc?.Envelope?.Body ||
|
||||
doc?.["soapenv:Envelope"]?.["soapenv:Body"] ||
|
||||
doc?.["SOAP-ENV:Envelope"]?.["SOAP-ENV:Body"] ||
|
||||
doc?.["S:Envelope"]?.["S:Body"] ||
|
||||
doc?.Body ||
|
||||
doc;
|
||||
|
||||
// SOAP Fault?
|
||||
const fault = body?.Fault || body?.["soap:Fault"];
|
||||
if (fault) {
|
||||
return {
|
||||
success: false,
|
||||
code: fault.faultcode || "SOAP_FAULT",
|
||||
message: fault.faultstring || "Unknown SOAP Fault",
|
||||
raw: xml
|
||||
};
|
||||
}
|
||||
|
||||
// STAR transport path: ProcessMessage/payload/content
|
||||
const processMessage = body?.ProcessMessage || body?.["ns0:ProcessMessage"] || body?.["ProcessMessageResponse"];
|
||||
|
||||
if (processMessage?.payload?.content) {
|
||||
const content = processMessage.payload.content;
|
||||
if (content && typeof content === "object") {
|
||||
const keys = Object.keys(content).filter((k) => k !== "@_id");
|
||||
const respKey = keys.find((k) => /Resp$/.test(k)) || (keys[0] === "ApplicationArea" && keys[1]) || keys[0];
|
||||
|
||||
const respNode = respKey ? content[respKey] : content;
|
||||
|
||||
const resultCode = respNode?.ResultCode || respNode?.ResponseCode || respNode?.StatusCode || "OK";
|
||||
const resultMessage = respNode?.ResultMessage || respNode?.ResponseMessage || respNode?.StatusMessage || null;
|
||||
|
||||
return {
|
||||
success: ["OK", "Success"].includes(String(resultCode)),
|
||||
code: resultCode,
|
||||
message: resultMessage,
|
||||
raw: xml,
|
||||
parsed: respNode
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: first element under Body (just in case)
|
||||
const keys = body && typeof body === "object" ? Object.keys(body) : [];
|
||||
const respNode = keys.length ? body[keys[0]] : body;
|
||||
|
||||
const resultCode = respNode?.ResultCode || respNode?.ResponseCode || "OK";
|
||||
const resultMessage = respNode?.ResultMessage || respNode?.ResponseMessage || null;
|
||||
|
||||
return {
|
||||
success: resultCode === "OK" || resultCode === "Success",
|
||||
code: resultCode,
|
||||
message: resultMessage,
|
||||
raw: xml,
|
||||
parsed: respNode
|
||||
};
|
||||
}
|
||||
|
||||
// ---------- STAR envelope helpers ----------
|
||||
function wrapWithApplicationArea(innerXml, { CreationDateTime, BODId, Sender, Destination }) {
|
||||
// Make sure we inject *inside* the STAR root, not before it.
|
||||
// 1) Strip any XML declaration just in case (idempotent)
|
||||
let xml = innerXml.replace(/^\s*<\?xml[^>]*\?>\s*/i, "");
|
||||
|
||||
const appArea = `
|
||||
<ApplicationArea>
|
||||
<CreationDateTime>${CreationDateTime}</CreationDateTime>
|
||||
<BODId>${BODId}</BODId>
|
||||
<Sender>
|
||||
${Sender?.Component ? `<Component>${Sender.Component}</Component>` : ""}
|
||||
${Sender?.Task ? `<Task>${Sender.Task}</Task>` : ""}
|
||||
${Sender?.ReferenceId ? `<ReferenceId>${Sender.ReferenceId}</ReferenceId>` : ""}
|
||||
</Sender>
|
||||
<Destination>
|
||||
<DestinationNameCode>RR</DestinationNameCode>
|
||||
${Destination?.DealerNumber ? `<DealerNumber>${Destination.DealerNumber}</DealerNumber>` : ""}
|
||||
${Destination?.StoreNumber ? `<StoreNumber>${Destination.StoreNumber}</StoreNumber>` : ""}
|
||||
${Destination?.AreaNumber ? `<AreaNumber>${Destination.AreaNumber}</AreaNumber>` : ""}
|
||||
</Destination>
|
||||
</ApplicationArea>`.trim();
|
||||
|
||||
// Inject right after the opening tag of the root element (skip processing instructions)
|
||||
// e.g. <rey_RomeGetAdvisorsReq ...> ==> insert ApplicationArea here
|
||||
xml = xml.replace(/^(\s*<[^!?][^>]*>)/, `$1\n${appArea}\n`);
|
||||
|
||||
return xml;
|
||||
}
|
||||
|
||||
async function buildStarEnvelope(innerBusinessXml, creds, appArea = {}) {
|
||||
const now = new Date().toISOString();
|
||||
const payloadWithAppArea = wrapWithApplicationArea(innerBusinessXml, {
|
||||
CreationDateTime: appArea.CreationDateTime || now,
|
||||
BODId: appArea.BODId || `BOD-${Date.now()}`,
|
||||
Sender: appArea.Sender || { Component: "Rome", Task: "SV", ReferenceId: "Update" },
|
||||
Destination: appArea.Destination || {
|
||||
DealerNumber: creds.dealerNumber,
|
||||
StoreNumber: String(creds.storeNumber ?? "").padStart(2, "0"),
|
||||
AreaNumber: String(creds.branchNumber || "01").padStart(2, "0")
|
||||
}
|
||||
});
|
||||
|
||||
return `<?xml version="1.0" encoding="utf-8"?>
|
||||
<soapenv:Envelope xmlns:soapenc="${RR_NS.SOAP_ENC}" xmlns:soapenv="${RR_NS.SOAP_ENV}" xmlns:xsd="${RR_NS.XSD}" xmlns:xsi="${RR_NS.XSI}">
|
||||
<soapenv:Header>
|
||||
${headerXml}
|
||||
<wsse:Security soapenv:mustUnderstand="1" xmlns:wsse="${RR_NS.WSSE}">
|
||||
<wsse:UsernameToken>
|
||||
<wsse:Username>${creds.username}</wsse:Username>
|
||||
<wsse:Password>${creds.password}</wsse:Password>
|
||||
</wsse:UsernameToken>
|
||||
</wsse:Security>
|
||||
</soapenv:Header>
|
||||
<soapenv:Body>
|
||||
${renderedBodyXml}
|
||||
<ProcessMessage xmlns="${RR_NS.STAR_TRANSPORT}">
|
||||
<payload xmlns:soap="${RR_NS.SOAP_ENV}" xmlns:xsi="${RR_NS.XSI}" xmlns:xsd="${RR_NS.XSD}" xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/03/addressing" xmlns:wsse="${RR_NS.WSSE}" xmlns:wsu="${RR_NS.WSU}" xmlns="${RR_NS.STAR_TRANSPORT}">
|
||||
<content id="content0">
|
||||
${payloadWithAppArea}
|
||||
</content>
|
||||
</payload>
|
||||
</ProcessMessage>
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>
|
||||
`.trim();
|
||||
</soapenv:Envelope>`;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------
|
||||
* Core SOAP caller
|
||||
* ----------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* 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 };
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs and sends a SOAP call to the Reynolds & Reynolds endpoint.
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
// ---------- Main transport (STAR only) ----------
|
||||
async function MakeRRCall({
|
||||
action,
|
||||
body,
|
||||
socket,
|
||||
// redisHelpers,
|
||||
jobid,
|
||||
dealerConfig,
|
||||
retries = 1
|
||||
dealerConfig, // optional per-shop overrides
|
||||
retries = 1,
|
||||
jobid
|
||||
}) {
|
||||
const correlationId = uuidv4();
|
||||
|
||||
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 || {});
|
||||
if (!action || !RR_ACTIONS[action]) {
|
||||
throw new Error(`Invalid RR action: ${action}`);
|
||||
}
|
||||
|
||||
// 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
|
||||
};
|
||||
const cfg = dealerConfig || (await resolveRRConfig(socket, undefined));
|
||||
const baseUrl = cfg.baseUrl;
|
||||
if (!baseUrl) throw new Error("Missing RR base URL");
|
||||
|
||||
// Build full SOAP envelope with proper header
|
||||
const soapEnvelope = await buildSoapEnvelopeWithHeader(renderedBody, headerVars);
|
||||
// Render STAR business body
|
||||
const templateName = body?.template || action;
|
||||
const renderedBusiness = await renderXmlTemplate(templateName, body?.data || {});
|
||||
|
||||
RRLogger(socket, "info", `RR → ${soapAction || "SOAP"} request`, {
|
||||
// Build STAR envelope
|
||||
let envelope = await buildStarEnvelope(renderedBusiness, cfg, body?.appArea);
|
||||
|
||||
const formattedEnvelope = prettyPrintXml(envelope);
|
||||
|
||||
// Guardrails
|
||||
if (!formattedEnvelope.includes("<ProcessMessage") || !formattedEnvelope.includes("<ApplicationArea>")) {
|
||||
throw new Error("STAR envelope malformed: missing ProcessMessage/ApplicationArea");
|
||||
}
|
||||
|
||||
const headers = { ...RR_SOAP_HEADERS, SOAPAction: RR_STAR_SOAP_ACTION };
|
||||
|
||||
RRLogger(socket, "debug", `Sending RR SOAP request`, {
|
||||
action,
|
||||
soapAction: RR_STAR_SOAP_ACTION,
|
||||
endpoint: baseUrl,
|
||||
jobid,
|
||||
url,
|
||||
correlationId
|
||||
mode: "STAR"
|
||||
});
|
||||
|
||||
const headers = {
|
||||
...RR_SOAP_HEADERS,
|
||||
SOAPAction: soapAction,
|
||||
"Content-Type": "text/xml; charset=utf-8",
|
||||
"X-Request-Id": correlationId
|
||||
};
|
||||
try {
|
||||
const { data: responseXml } = await axios.post(baseUrl, formattedEnvelope, {
|
||||
headers,
|
||||
timeout: cfg.timeout
|
||||
// Some RCI tenants require Basic in addition to WSSE
|
||||
// auth: { username: cfg.username, password: cfg.password }
|
||||
});
|
||||
|
||||
let attempt = 0;
|
||||
while (attempt <= retries) {
|
||||
attempt += 1;
|
||||
try {
|
||||
const response = await axios.post(url, soapEnvelope, {
|
||||
headers,
|
||||
timeout: effectiveConfig.timeout || 30000,
|
||||
responseType: "text",
|
||||
validateStatus: () => true
|
||||
const parsed = parseRRResponse(responseXml);
|
||||
|
||||
if (!parsed.success) {
|
||||
RRLogger(socket, "error", `RR ${action} failed`, {
|
||||
code: parsed.code,
|
||||
message: parsed.message
|
||||
});
|
||||
|
||||
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;
|
||||
throw new RrApiError(parsed.message || `RR ${action} failed`, parsed.code || "RR_ERROR");
|
||||
}
|
||||
|
||||
RRLogger(socket, "info", `RR ${action} success`, {
|
||||
result: parsed.code,
|
||||
message: parsed.message
|
||||
});
|
||||
|
||||
return responseXml;
|
||||
} catch (err) {
|
||||
if (retries > 0) {
|
||||
RRLogger(socket, "warn", `Retrying RR ${action} (${retries - 1} left)`, {
|
||||
error: err.message
|
||||
});
|
||||
return MakeRRCall({
|
||||
action,
|
||||
body,
|
||||
socket,
|
||||
dealerConfig: cfg,
|
||||
retries: retries - 1,
|
||||
jobid
|
||||
});
|
||||
}
|
||||
|
||||
RRLogger(socket, "error", `RR ${action} failed permanently`, { error: err.message });
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------
|
||||
* Exports
|
||||
* ----------------------------------------------------------------------------------------------*/
|
||||
|
||||
const RRActions = RR_ACTIONS;
|
||||
|
||||
module.exports = {
|
||||
MakeRRCall,
|
||||
getDealerConfig,
|
||||
renderXmlTemplate,
|
||||
resolveRRConfig,
|
||||
parseRRResponse,
|
||||
buildStarEnvelope,
|
||||
RRActions
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user