IO-2776 Add additional redis helpers, restructure some fortellis calls.
This commit is contained in:
253
server/fortellis/fortellis-helpers.js
Normal file
253
server/fortellis/fortellis-helpers.js
Normal file
@@ -0,0 +1,253 @@
|
||||
const path = require("path");
|
||||
require("dotenv").config({
|
||||
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
||||
});
|
||||
|
||||
const GraphQLClient = require("graphql-request").GraphQLClient;
|
||||
// const CalcualteAllocations = require("../cdk/cdk-calculate-allocations").default;
|
||||
const InstanceMgr = require("../utils/instanceMgr").default;
|
||||
const CreateFortellisLogEvent = require("./fortellis-logger");
|
||||
const queries = require("../graphql-client/queries");
|
||||
const logger = require("../utils/logger");
|
||||
const uuid = require("uuid").v4;
|
||||
const AxiosLib = require("axios").default;
|
||||
const axios = AxiosLib.create();
|
||||
|
||||
const getTransactionType = (jobid) => `fortellis:${jobid}`;
|
||||
const defaultFortellisTTL = 60 * 60;
|
||||
|
||||
async function GetAuthToken() {
|
||||
//Done with Authorization Code Flow
|
||||
//https://docs.fortellis.io/docs/tutorials/solution-integration/authorization-code-flow/
|
||||
|
||||
//TODO: This should get stored in the redis cache and only be refreshed when it expires.
|
||||
const {
|
||||
data: { access_token, expires_in, token_type }
|
||||
} = await axios.post(
|
||||
process.env.FORTELLIS_AUTH_URL,
|
||||
{},
|
||||
{
|
||||
auth: {
|
||||
username: process.env.FORTELLIS_KEY,
|
||||
password: process.env.FORTELLIS_SECRET
|
||||
},
|
||||
params: {
|
||||
grant_type: "client_credentials",
|
||||
scope: "anonymous"
|
||||
}
|
||||
}
|
||||
);
|
||||
return access_token;
|
||||
}
|
||||
|
||||
async function FetchSubscriptions({ redisHelpers, socket, jobid }) {
|
||||
try {
|
||||
const { setSessionTransactionData, getSessionTransactionData } = redisHelpers;
|
||||
|
||||
//Get Subscription ID from Transaction Envelope
|
||||
const { SubscriptionID } = await getSessionTransactionData(socket.id, getTransactionType(jobid), `txEnvelope`);
|
||||
if (!SubscriptionID) {
|
||||
throw new Error("Subscription ID not found in transaction envelope.");
|
||||
}
|
||||
|
||||
//Check to See if the subscription meta is in the Redis Cache.
|
||||
const SubscriptionMetaFromCache = await getSessionTransactionData(
|
||||
socket.id,
|
||||
getTransactionType(jobid),
|
||||
FortellisCacheEnums.SubscriptionMeta
|
||||
);
|
||||
|
||||
// If it is, return it.
|
||||
if (SubscriptionMetaFromCache) {
|
||||
return SubscriptionMetaFromCache;
|
||||
} else {
|
||||
const access_token = await GetAuthToken();
|
||||
const subscriptions = await axios.get(`https://subscriptions.fortellis.io/v1/solution/subscriptions`, {
|
||||
headers: { Authorization: `Bearer ${access_token}` }
|
||||
});
|
||||
const SubscriptionMeta = subscriptions.data.subscriptions.find((s) => s.subscriptionId === SubscriptionID);
|
||||
await setSessionTransactionData(
|
||||
socket.id,
|
||||
getTransactionType(jobid),
|
||||
FortellisCacheEnums.SubscriptionMeta,
|
||||
SubscriptionMeta,
|
||||
defaultFortellisTTL
|
||||
);
|
||||
return SubscriptionMeta;
|
||||
}
|
||||
} catch (error) {
|
||||
CreateFortellisLogEvent(socket, "ERROR", `Error fetching subscription metadata`, {
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function GetDepartmentId({ apiName, debug = false, SubscriptionMeta }) {
|
||||
if (!apiName) throw new Error("apiName not provided. Unable to get department without apiName.");
|
||||
if (debug) {
|
||||
console.log("API Names & Departments ");
|
||||
console.log("===========");
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
SubscriptionMeta.apiDmsInfo.map((a) => ({
|
||||
name: a.name,
|
||||
departments: a.departments.map((d) => d.id)
|
||||
})),
|
||||
null,
|
||||
4
|
||||
)
|
||||
);
|
||||
console.log("===========");
|
||||
}
|
||||
const departmentIds2 = SubscriptionMeta.apiDmsInfo //Get the subscription object.
|
||||
.find((info) => info.name === apiName)?.departments; //Departments are categorized by API name and have an array of departments.
|
||||
|
||||
return departmentIds2[0].id; //TODO: This makes the assumption that there is only 1 department.
|
||||
}
|
||||
|
||||
//Highest level function call to make a call to fortellis. This should be the only call required, and it will handle all the logic for making the call.
|
||||
async function MakeFortellisCall({
|
||||
apiName,
|
||||
url,
|
||||
headers = {},
|
||||
body = {},
|
||||
type = "post",
|
||||
debug = true,
|
||||
jobid,
|
||||
redisHelpers,
|
||||
socket
|
||||
}) {
|
||||
const { setSessionTransactionData, getSessionTransactionData } = redisHelpers;
|
||||
|
||||
if (debug) logger.log(`Executing ${type} to ${url}`);
|
||||
const ReqId = uuid();
|
||||
const access_token = await GetAuthToken();
|
||||
const SubscriptionMeta = await FetchSubscriptions({ redisHelpers, socket, jobid });
|
||||
const DepartmentId = await GetDepartmentId({ apiName, debug, SubscriptionMeta });
|
||||
|
||||
if (debug) {
|
||||
console.log(
|
||||
`ReqID: ${ReqId} | SubscriptionID: ${SubscriptionMeta.subscriptionId} | DepartmentId: ${DepartmentId}`
|
||||
);
|
||||
console.log(`Body Contents: ${JSON.stringify(body, null, 4)}`);
|
||||
}
|
||||
|
||||
try {
|
||||
let result;
|
||||
switch (type) {
|
||||
case "post":
|
||||
default:
|
||||
result = await axios.post(url, body, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
"Subscription-Id": SubscriptionMeta.subscriptionId,
|
||||
"Request-Id": ReqId,
|
||||
"Department-Id": DepartmentId,
|
||||
...headers
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "get":
|
||||
result = await axios.get(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
"Subscription-Id": SubscriptionMeta.subscriptionId,
|
||||
"Request-Id": ReqId,
|
||||
"Department-Id": DepartmentId,
|
||||
...headers
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
console.log(`ReqID: ${ReqId} Data`);
|
||||
console.log(JSON.stringify(result.data, null, 4));
|
||||
}
|
||||
|
||||
if (result.data.checkStatusAfterSeconds) {
|
||||
return DelayedCallback({
|
||||
delayMeta: result.data,
|
||||
access_token,
|
||||
SubscriptionID: SubscriptionMeta.subscriptionId,
|
||||
ReqId,
|
||||
departmentIds: DepartmentId
|
||||
});
|
||||
}
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
console.log(`ReqID: ${ReqId} Error`, error.response?.data);
|
||||
//console.log(`ReqID: ${ReqId} Full Error`, JSON.stringify(error, null, 4));
|
||||
}
|
||||
}
|
||||
|
||||
//Some Fortellis calls return a batch result that isn't ready immediately.
|
||||
//This function will check the status of the call and wait until it is ready.
|
||||
//It will try 5 times before giving up.
|
||||
async function DelayedCallback({ delayMeta, access_token, SubscriptionID, ReqId, departmentIds }) {
|
||||
for (let index = 0; index < 5; index++) {
|
||||
await sleep(delayMeta.checkStatusAfterSeconds * 1000);
|
||||
//Check to see if the call is ready.
|
||||
const statusResult = await axios.get(delayMeta._links.status.href, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
"Subscription-Id": SubscriptionID,
|
||||
"Request-Id": ReqId,
|
||||
"Department-Id": departmentIds[0].id
|
||||
}
|
||||
});
|
||||
|
||||
//TODO: Add a check if the status result is not ready, to try again.
|
||||
if (statusResult.data.status === "complete") {
|
||||
//This may have to check again if it isn't ready.
|
||||
const batchResult = await axios.get(statusResult.data._links.result.href, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
"Subscription-Id": SubscriptionID,
|
||||
"Request-Id": ReqId
|
||||
//"Department-Id": departmentIds[0].id
|
||||
}
|
||||
});
|
||||
return batchResult;
|
||||
} else {
|
||||
return "Error!!! Still need to implement batch waiting.";
|
||||
}
|
||||
}
|
||||
}
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
|
||||
const FortellisActions = {
|
||||
QueryVehicles: {
|
||||
url: isProduction
|
||||
? "https://api.fortellis.io/cdkdrive/service/v1/vehicles/"
|
||||
: "https://api.fortellis.io/cdk-test/cdkdrive/service/v1/vehicles/",
|
||||
type: "get",
|
||||
apiName: "Service Vehicle - Query Vehicles"
|
||||
},
|
||||
GetCOA: {
|
||||
type: "get",
|
||||
apiName: "CDK Drive Post Accounts GL WIP",
|
||||
url: `https://api.fortellis.io/cdk-test/drive/chartofaccounts/v2/bulk`,
|
||||
waitForResult: true
|
||||
}
|
||||
};
|
||||
|
||||
const FortellisCacheEnums = {
|
||||
txEnvelope: "txEnvelope",
|
||||
SubscriptionMeta: "SubscriptionMeta",
|
||||
DepartmentId: "DepartmentId"
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
GetAuthToken,
|
||||
FortellisCacheEnums,
|
||||
MakeFortellisCall,
|
||||
FortellisActions,
|
||||
getTransactionType,
|
||||
defaultFortellisTTL
|
||||
};
|
||||
9
server/fortellis/fortellis-logger.js
Normal file
9
server/fortellis/fortellis-logger.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const logger = require("../utils/logger");
|
||||
|
||||
const CreateFortellisLogEvent = (socket, level, message, txnDetails) => {
|
||||
//TODO: Add detaisl to track the whole transaction between Fortellis and the server.
|
||||
logger.log("fortellis-log-event", level, socket?.user?.email, null, { wsmessage: message, txnDetails });
|
||||
socket.emit("fortellis-log-event", { level, message, txnDetails });
|
||||
};
|
||||
|
||||
module.exports = CreateFortellisLogEvent;
|
||||
1047
server/fortellis/fortellis.js
Normal file
1047
server/fortellis/fortellis.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user