feature/Reynolds-and-Reynolds-DMS-API-Integration -Expand
This commit is contained in:
@@ -1,14 +1,28 @@
|
||||
/**
|
||||
* RR (Reynolds & Reynolds) helper module
|
||||
* - Loads env (.env.{NODE_ENV})
|
||||
* - Provides token retrieval + simple Redis-backed caching
|
||||
* -----------------------------------------------------------------------------
|
||||
* 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
|
||||
* - Exports everything needed by rr-* feature files
|
||||
* - 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
|
||||
*/
|
||||
|
||||
const path = require("path");
|
||||
require("dotenv").config({
|
||||
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
||||
});
|
||||
|
||||
const uuid = require("uuid").v4;
|
||||
const AxiosLib = require("axios").default;
|
||||
const axios = AxiosLib.create();
|
||||
@@ -17,29 +31,71 @@ const axiosCurlirize = require("axios-curlirize").default;
|
||||
const logger = require("../utils/logger");
|
||||
const { RrApiError } = require("./rr-error");
|
||||
|
||||
// Optional curl logging (handy while scaffolding)
|
||||
// Emit curl equivalents for dev troubleshooting (safe to disable in prod)
|
||||
axiosCurlirize(axios, (result /*, err */) => {
|
||||
const { command } = result;
|
||||
// Disable or pipe to your logger if you prefer:
|
||||
// logger.log("rr-axios-curl", "DEBUG", "api", null, { command });
|
||||
// Keeping a console for local scaffolding/bring-up:
|
||||
console.log("*** rr axios (curl):", command);
|
||||
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";
|
||||
|
||||
/**
|
||||
* Simple provider-level token cache using existing session helpers.
|
||||
* We re-use setSessionData/getSessionData with a synthetic "socketId"
|
||||
* key (so we don't need pubClient here).
|
||||
* Transaction key namespace (mirrors Fortellis' getTransactionType)
|
||||
* Used to partition per-job Redis session hashes.
|
||||
*/
|
||||
const RR_PROVIDER_TOKEN_BUCKET = "rr:provider-token";
|
||||
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. Replace with the real auth call when available.
|
||||
* 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
|
||||
* @param {Object} deps.redisHelpers - Your redisHelpers API
|
||||
* @returns {Promise<string>} accessToken
|
||||
*/
|
||||
async function getRRToken({ redisHelpers }) {
|
||||
@@ -50,10 +106,9 @@ async function getRRToken({ redisHelpers }) {
|
||||
return cached.accessToken;
|
||||
}
|
||||
|
||||
// TODO: Implement real RR auth flow here.
|
||||
// Stub: use env var or a fixed dev token
|
||||
// TODO: Replace with real RR auth call. For now, fallback to env.
|
||||
const accessToken = process.env.RR_FAKE_TOKEN || "rr-dev-token";
|
||||
// Set an artificial 55-minute expiry (adjust to real value)
|
||||
// Artificial ~55m expiry (adjust to actual token TTL)
|
||||
const expiresAt = Date.now() + 55 * 60 * 1000;
|
||||
|
||||
await redisHelpers.setSessionData(
|
||||
@@ -69,41 +124,44 @@ async function getRRToken({ redisHelpers }) {
|
||||
message: error?.message,
|
||||
stack: error?.stack
|
||||
});
|
||||
// In absolute worst case, return a stub so dev environments keep moving
|
||||
// Keep local dev moving even if cache errors
|
||||
return process.env.RR_FAKE_TOKEN || "rr-dev-token";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 to append to URL as path (no leading slash needed)
|
||||
* @param {Array<[string,string]>} [args.requestSearchParams] - tuples of [key, value] for query
|
||||
* @param {string} [args.pathParams] - string appended to URL (no leading slash)
|
||||
* @param {Array<[string,string]>} [args.requestSearchParams] - pairs converted to query params
|
||||
* @returns {string}
|
||||
*/
|
||||
function constructFullUrl({ url, pathParams = "", requestSearchParams = [] }) {
|
||||
// normalize single trailing slash
|
||||
// normalize: ensure exactly one trailing slash on base
|
||||
url = url.replace(/\/+$/, "/");
|
||||
const fullPath = pathParams ? `${url}${pathParams}` : url;
|
||||
const searchParams = new URLSearchParams(requestSearchParams).toString();
|
||||
return searchParams ? `${fullPath}?${searchParams}` : fullPath;
|
||||
const query = new URLSearchParams(requestSearchParams).toString();
|
||||
return query ? `${fullPath}?${query}` : fullPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param {string} args.access_token
|
||||
* @param {string} args.reqId
|
||||
* @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>}
|
||||
*/
|
||||
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);
|
||||
await sleep((delayMeta?.checkStatusAfterSeconds || 2) * 1000);
|
||||
|
||||
const statusUrl = delayMeta?._links?.status?.href;
|
||||
if (!statusUrl) {
|
||||
@@ -137,7 +195,7 @@ function sleep(ms) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Core caller. Mirrors Fortellis' MakeFortellisCall shape so you can reuse flow.
|
||||
* Core caller. Mirrors Fortellis' MakeFortellisCall shape so we can reuse flow.
|
||||
*
|
||||
* @param {Object} args
|
||||
* @param {string} args.apiName - logical name (used in logs/errors)
|
||||
@@ -147,7 +205,7 @@ function sleep(ms) {
|
||||
* @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] added as query params
|
||||
* @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
|
||||
@@ -169,7 +227,6 @@ async function MakeRRCall({
|
||||
const fullUrl = constructFullUrl({ url, pathParams: requestPathParams, requestSearchParams });
|
||||
const reqId = uuid();
|
||||
const idempotencyKey = uuid();
|
||||
|
||||
const access_token = await getRRToken({ redisHelpers });
|
||||
|
||||
if (debug) {
|
||||
@@ -179,12 +236,11 @@ async function MakeRRCall({
|
||||
url: fullUrl,
|
||||
jobid,
|
||||
reqId,
|
||||
body
|
||||
body: safeLogJson(body)
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
let resp;
|
||||
const baseHeaders = {
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
"X-Request-Id": reqId,
|
||||
@@ -192,6 +248,7 @@ async function MakeRRCall({
|
||||
...headers
|
||||
};
|
||||
|
||||
let resp;
|
||||
switch ((type || "post").toLowerCase()) {
|
||||
case "get":
|
||||
resp = await axios.get(fullUrl, { headers: baseHeaders });
|
||||
@@ -200,6 +257,7 @@ async function MakeRRCall({
|
||||
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":
|
||||
@@ -268,9 +326,9 @@ async function MakeRRCall({
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep action registry centralized so upstream modules can import a single map.
|
||||
* Replace the base URLs with real RR/Rome endpoints as you finalize the integration.
|
||||
* You can also split this into per-domain registries once you know the layout.
|
||||
* 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
|
||||
@@ -312,7 +370,7 @@ const RRActions = {
|
||||
UpdateCustomer: {
|
||||
apiName: "RR Update Customer",
|
||||
url: isProduction
|
||||
? "https://rr.example.com/customer/v1/" // append /{id} if required
|
||||
? "https://rr.example.com/customer/v1/" // append /{id} if required by spec
|
||||
: "https://rr-uat.example.com/customer/v1/",
|
||||
type: "put"
|
||||
},
|
||||
@@ -323,11 +381,12 @@ const RRActions = {
|
||||
: "https://rr-uat.example.com/customer/v1/",
|
||||
type: "get"
|
||||
},
|
||||
QueryCustomerByName: {
|
||||
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)",
|
||||
@@ -336,12 +395,14 @@ const RRActions = {
|
||||
: "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",
|
||||
@@ -413,9 +474,15 @@ function safeLogJson(data) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
// core helpers
|
||||
MakeRRCall,
|
||||
RRActions,
|
||||
getRRToken,
|
||||
constructFullUrl,
|
||||
DelayedCallback
|
||||
DelayedCallback,
|
||||
|
||||
// parity exports required by other RR modules
|
||||
getTransactionType,
|
||||
defaultRRTTL,
|
||||
RRCacheEnums
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user