174 lines
5.4 KiB
JavaScript
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;
|