Files
bodyshop/server/web-sockets/redisSocketEvents.js

467 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 dont 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
};