feature/Reynolds-and-Reynolds-DMS-API-Integration -Expand

This commit is contained in:
Dave
2025-10-01 17:11:34 -04:00
parent 42027f0858
commit 24f017bfd2
11 changed files with 744 additions and 333 deletions

View File

@@ -1,4 +1,22 @@
const GraphQLClient = require("graphql-request").GraphQLClient;
// -----------------------------------------------------------------------------
// Reynolds & Reynolds (RR) Job Export flow (scaffold).
//
// Parity with Fortellis/CDK export shape so the UI + socket flows remain
// consistent:
//
// - RRJobExport: initial VIN/customer discovery & prompt for customer select
// - RRSelectedCustomer: create/update customer, insert/read vehicle,
// post WIP batch, post history, mark success/failure, notify client
//
// Whats still missing (fill in from Rome/RR PDFs you provided):
// - Exact request/response envelopes for each RR operation
// (Customer Insert/Update, Vehicle Insert/Read, WIP APIs, Service History).
// - Final success/error conditions for assertRrOk (we currently use heuristics).
// - Precise field mappings inside CreateCustomer, InsertVehicle,
// StartWip/TransBatchWip/PostBatchWip, InsertServiceVehicleHistory.
// -----------------------------------------------------------------------------
const { GraphQLClient } = require("graphql-request");
const moment = require("moment-timezone");
const CalculateAllocations = require("../cdk/cdk-calculate-allocations").default; // reuse allocations
@@ -6,13 +24,21 @@ const CreateRRLogEvent = require("./rr-logger");
const queries = require("../graphql-client/queries");
const { MakeRRCall, RRActions, getTransactionType, defaultRRTTL, RRCacheEnums } = require("./rr-helpers");
// --- Public entry points (similar to Fortellis)
// -----------------------------------------------------------------------------
// Public entry points (wired in redisSocketEvents.js)
// -----------------------------------------------------------------------------
/**
* Seed export: cache txEnvelope + JobData, discover VIN->VehicleId + owner,
* search by customer name, and prompt client to select/create a customer.
*/
async function RRJobExport({ socket, redisHelpers, txEnvelope, jobid }) {
const { setSessionTransactionData } = redisHelpers;
try {
CreateRRLogEvent(socket, "DEBUG", `[RR] Received Job export request`, { jobid });
// cache txEnvelope for this job session
await setSessionTransactionData(
socket.id,
getTransactionType(jobid),
@@ -31,10 +57,12 @@ async function RRJobExport({ socket, redisHelpers, txEnvelope, jobid }) {
let DMSVehCustomer;
if (!DMSVid?.newId) {
// existing vehicle, load details
const DMSVeh = await ReadVehicleById({ socket, redisHelpers, JobData, vehicleId: DMSVid.vehiclesVehId });
await setSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.DMSVeh, DMSVeh, defaultRRTTL);
const owner = DMSVeh?.owners && DMSVeh.owners.find((o) => o.id.assigningPartyId === "CURRENT");
// Try to read the CURRENT owner (shape TBD per RR)
const owner = DMSVeh?.owners && DMSVeh.owners.find((o) => o.id?.assigningPartyId === "CURRENT");
if (owner?.id?.value) {
DMSVehCustomer = await ReadCustomerById({ socket, redisHelpers, JobData, customerId: owner.id.value });
await setSessionTransactionData(
@@ -47,6 +75,7 @@ async function RRJobExport({ socket, redisHelpers, txEnvelope, jobid }) {
}
}
// Search customers by job owner name (param names TBD per RR)
const DMSCustList = await SearchCustomerByName({ socket, redisHelpers, JobData });
await setSessionTransactionData(
socket.id,
@@ -56,6 +85,7 @@ async function RRJobExport({ socket, redisHelpers, txEnvelope, jobid }) {
defaultRRTTL
);
// Emit choices: (VIN owner first if present) + search results
socket.emit("rr-select-customer", [
...(DMSVehCustomer ? [{ ...DMSVehCustomer, vinOwner: true }] : []),
...(Array.isArray(DMSCustList) ? DMSCustList : [])
@@ -65,6 +95,13 @@ async function RRJobExport({ socket, redisHelpers, txEnvelope, jobid }) {
}
}
/**
* After client selects a customer (or requests create):
* - Read or create the customer
* - Insert vehicle if needed (or read existing)
* - StartWip -> TransBatchWip -> PostBatchWip -> Mark exported
* - Optionally insert service history
*/
async function RRSelectedCustomer({ socket, redisHelpers, selectedCustomerId, jobid }) {
const { setSessionTransactionData, getSessionTransactionData } = redisHelpers;
@@ -81,6 +118,7 @@ async function RRSelectedCustomer({ socket, redisHelpers, selectedCustomerId, jo
const txEnvelope = await getSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.txEnvelope);
const DMSVid = await getSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.DMSVid);
// Ensure we have a customer to use
let DMSCust;
if (selectedCustomerId) {
DMSCust = await ReadCustomerById({ socket, redisHelpers, JobData, customerId: selectedCustomerId });
@@ -90,16 +128,17 @@ async function RRSelectedCustomer({ socket, redisHelpers, selectedCustomerId, jo
}
await setSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.DMSCust, DMSCust, defaultRRTTL);
// Ensure the vehicle exists (ownership model TBD per RR)
let DMSVeh;
if (DMSVid?.newId) {
DMSVeh = await InsertVehicle({ socket, redisHelpers, JobData, txEnvelope, DMSVid, DMSCust });
} else {
DMSVeh = await ReadVehicleById({ socket, redisHelpers, JobData, vehicleId: DMSVid.vehiclesVehId });
// TODO: implement UpdateVehicle if RR supports updating ownership
// DMSVeh = await UpdateVehicle({ ... })
// TODO: If RR supports “UpdateVehicle” to change ownership, add it here.
}
await setSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.DMSVeh, DMSVeh, defaultRRTTL);
// Start WIP header
const DMSTransHeader = await StartWip({ socket, redisHelpers, JobData, txEnvelope });
await setSessionTransactionData(
socket.id,
@@ -109,6 +148,7 @@ async function RRSelectedCustomer({ socket, redisHelpers, selectedCustomerId, jo
defaultRRTTL
);
// Post lines
const DMSBatchTxn = await TransBatchWip({ socket, redisHelpers, JobData });
await setSessionTransactionData(
socket.id,
@@ -118,7 +158,7 @@ async function RRSelectedCustomer({ socket, redisHelpers, selectedCustomerId, jo
defaultRRTTL
);
// decide success/err format later; keep parity with Fortellis shape
// Decide success from envelope (heuristic until exact spec confirmed)
if (String(DMSBatchTxn?.rtnCode || "0") === "0") {
const DmsBatchTxnPost = await PostBatchWip({ socket, redisHelpers, JobData });
await setSessionTransactionData(
@@ -132,7 +172,7 @@ async function RRSelectedCustomer({ socket, redisHelpers, selectedCustomerId, jo
if (String(DmsBatchTxnPost?.rtnCode || "0") === "0") {
await MarkJobExported({ socket, jobid: JobData.id, redisHelpers });
// Optional service history write
// Optional service history write (non-blocking)
try {
const DMSVehHistory = await InsertServiceVehicleHistory({ socket, redisHelpers, JobData });
await setSessionTransactionData(
@@ -168,7 +208,10 @@ async function RRSelectedCustomer({ socket, redisHelpers, selectedCustomerId, jo
}
}
// --- GraphQL job fetch
// -----------------------------------------------------------------------------
// GraphQL job fetch
// -----------------------------------------------------------------------------
async function QueryJobData({ socket, jobid }) {
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
const currentToken =
@@ -177,10 +220,14 @@ async function QueryJobData({ socket, jobid }) {
const result = await client
.setHeaders({ Authorization: `Bearer ${currentToken}` })
.request(queries.QUERY_JOBS_FOR_CDK_EXPORT, { id: jobid });
return result.jobs_by_pk;
}
// --- RR API step stubs (wire to MakeRRCall) -------------------------
// -----------------------------------------------------------------------------
// RR API step stubs (wire to MakeRRCall). Replace request payloads once the
// exact RR/Rome schemas are confirmed from the PDFs.
// -----------------------------------------------------------------------------
async function GetVehicleId({ socket, redisHelpers, JobData }) {
return await MakeRRCall({
@@ -213,17 +260,17 @@ async function ReadCustomerById({ socket, redisHelpers, JobData, customerId }) {
}
async function SearchCustomerByName({ socket, redisHelpers, JobData }) {
// align with Rome Search spec later
// TODO: Confirm exact query param names from the RR search spec
const ownerNameParams =
JobData.ownr_co_nm && JobData.ownr_co_nm.trim() !== ""
? [["lastName", JobData.ownr_co_nm]]
? [["lastName", JobData.ownr_co_nm]] // placeholder: business search
: [
["firstName", JobData.ownr_fn],
["lastName", JobData.ownr_ln]
];
return await MakeRRCall({
...RRActions.SearchCustomer,
...RRActions.QueryCustomerByName, // ✅ use action defined in rr-helpers
requestSearchParams: ownerNameParams,
redisHelpers,
socket,
@@ -232,10 +279,9 @@ async function SearchCustomerByName({ socket, redisHelpers, JobData }) {
}
async function CreateCustomer({ socket, redisHelpers, JobData }) {
// shape per Rome Customer Insert spec
// TODO: Replace with exact RR Customer Insert envelope & fields
const body = {
customerType: JobData.ownr_co_nm ? "BUSINESS" : "INDIVIDUAL"
// fill minimal required fields later
};
return await MakeRRCall({
...RRActions.CreateCustomer,
@@ -246,9 +292,9 @@ async function CreateCustomer({ socket, redisHelpers, JobData }) {
});
}
async function InsertVehicle({ socket, redisHelpers, JobData, txEnvelope, DMSVid, DMSCust }) {
async function InsertVehicle({ socket, redisHelpers, JobData /*, txEnvelope, DMSVid, DMSCust*/ }) {
// TODO: Replace with exact RR Service Vehicle Insert mapping
const body = {
// map fields per Rome Insert Service Vehicle spec
vin: JobData.v_vin
// owners, make/model, odometer, etc…
};
@@ -262,14 +308,15 @@ async function InsertVehicle({ socket, redisHelpers, JobData, txEnvelope, DMSVid
}
async function StartWip({ socket, redisHelpers, JobData, txEnvelope }) {
// TODO: Replace body fields with RR WIP header schema
const body = {
acctgDate: moment().tz(JobData.bodyshop.timezone).format("YYYY-MM-DD"),
desc: txEnvelope.story || "",
desc: txEnvelope?.story || "",
docType: "10",
m13Flag: "0",
refer: JobData.ro_number,
srcCo: JobData.bodyshop?.cdk_configuration?.srcco || "00", // placeholder
srcJrnl: txEnvelope.journal,
srcCo: JobData.bodyshop?.cdk_configuration?.srcco || "00", // placeholder from CDK config; RR equivalent TBD
srcJrnl: txEnvelope?.journal,
userID: "BSMS"
};
return await MakeRRCall({
@@ -283,9 +330,11 @@ async function StartWip({ socket, redisHelpers, JobData, txEnvelope }) {
async function TransBatchWip({ socket, redisHelpers, JobData }) {
const wips = await GenerateTransWips({ socket, redisHelpers, JobData });
// TODO: Ensure this body shape matches RR batch transaction schema
return await MakeRRCall({
...RRActions.TranBatchWip,
body: wips, // shape per Rome spec
body: wips,
redisHelpers,
socket,
jobid: JobData.id
@@ -299,6 +348,7 @@ async function PostBatchWip({ socket, redisHelpers, JobData }) {
RRCacheEnums.DMSTransHeader
);
// TODO: Confirm final field names for “post” operation in RR
const body = {
opCode: "P",
transID: DMSTransHeader?.transID
@@ -334,7 +384,10 @@ async function DeleteWip({ socket, redisHelpers, JobData }) {
getTransactionType(JobData.id),
RRCacheEnums.DMSTransHeader
);
// TODO: Confirm if RR uses the same endpoint with opCode=D to delete/void
const body = { opCode: "D", transID: DMSTransHeader?.transID };
return await MakeRRCall({
...RRActions.PostBatchWip,
body,
@@ -351,8 +404,8 @@ async function InsertServiceVehicleHistory({ socket, redisHelpers, JobData }) {
RRCacheEnums.txEnvelope
);
// TODO: Replace with RR Service Vehicle History schema
const body = {
// map to Rome “Service Vehicle History Insert” spec
comments: txEnvelope?.story || ""
};
return await MakeRRCall({
@@ -364,7 +417,7 @@ async function InsertServiceVehicleHistory({ socket, redisHelpers, JobData }) {
});
}
async function HandlePostingError({ socket, redisHelpers, JobData, DMSTransHeader }) {
async function HandlePostingError({ socket, redisHelpers, JobData /*, DMSTransHeader*/ }) {
const DmsError = await QueryErrWip({ socket, redisHelpers, JobData });
await DeleteWip({ socket, redisHelpers, JobData });
@@ -373,25 +426,30 @@ async function HandlePostingError({ socket, redisHelpers, JobData, DMSTransHeade
await InsertFailedExportLog({ socket, JobData, error: errString });
}
/**
* Convert app allocations to RR WIP lines.
* Re-uses existing CalculateAllocations to keep parity with CDK/Fortellis.
*
* TODO: Confirm exact RR posting model (accounts, control numbers, company ids,
* and whether amounts are signed or need separate debit/credit flags).
*/
async function GenerateTransWips({ socket, redisHelpers, JobData }) {
// reuse the existing allocator
const allocations = await CalculateAllocations(socket, JobData.id, true); // true==enable verbose logging
const allocations = await CalculateAllocations(socket, JobData.id, true); // true==verbose logging
const DMSTransHeader = await redisHelpers.getSessionTransactionData(
socket.id,
getTransactionType(JobData.id),
RRCacheEnums.DMSTransHeader
);
// Translate allocations -> RR WIP line shape later. For now: keep parity with Fortellis skeleton
const wips = [];
allocations.forEach((alloc) => {
if (alloc.sale.getAmount() > 0 && !alloc.tax) {
wips.push({
acct: alloc.profitCenter.dms_acctnumber,
cntl: alloc.profitCenter.dms_control_override || JobData.ro_number,
postAmt: alloc.sale.multiply(-1).getAmount(),
postAmt: alloc.sale.multiply(-1).getAmount(), // sale is a credit in many GLs; confirm RR sign
transID: DMSTransHeader?.transID,
trgtCoID: JobData.bodyshop?.cdk_configuration?.srcco
trgtCoID: JobData.bodyshop?.cdk_configuration?.srcco // RR equivalent TBD
});
}
if (alloc.cost.getAmount() > 0 && !alloc.tax) {
@@ -426,11 +484,12 @@ async function GenerateTransWips({ socket, redisHelpers, JobData }) {
getTransactionType(JobData.id),
RRCacheEnums.txEnvelope
);
txEnvelope?.payers?.forEach((payer) => {
wips.push({
acct: payer.dms_acctnumber,
cntl: payer.controlnumber,
postAmt: Math.round(payer.amount * 100),
postAmt: Math.round(payer.amount * 100), // assuming cents (confirm RR units)
transID: DMSTransHeader?.transID,
trgtCoID: JobData.bodyshop?.cdk_configuration?.srcco
});
@@ -446,28 +505,37 @@ async function GenerateTransWips({ socket, redisHelpers, JobData }) {
return wips;
}
// --- DB logging mirrors Fortellis
// -----------------------------------------------------------------------------
// DB logging mirrors Fortellis (status + export log)
// -----------------------------------------------------------------------------
async function MarkJobExported({ socket, jobid, redisHelpers }) {
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
const currentToken =
(socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token);
// Pull JobData from the session to get bodyshop info + default statuses
const JobData =
(await redisHelpers.getSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.JobData)) || {};
const transWips = await redisHelpers.getSessionTransactionData(
socket.id,
getTransactionType(jobid),
RRCacheEnums.transWips
);
return client.setHeaders({ Authorization: `Bearer ${currentToken}` }).request(queries.MARK_JOB_EXPORTED, {
jobId: jobid,
job: {
status: socket.JobData?.bodyshop?.md_ro_statuses?.default_exported || "Exported*",
status: JobData?.bodyshop?.md_ro_statuses?.default_exported || "Exported*",
date_exported: new Date()
},
log: {
bodyshopid: socket.JobData?.bodyshop?.id,
bodyshopid: JobData?.bodyshop?.id,
jobid,
successful: true,
useremail: socket.user?.email,
metadata: await redisHelpers.getSessionTransactionData(
socket.id,
getTransactionType(jobid),
RRCacheEnums.transWips
)
metadata: transWips
},
bill: { exported: true, exported_at: new Date() }
});
@@ -478,6 +546,7 @@ async function InsertFailedExportLog({ socket, JobData, error }) {
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
const currentToken =
(socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token);
return await client.setHeaders({ Authorization: `Bearer ${currentToken}` }).request(queries.INSERT_EXPORT_LOG, {
log: {
bodyshopid: JobData.bodyshop.id,