const path = require("path"); require("dotenv").config({ path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) }); // const CalcualteAllocations = require("../cdk/cdk-calculate-allocations").default; const CreateFortellisLogEvent = require("./fortellis-logger"); const logger = require("../utils/logger"); const uuid = require("uuid").v4; const AxiosLib = require("axios").default; const axios = AxiosLib.create(); const axiosCurlirize = require('axios-curlirize').default; // Custom error class for Fortellis API errors class FortellisApiError extends Error { constructor(message, details) { super(message); this.name = 'FortellisApiError'; this.reqId = details.reqId; this.url = details.url; this.apiName = details.apiName; this.errorData = details.errorData; this.errorStatus = details.errorStatus; this.errorStatusText = details.errorStatusText; this.originalError = details.originalError; } } axiosCurlirize(axios, (result, err) => { const { command } = result; console.log("*** ~ axiosCurlirize ~ command:", command); // if (err) { // use your logger here // } else { // } }); 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, null, 4 ) ); console.log("==========="); } //TODO: Verify how to select the correct department. 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 && departmentIds2[0] && 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, requestPathParams, requestSearchParams = [], //Array of key/value strings like [["key", "value"]] jobid, redisHelpers, socket, }) { const { setSessionTransactionData, getSessionTransactionData } = redisHelpers; const fullUrl = constructFullUrl({ url, pathParams: requestPathParams, requestSearchParams }); if (debug) logger.log(`Executing ${type} to ${fullUrl}`); 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(fullUrl, body, { headers: { Authorization: `Bearer ${access_token}`, "Subscription-Id": SubscriptionMeta.subscriptionId, "Request-Id": ReqId, ...DepartmentId && { "Department-Id": DepartmentId }, ...headers } }); break; case "get": result = await axios.get(fullUrl, { headers: { Authorization: `Bearer ${access_token}`, "Subscription-Id": SubscriptionMeta.subscriptionId, "Request-Id": ReqId, "Department-Id": DepartmentId, ...headers } }); break; case "put": result = await axios.put(fullUrl, body, { 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)); const errorDetails = { reqId: ReqId, url: fullUrl, apiName, errorData: error.response?.data, errorStatus: error.response?.status, errorStatusText: error.response?.statusText, originalError: error }; // CreateFortellisLogEvent(socket, "ERROR", `Error in MakeFortellisCall for ${apiName}: ${error.message}`, { // ...errorDetails, // errorStack: error.stack // }); // Throw custom error with all the details throw new FortellisApiError(`Fortellis API call failed for ${apiName}: ${error.message}`, errorDetails); } } //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" }, GetVehicleId: { url: isProduction ? "https://api.fortellis.io/cdk/drive/service-vehicle-mgmt/v2/vehicle-ids/" //Request path params of vins : "https://api.fortellis.io/cdk-test/drive/service-vehicle-mgmt/v2/vehicle-ids/", type: "get", apiName: "CDK Drive Post Service Vehicle", }, GetVehicleById: { url: isProduction ? "https://api.fortellis.io/cdk/drive/service-vehicle-mgmt/v2/" //Request path params of vehicleId : "https://api.fortellis.io/cdk-test/drive/service-vehicle-mgmt/v2/", type: "get", apiName: "CDK Drive Post Service Vehicle", }, QueryCustomerByName: { url: isProduction ? "https://api.fortellis.io/cdk/drive/customerpost/v1/search" : "https://api.fortellis.io/cdk-test/drive/customerpost/v1/search", type: "get", apiName: "CDK Drive Post Customer", }, ReadCustomer: { url: isProduction ? "https://api.fortellis.io/cdk/drive/customerpost/v1/" //Customer ID is request param. : "https://api.fortellis.io/cdk-test/drive/customerpost/v1/", type: "get", apiName: "CDK Drive Post Customer", }, CreateCustomer: { url: isProduction ? "https://api.fortellis.io/cdk/drive/customerpost/v1/" : "https://api.fortellis.io/cdk-test/drive/customerpost/v1/", type: "post", apiName: "CDK Drive Post Customer", }, InsertVehicle: { url: isProduction ? "https://api.fortellis.io/cdk/drive/service-vehicle-mgmt/v2/" : "https://api.fortellis.io/cdk-test/drive/service-vehicle-mgmt/v2/", type: "post", apiName: "CDK Drive Post Service Vehicle", }, UpdateVehicle: { url: isProduction ? "https://api.fortellis.io/cdk/drive/service-vehicle-mgmt/v2/" : "https://api.fortellis.io/cdk-test/drive/service-vehicle-mgmt/v2/", type: "put", apiName: "CDK Drive Post Service Vehicle", }, GetCOA: { type: "get", apiName: "CDK Drive Post Accounts GL WIP", url: `https://api.fortellis.io/cdk-test/drive/chartofaccounts/v2/bulk/`, waitForResult: true }, StartWip: { url: isProduction ? "https://api.fortellis.io/cdk/drive/glpost/startWIP" : "https://api.fortellis.io/cdk-test/drive/glpost/startWIP", type: "post", apiName: "CDK Drive Post Accounting GL", }, TranBatchWip: { url: isProduction ? "https://api.fortellis.io/cdk/drive/glpost/transBatchWIP" : "https://api.fortellis.io/cdk-test/drive/glpost/transBatchWIP", type: "post", apiName: "CDK Drive Post Accounting GL", }, PostBatchWip: { url: isProduction ? "https://api.fortellis.io/cdk/drive/glpost/postBatchWIP" : "https://api.fortellis.io/cdk-test/drive/glpost/postBatchWIP", type: "post", apiName: "CDK Drive Post Accounting GL", }, QueryErrorWip: { url: isProduction ? "https://api.fortellis.io/cdk/drive/glpost/errWIP" : "https://api.fortellis.io/cdk-test/drive/glpost/errWIP", type: "get", apiName: "CDK Drive Post Accounting GL", }, ServiceHistoryInsert: { url: isProduction ? "https://api.fortellis.io/cdk/drive/post/service-vehicle-history-mgmt/v2/" : "https://api.fortellis.io/cdk-test/drive/post/service-vehicle-history-mgmt/v2/", type: "post", apiName: "CDK Drive Post Service Vehicle History", }, }; const FortellisCacheEnums = { txEnvelope: "txEnvelope", DMSBatchTxn: "DMSBatchTxn", SubscriptionMeta: "SubscriptionMeta", DepartmentId: "DepartmentId", JobData: "JobData", DMSVid: "DMSVid", DMSVeh: "DMSVeh", DMSVehCustomer: "DMSVehCustomer", DMSCustList: "DMSCustList", DMSCust: "DMSCust", selectedCustomerId: "selectedCustomerId", DMSTransHeader: "DMSTransHeader", transWips: "transWips", DmsBatchTxnPost: "DmsBatchTxnPost", DMSVehHistory: "DMSVehHistory", }; function constructFullUrl({ url, pathParams = "", requestSearchParams = [] }) { // Ensure the base URL ends with a single "/" url = url.replace(/\/+$/, "/"); const fullPath = pathParams ? `${url}${pathParams}` : url; const searchParams = new URLSearchParams(requestSearchParams).toString(); const fullUrl = searchParams ? `${fullPath}?${searchParams}` : fullPath; return fullUrl; } module.exports = { GetAuthToken, FortellisCacheEnums, MakeFortellisCall, FortellisActions, getTransactionType, defaultFortellisTTL, FortellisApiError };