// server/rr/rrRoutes.js const express = require("express"); const router = express.Router(); const RRLogger = require("./rr-logger"); const { RrApiError } = require("./rr-error"); const { getRRConfigForBodyshop } = require("./rr-config"); const lookupApi = require("./rr-lookup"); const { insertCustomer, updateCustomer } = require("./rr-customer"); const { exportJobToRR } = require("./rr-job-export"); const { SelectedCustomer } = require("./rr-selected-customer"); const { QueryJobData } = require("./rr-job-helpers"); // --- helpers & middleware (kept local for this router) --- function socketOf(req) { // attach a minimal "socket-like" emitter for logger compatibility return { emit: () => { // }, handshake: { auth: { token: req?.headers?.authorization?.replace(/^Bearer\s+/i, "") } }, user: req?.user }; } function ok(res, payload) { return res.status(200).json(payload || { ok: true }); } function fail(res, e) { const code = e?.code === "BAD_REQUEST" ? 400 : e?.code === "NOT_CONFIGURED" ? 412 : 500; return res.status(code).json({ error: e?.message || String(e), code: e?.code || "RR_API_ERROR" }); } function requireBodyshopId(req) { const body = req?.body || {}; const fromBody = body.bodyshopId; const fromJob = body.job && (body.job.shopid || body.job.bodyshopId); const fromHeader = typeof req.get === "function" ? req.get("x-bodyshop-id") : undefined; const bodyshopId = fromBody || fromJob || fromHeader; if (!bodyshopId) { throw new RrApiError( "Missing bodyshopId (expected in body.bodyshopId, body.job.shopid/bodyshopId, or x-bodyshop-id header)", "BAD_REQUEST" ); } return bodyshopId; } // --- sanity/config checks --- router.get("/rr/config", async (req, res) => { try { const bodyshopId = requireBodyshopId(req); const cfg = await getRRConfigForBodyshop(bodyshopId); return ok(res, { data: cfg }); } catch (e) { return fail(res, e); } }); // --- lookups --- router.post("/rr/lookup/advisors", async (req, res) => { try { const bodyshopId = requireBodyshopId(req); const data = await lookupApi.getAdvisors({ bodyshopId, ...(req.body || {}) }); return ok(res, { data }); } catch (e) { return fail(res, e); } }); router.post("/rr/lookup/parts", async (req, res) => { try { const bodyshopId = requireBodyshopId(req); const data = await lookupApi.getParts({ bodyshopId, ...(req.body || {}) }); return ok(res, { data }); } catch (e) { return fail(res, e); } }); router.post("/rr/combined-search", async (req, res) => { try { const bodyshopId = requireBodyshopId(req); const data = await lookupApi.combinedSearch({ bodyshopId, ...(req.body || {}) }); return ok(res, { data }); } catch (e) { return fail(res, e); } }); // --- customers (basic insert/update) --- router.post("/rr/customer/insert", async (req, res) => { try { const bodyshopId = requireBodyshopId(req); const data = await insertCustomer({ bodyshopId, payload: req.body }); return ok(res, { data }); } catch (e) { return fail(res, e); } }); router.post("/rr/customer/update", async (req, res) => { try { const bodyshopId = requireBodyshopId(req); const data = await updateCustomer({ bodyshopId, payload: req.body }); return ok(res, { data }); } catch (e) { return fail(res, e); } }); /** * NEW: set or create the selected RR customer for a given job * body: { jobid: uuid, selectedCustomerId?: string, bodyshopId?: uuid } */ router.post("/rr/customer/selected", async (req, res) => { const socket = socketOf(req); const logger = (level, message, ctx) => RRLogger(socket)(level, message, ctx); try { const { jobid, selectedCustomerId } = req.body || {}; if (!jobid) throw new RrApiError("Missing 'jobid' in body", "BAD_REQUEST"); // We allow bodyshopId in the body, but will resolve from JobData if not present. const bodyshopId = req.body?.bodyshopId || null; const result = await SelectedCustomer({ socket, jobid, bodyshopId, selectedCustomerId, redisHelpers: req.redisHelpers }); logger("info", "RR /rr/customer/selected success", { jobid, selectedCustomerId: result.selectedCustomerId }); return ok(res, { data: result }); } catch (e) { RRLogger(socket)("error", "RR /rr/customer/selected failed", { error: e.message }); return fail(res, e); } }); /** * NEW: fetch canonical JobData used for DMS exports (mirrors Fortellis/CDK QueryJobData) * body: { jobid: uuid } */ router.post("/rr/job/query", async (req, res) => { try { const { jobid } = req.body || {}; if (!jobid) throw new RrApiError("Missing 'jobid' in body", "BAD_REQUEST"); const data = await QueryJobData({ socket: socketOf(req), jobid }); return ok(res, { data }); } catch (e) { return fail(res, e); } }); // --- export orchestrator --- router.post("/rr/export/job", async (req, res) => { const socket = socketOf(req); const logger = (level, message, ctx) => RRLogger(socket)(level, message, ctx); try { const bodyshopId = requireBodyshopId(req); const { job, options = {} } = req.body || {}; if (!job) throw new RrApiError("Missing 'job' in request body", "BAD_REQUEST"); const data = await exportJobToRR({ bodyshopId, job, logger, ...options }); return ok(res, { data }); } catch (e) { RRLogger(socket)("error", "RR /rr/export/job failed", { error: e.message }); return fail(res, e); } }); module.exports = router;