467 lines
17 KiB
JavaScript
467 lines
17 KiB
JavaScript
const { admin } = require("../firebase/firebase-handler");
|
||
const FortellisLogger = require("../fortellis/fortellis-logger");
|
||
const RRLogger = require("../rr/rr-logger");
|
||
const { FortellisJobExport, FortellisSelectedCustomer } = require("../fortellis/fortellis");
|
||
const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
|
||
const { exportJobToRome } = require("../rr/rr-job-export");
|
||
const lookupApi = require("../rr/rr-lookup");
|
||
const RRCalculateAllocations = require("../rr/rr-calculate-allocations");
|
||
|
||
function resolveRRConfigFrom(payload = {}) {
|
||
// Back-compat: allow txEnvelope.config from old callers
|
||
const cfg = payload.config || payload.bodyshopConfig || payload.txEnvelope?.config || {};
|
||
return {
|
||
baseUrl: cfg.baseUrl || process.env.RR_BASE_URL,
|
||
username: cfg.username || process.env.RR_USERNAME,
|
||
password: cfg.password || process.env.RR_PASSWORD,
|
||
ppsysId: cfg.ppsysId || process.env.RR_PPSYSID,
|
||
dealer_number: cfg.dealer_number || process.env.RR_DEALER_NUMBER,
|
||
store_number: cfg.store_number || process.env.RR_STORE_NUMBER,
|
||
branch_number: cfg.branch_number || process.env.RR_BRANCH_NUMBER,
|
||
rrTransport: (cfg.rrTransport || process.env.RR_TRANSPORT || "STAR").toUpperCase()
|
||
};
|
||
}
|
||
|
||
const redisSocketEvents = ({
|
||
io,
|
||
redisHelpers: {
|
||
setSessionData,
|
||
getSessionData,
|
||
addUserSocketMapping,
|
||
removeUserSocketMapping,
|
||
refreshUserSocketTTL,
|
||
getUserSocketMappingByBodyshop,
|
||
setSessionTransactionData,
|
||
getSessionTransactionData,
|
||
clearSessionTransactionData
|
||
},
|
||
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom },
|
||
logger
|
||
}) => {
|
||
// Logging helper functions
|
||
const createLogEvent = (socket, level, message) => {
|
||
logger.log("ioredis-log-event", level, socket?.user?.email, null, { wsmessage: message });
|
||
};
|
||
|
||
// Socket Auth Middleware
|
||
const authMiddleware = async (socket, next) => {
|
||
const { token, bodyshopId } = socket.handshake.auth;
|
||
|
||
if (!token) {
|
||
return next(new Error("Authentication error - no authorization token."));
|
||
}
|
||
|
||
if (!bodyshopId) {
|
||
return next(new Error("Authentication error - no bodyshopId provided."));
|
||
}
|
||
|
||
try {
|
||
const user = await admin.auth().verifyIdToken(token);
|
||
socket.user = user;
|
||
socket.bodyshopId = bodyshopId;
|
||
socket.data = socket.data || {};
|
||
socket.data.authToken = token;
|
||
|
||
// Used to update legacy sockets
|
||
if (socket.handshake?.auth) {
|
||
socket.handshake.auth.token = token;
|
||
socket.handshake.auth.bodyshopId = bodyshopId;
|
||
}
|
||
await addUserSocketMapping(user.email, socket.id, bodyshopId);
|
||
next();
|
||
} catch (error) {
|
||
next(new Error(`Authentication error: ${error.message}`));
|
||
}
|
||
};
|
||
|
||
// Register Socket Events
|
||
const registerSocketEvents = (socket) => {
|
||
// Token Update Events
|
||
const registerUpdateEvents = (socket) => {
|
||
let latestTokenTimestamp = 0;
|
||
|
||
const updateToken = async ({ token, bodyshopId }) => {
|
||
const currentTimestamp = Date.now();
|
||
latestTokenTimestamp = currentTimestamp;
|
||
|
||
if (!token || !bodyshopId) {
|
||
socket.emit("token-updated", { success: false, error: "Token or bodyshopId missing" });
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const user = await admin.auth().verifyIdToken(token);
|
||
if (currentTimestamp < latestTokenTimestamp) {
|
||
createLogEvent(socket, "warn", "Outdated token validation skipped.");
|
||
return;
|
||
}
|
||
socket.user = user;
|
||
socket.bodyshopId = bodyshopId;
|
||
// 🔑 keep the live token in a mutable place used by downstream code
|
||
socket.data = socket.data || {};
|
||
socket.data.authToken = token;
|
||
// also keep handshake in sync for any legacy reads
|
||
if (socket.handshake?.auth) {
|
||
socket.handshake.auth.token = token;
|
||
socket.handshake.auth.bodyshopId = bodyshopId;
|
||
}
|
||
await refreshUserSocketTTL(user.email, bodyshopId);
|
||
socket.emit("token-updated", { success: true });
|
||
} catch (error) {
|
||
if (error.code === "auth/id-token-expired") {
|
||
createLogEvent(socket, "warn", "Stale token received, waiting for new token");
|
||
socket.emit("token-updated", { success: false, error: "Stale token." });
|
||
return;
|
||
}
|
||
createLogEvent(socket, "error", `Token update failed for socket ID: ${socket.id}, Error: ${error.message}`);
|
||
socket.emit("token-updated", { success: false, error: error.message });
|
||
socket.disconnect();
|
||
}
|
||
};
|
||
socket.on("update-token", updateToken);
|
||
};
|
||
|
||
// Room Broadcast Events
|
||
const registerRoomAndBroadcastEvents = (socket) => {
|
||
const joinBodyshopRoom = (bodyshopUUID) => {
|
||
try {
|
||
const room = getBodyshopRoom(bodyshopUUID);
|
||
socket.join(room);
|
||
} catch (error) {
|
||
createLogEvent(socket, "error", `Error joining room: ${error}`);
|
||
}
|
||
};
|
||
|
||
const leaveBodyshopRoom = (bodyshopUUID) => {
|
||
try {
|
||
const room = getBodyshopRoom(bodyshopUUID);
|
||
socket.leave(room);
|
||
} catch (error) {
|
||
createLogEvent(socket, "error", `Error joining room: ${error}`);
|
||
}
|
||
};
|
||
|
||
const broadcastToBodyshopRoom = (bodyshopUUID, message) => {
|
||
try {
|
||
const room = getBodyshopRoom(bodyshopUUID);
|
||
io.to(room).emit("bodyshop-message", message);
|
||
} catch (error) {
|
||
createLogEvent(socket, "error", `Error getting room: ${error}`);
|
||
}
|
||
};
|
||
|
||
socket.on("join-bodyshop-room", joinBodyshopRoom);
|
||
socket.on("leave-bodyshop-room", leaveBodyshopRoom);
|
||
socket.on("broadcast-to-bodyshop", broadcastToBodyshopRoom);
|
||
};
|
||
|
||
// Disconnect Events
|
||
const registerDisconnectEvents = (socket) => {
|
||
const disconnect = async () => {
|
||
if (socket.user?.email) {
|
||
await removeUserSocketMapping(socket.user.email, socket.id);
|
||
}
|
||
// Leave all rooms except the default room (socket.id)
|
||
const rooms = Array.from(socket.rooms).filter((room) => room !== socket.id);
|
||
for (const room of rooms) {
|
||
socket.leave(room);
|
||
}
|
||
};
|
||
|
||
socket.on("disconnect", disconnect);
|
||
};
|
||
|
||
// Messaging Events
|
||
const registerMessagingEvents = (socket) => {
|
||
const joinConversationRoom = async ({ bodyshopId, conversationId }) => {
|
||
try {
|
||
const room = getBodyshopConversationRoom({ bodyshopId, conversationId });
|
||
socket.join(room);
|
||
} catch (error) {
|
||
logger.log("Failed to Join Conversation Room", "error", "io-redis", null, {
|
||
bodyshopId,
|
||
conversationId,
|
||
error: error.message,
|
||
stack: error.stack
|
||
});
|
||
}
|
||
};
|
||
|
||
const leaveConversationRoom = ({ bodyshopId, conversationId }) => {
|
||
try {
|
||
const room = getBodyshopConversationRoom({ bodyshopId, conversationId });
|
||
socket.leave(room);
|
||
} catch (error) {
|
||
logger.log("Failed to Leave Conversation Room", "error", "io-redis", null, {
|
||
bodyshopId,
|
||
conversationId,
|
||
error: error.message,
|
||
stack: error.stack
|
||
});
|
||
}
|
||
};
|
||
|
||
const conversationModified = ({ bodyshopId, conversationId, ...fields }) => {
|
||
try {
|
||
// Retrieve the room name for the conversation
|
||
const room = getBodyshopRoom(bodyshopId);
|
||
// Emit the updated data to all clients in the room
|
||
io.to(room).emit("conversation-changed", {
|
||
conversationId,
|
||
...fields
|
||
});
|
||
} catch (error) {
|
||
logger.log("Failed to handle conversation modification", "error", "io-redis", null, {
|
||
bodyshopId,
|
||
conversationId,
|
||
fields,
|
||
error: error.message,
|
||
stack: error.stack
|
||
});
|
||
}
|
||
};
|
||
|
||
socket.on("conversation-modified", conversationModified);
|
||
socket.on("join-bodyshop-conversation", joinConversationRoom);
|
||
socket.on("leave-bodyshop-conversation", leaveConversationRoom);
|
||
};
|
||
|
||
// Sync Notification Read Events
|
||
const registerSyncEvents = (socket) => {
|
||
socket.on("sync-notification-read", async ({ email, bodyshopId, notificationId }) => {
|
||
try {
|
||
const socketMapping = await getUserSocketMappingByBodyshop(email, bodyshopId);
|
||
const timestamp = new Date().toISOString();
|
||
|
||
if (socketMapping?.socketIds) {
|
||
socketMapping?.socketIds.forEach((socketId) => {
|
||
if (socketId !== socket.id) {
|
||
// Avoid sending back to the originating socket
|
||
io.to(socketId).emit("sync-notification-read", { notificationId, timestamp });
|
||
}
|
||
});
|
||
}
|
||
} catch (error) {
|
||
createLogEvent(socket, "error", `Error syncing notification read: ${error.message}`);
|
||
}
|
||
});
|
||
|
||
socket.on("sync-all-notifications-read", async ({ email, bodyshopId }) => {
|
||
try {
|
||
const socketMapping = await getUserSocketMappingByBodyshop(email, bodyshopId);
|
||
const timestamp = new Date().toISOString();
|
||
|
||
if (socketMapping?.socketIds) {
|
||
socketMapping?.socketIds.forEach((socketId) => {
|
||
if (socketId !== socket.id) {
|
||
// Avoid sending back to the originating socket
|
||
io.to(socketId).emit("sync-all-notifications-read", { timestamp });
|
||
}
|
||
});
|
||
}
|
||
} catch (error) {
|
||
createLogEvent(socket, "error", `Error syncing all notifications read: ${error.message}`);
|
||
}
|
||
});
|
||
};
|
||
|
||
//Fortellis/CDK Handlers
|
||
const registerFortellisEvents = (socket) => {
|
||
socket.on("fortellis-export-job", async ({ jobid, txEnvelope }) => {
|
||
try {
|
||
await FortellisJobExport({
|
||
socket,
|
||
redisHelpers: {
|
||
setSessionData,
|
||
getSessionData,
|
||
addUserSocketMapping,
|
||
removeUserSocketMapping,
|
||
refreshUserSocketTTL,
|
||
getUserSocketMappingByBodyshop,
|
||
setSessionTransactionData,
|
||
getSessionTransactionData,
|
||
clearSessionTransactionData
|
||
},
|
||
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom },
|
||
jobid,
|
||
txEnvelope
|
||
});
|
||
} catch (error) {
|
||
FortellisLogger(socket, "error", `Error during Fortellis export : ${error.message}`);
|
||
logger.log("fortellis-job-export-error", "error", null, null, {
|
||
message: error.message,
|
||
stack: error.stack
|
||
});
|
||
}
|
||
});
|
||
socket.on("fortellis-selected-customer", async ({ jobid, selectedCustomerId }) => {
|
||
try {
|
||
await FortellisSelectedCustomer({
|
||
socket,
|
||
redisHelpers: {
|
||
setSessionData,
|
||
getSessionData,
|
||
addUserSocketMapping,
|
||
removeUserSocketMapping,
|
||
refreshUserSocketTTL,
|
||
getUserSocketMappingByBodyshop,
|
||
setSessionTransactionData,
|
||
getSessionTransactionData,
|
||
clearSessionTransactionData
|
||
},
|
||
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom },
|
||
jobid,
|
||
selectedCustomerId
|
||
});
|
||
} catch (error) {
|
||
FortellisLogger(socket, "error", `Error during Fortellis export : ${error.message}`);
|
||
logger.log("fortellis-selectd-customer-error", "error", null, null, {
|
||
message: error.message,
|
||
stack: error.stack
|
||
});
|
||
}
|
||
});
|
||
socket.on("fortellis-calculate-allocations", async (jobid, callback) => {
|
||
try {
|
||
const allocations = await CdkCalculateAllocations(socket, jobid);
|
||
callback(allocations);
|
||
} catch (error) {
|
||
FortellisLogger(socket, "error", `Error during Fortellis export : ${error.message}`);
|
||
logger.log("fortellis-selectd-customer-error", "error", null, null, {
|
||
message: error.message,
|
||
stack: error.stack
|
||
});
|
||
}
|
||
});
|
||
};
|
||
|
||
// Task Events
|
||
const registerTaskEvents = (socket) => {
|
||
socket.on("task-created", (payload) => {
|
||
if (!payload) return;
|
||
const room = getBodyshopRoom(socket.bodyshopId);
|
||
io.to(room).emit("bodyshop-message", { type: "task-created", payload });
|
||
});
|
||
|
||
socket.on("task-updated", (payload) => {
|
||
if (!payload) return;
|
||
const room = getBodyshopRoom(socket.bodyshopId);
|
||
io.to(room).emit("bodyshop-message", { type: "task-updated", payload });
|
||
});
|
||
|
||
socket.on("task-deleted", (payload) => {
|
||
if (!payload || !payload.id) return;
|
||
const room = getBodyshopRoom(socket.bodyshopId);
|
||
io.to(room).emit("bodyshop-message", { type: "task-deleted", payload });
|
||
});
|
||
};
|
||
|
||
const registerRREvents = (socket) => {
|
||
// Orchestrated Export (Customer → Vehicle → Repair Order)
|
||
socket.on("rr-export-job", async (payload = {}) => {
|
||
try {
|
||
// Back-compat: old callers: { jobid, txEnvelope }; new: { job, config, options }
|
||
// Prefer direct job/config, otherwise try txEnvelope.{job,config}
|
||
const job = payload.job || payload.txEnvelope?.job;
|
||
const options = payload.options || payload.txEnvelope?.options || {};
|
||
const cfg = resolveRRConfigFrom(payload);
|
||
|
||
if (!job) {
|
||
RRLogger(socket, "error", "RR export missing job payload");
|
||
return;
|
||
}
|
||
|
||
const result = await exportJobToRome(socket, job, cfg, options);
|
||
// Broadcast to bodyshop room for UI to pick up
|
||
const room = getBodyshopRoom(socket.bodyshopId);
|
||
io.to(room).emit("rr-export-job:result", { jobid: job.id, result });
|
||
} catch (error) {
|
||
RRLogger(socket, "error", `Error during RR export: ${error.message}`);
|
||
logger.log("rr-job-export-error", "error", null, null, { message: error.message, stack: error.stack });
|
||
}
|
||
});
|
||
|
||
// Combined search
|
||
socket.on("rr-lookup-combined", async ({ jobid, params } = {}, cb) => {
|
||
try {
|
||
const cfg = resolveRRConfigFrom({}); // if you want per-call overrides, pass them in the payload and merge here
|
||
const data = await lookupApi.combinedSearch(socket, params || {}, cfg);
|
||
cb?.(data);
|
||
} catch (e) {
|
||
RRLogger(socket, "error", `RR combined lookup error: ${e.message}`);
|
||
cb?.(null);
|
||
}
|
||
});
|
||
|
||
// Get Advisors
|
||
socket.on("rr-get-advisors", async ({ jobid, params } = {}, cb) => {
|
||
try {
|
||
const cfg = resolveRRConfigFrom({});
|
||
const data = await lookupApi.getAdvisors(socket, params || {}, cfg);
|
||
cb?.(data);
|
||
} catch (e) {
|
||
RRLogger(socket, "error", `RR get advisors error: ${e.message}`);
|
||
cb?.(null);
|
||
}
|
||
});
|
||
|
||
// Get Parts
|
||
socket.on("rr-get-parts", async ({ jobid, params } = {}, cb) => {
|
||
try {
|
||
const cfg = resolveRRConfigFrom({});
|
||
const data = await lookupApi.getParts(socket, params || {}, cfg);
|
||
cb?.(data);
|
||
} catch (e) {
|
||
RRLogger(socket, "error", `RR get parts error: ${e.message}`);
|
||
cb?.(null);
|
||
}
|
||
});
|
||
|
||
// (Optional) Selected customer — only keep this if you actually implement it for RR
|
||
socket.on("rr-selected-customer", async ({ jobid, selectedCustomerId } = {}) => {
|
||
try {
|
||
// If you don’t have an RRSelectedCustomer implementation now, either:
|
||
// 1) no-op with a log, or
|
||
// 2) emit a structured event UI can handle as "not supported".
|
||
RRLogger(socket, "info", "rr-selected-customer not implemented for RR (no-op)", {
|
||
jobid,
|
||
selectedCustomerId
|
||
});
|
||
// If later you add support, call your implementation here.
|
||
} catch (error) {
|
||
RRLogger(socket, "error", `Error during RR selected-customer: ${error.message}`);
|
||
logger.log("rr-selected-customer-error", "error", null, null, { message: error.message, stack: error.stack });
|
||
}
|
||
});
|
||
|
||
// Calculate allocations (unchanged — CDK utility)
|
||
socket.on("rr-calculate-allocations", async (jobid, callback) => {
|
||
try {
|
||
const allocations = await RRCalculateAllocations(socket, jobid);
|
||
callback(allocations);
|
||
} catch (error) {
|
||
RRLogger(socket, "error", `Error during RR calculate allocations: ${error.message}`);
|
||
logger.log("rr-calc-allocations-error", "error", null, null, { message: error.message, stack: error.stack });
|
||
}
|
||
});
|
||
};
|
||
// Call Handlers
|
||
registerRoomAndBroadcastEvents(socket);
|
||
registerUpdateEvents(socket);
|
||
registerMessagingEvents(socket);
|
||
registerDisconnectEvents(socket);
|
||
registerSyncEvents(socket);
|
||
registerTaskEvents(socket);
|
||
registerFortellisEvents(socket);
|
||
registerRREvents(socket);
|
||
};
|
||
|
||
// Associate Middleware and Handlers
|
||
io.use(authMiddleware);
|
||
io.on("connection", registerSocketEvents);
|
||
};
|
||
|
||
module.exports = {
|
||
redisSocketEvents
|
||
};
|