Files
bodyshop/server/rr/rrRoutes.js

174 lines
5.4 KiB
JavaScript

// 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;