diff --git a/server/rr/rr-export-logs.test.js b/server/rr/rr-export-logs.test.js index 5178595cc..c32f24eee 100644 --- a/server/rr/rr-export-logs.test.js +++ b/server/rr/rr-export-logs.test.js @@ -88,6 +88,15 @@ describe("server/rr/rr-export-logs", () => { statusCode: "0", message: "Finalized" } + }, + metaExtra: { + rrPreview: { + provider: "rr", + previewFormat: "rr-rogog-preview.v1", + rogg: { + rows: [{ jobNo: "1", opCode: "BODY", custPrice: "125.00", dlrCost: "50.00" }] + } + } } }); @@ -106,7 +115,17 @@ describe("server/rr/rr-export-logs", () => { bodyshopid: "bodyshop-1", jobid: "job-1", successful: true, - useremail: "tech@example.com" + useremail: "tech@example.com", + metadata: { + provider: "rr", + rrPreview: { + provider: "rr", + previewFormat: "rr-rogog-preview.v1", + rogg: { + rows: [{ jobNo: "1", opCode: "BODY", custPrice: "125.00", dlrCost: "50.00" }] + } + } + } }, bill: { exported: true, diff --git a/server/rr/rr-job-export.js b/server/rr/rr-job-export.js index 4f8b9841e..bb26edbab 100644 --- a/server/rr/rr-job-export.js +++ b/server/rr/rr-job-export.js @@ -2,6 +2,7 @@ const { buildRRRepairOrderPayload, buildMinimalRolaborFromJob } = require("./rr- const { buildClientAndOpts } = require("./rr-lookup"); const CreateRRLogEvent = require("./rr-logger-event"); const { withRRRequestXml } = require("./rr-log-xml"); +const { buildRRPreviewMetadata } = require("./rr-preview-metadata"); const { extractRrResponsibilityCenters } = require("./rr-responsibility-centers"); const CdkCalculateAllocations = require("./rr-calculate-allocations").default; const { isEnhancedEarlyROEnabled, resolveRROpCodeFromBodyshop } = require("./rr-utils"); @@ -392,7 +393,7 @@ const updateRRRepairOrderWithFullData = async (args) => { success = String(roStatus.status).toUpperCase() === "SUCCESS"; } - return { + const exportResult = { success, data, roStatus, @@ -402,6 +403,8 @@ const updateRRRepairOrderWithFullData = async (args) => { roNo: String(roNo), xml: response?.xml }; + exportResult.rrPreview = buildRRPreviewMetadata({ payload, result: exportResult }); + return exportResult; }; /** @@ -533,7 +536,7 @@ const exportJobToRR = async (args) => { // Extract canonical roNo you'll need for finalize step const roNo = data?.dmsRoNo ?? data?.outsdRoNo ?? roStatus?.dmsRoNo ?? null; - return { + const exportResult = { success, data, roStatus, @@ -543,6 +546,8 @@ const exportJobToRR = async (args) => { roNo, xml: response?.xml // expose XML for logging/diagnostics }; + exportResult.rrPreview = buildRRPreviewMetadata({ payload, result: exportResult }); + return exportResult; }; /** diff --git a/server/rr/rr-preview-metadata.js b/server/rr/rr-preview-metadata.js new file mode 100644 index 000000000..4b4b2ec49 --- /dev/null +++ b/server/rr/rr-preview-metadata.js @@ -0,0 +1,85 @@ +const segmentLabelMap = { + partsTaxable: "Parts Taxable", + partsNonTaxable: "Parts Non-Taxable", + extrasTaxable: "Extras Taxable", + extrasNonTaxable: "Extras Non-Taxable", + laborTaxable: "Labor Taxable", + laborNonTaxable: "Labor Non-Taxable" +}; + +const toCentsFromAmountString = (value) => { + const parsed = Number.parseFloat(value || "0"); + return Number.isNaN(parsed) ? 0 : Math.round(parsed * 100); +}; + +const buildRoggRows = (rogg) => { + if (!rogg || !Array.isArray(rogg.ops)) return []; + + const rows = []; + + rogg.ops.forEach((op) => { + (op.lines || []).forEach((line, idx) => { + const segmentKind = op.segmentKind; + const segmentCount = op.segmentCount || 0; + const segmentLabel = segmentLabelMap[segmentKind] || segmentKind; + const itemDesc = + segmentCount > 1 && segmentLabel ? `${line.itemDesc} (${segmentLabel})` : line.itemDesc; + + rows.push({ + key: `${op.jobNo}-${idx}`, + opCode: op.opCode, + jobNo: op.jobNo, + breakOut: line.breakOut, + itemType: line.itemType, + itemDesc, + custQty: line.custQty, + custPayTypeFlag: line.custPayTypeFlag, + custTxblNtxblFlag: line.custTxblNtxblFlag, + custPrice: line.amount?.custPrice, + dlrCost: line.amount?.dlrCost, + segmentKind, + segmentCount + }); + }); + }); + + return rows; +}; + +const buildRoggTotals = (roggRows) => { + const totals = roggRows.reduce( + (acc, row) => { + acc.totalCustPriceCents += toCentsFromAmountString(row.custPrice); + acc.totalDlrCostCents += toCentsFromAmountString(row.dlrCost); + return acc; + }, + { totalCustPriceCents: 0, totalDlrCostCents: 0 } + ); + + return { + ...totals, + totalCustPrice: (totals.totalCustPriceCents / 100).toFixed(2), + totalDlrCost: (totals.totalDlrCostCents / 100).toFixed(2) + }; +}; + +const buildRRPreviewMetadata = ({ payload, result } = {}) => { + const rogg = payload?.rogg || null; + const roggRows = buildRoggRows(rogg); + + return { + provider: "rr", + previewFormat: "rr-rogog-preview.v1", + roNo: result?.roNo || payload?.roNo || null, + outsdRoNo: payload?.outsdRoNo || null, + rogg: { + raw: rogg, + rows: roggRows, + totals: buildRoggTotals(roggRows) + } + }; +}; + +module.exports = { + buildRRPreviewMetadata +}; diff --git a/server/rr/rr-preview-metadata.test.js b/server/rr/rr-preview-metadata.test.js new file mode 100644 index 000000000..a3f205573 --- /dev/null +++ b/server/rr/rr-preview-metadata.test.js @@ -0,0 +1,68 @@ +import { describe, expect, it } from "vitest"; +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +const { buildRRPreviewMetadata } = require("./rr-preview-metadata"); + +describe("server/rr/rr-preview-metadata", () => { + it("captures ROGOG preview rows and totals", () => { + const metadata = buildRRPreviewMetadata({ + payload: { + outsdRoNo: "RO-100", + rogg: { + ops: [ + { + opCode: "BODY", + jobNo: "1", + segmentKind: "laborTaxable", + segmentCount: 2, + lines: [ + { + breakOut: "B", + itemType: "LAB", + itemDesc: "Body Labor", + custQty: "1.0", + custPayTypeFlag: "C", + custTxblNtxblFlag: "T", + amount: { + custPrice: "125.00", + dlrCost: "50.00" + } + } + ] + } + ] + } + }, + result: { roNo: "12345" } + }); + + expect(metadata).toMatchObject({ + provider: "rr", + previewFormat: "rr-rogog-preview.v1", + roNo: "12345", + outsdRoNo: "RO-100", + rogg: { + rows: [ + { + opCode: "BODY", + jobNo: "1", + breakOut: "B", + itemType: "LAB", + itemDesc: "Body Labor (Labor Taxable)", + custTxblNtxblFlag: "T", + custPrice: "125.00", + dlrCost: "50.00" + } + ], + totals: { + totalCustPriceCents: 12500, + totalDlrCostCents: 5000, + totalCustPrice: "125.00", + totalDlrCost: "50.00" + } + } + }); + expect(metadata).not.toHaveProperty("rolabor"); + }); +}); diff --git a/server/rr/rr-register-socket-events.js b/server/rr/rr-register-socket-events.js index 378469322..d64a80404 100644 --- a/server/rr/rr-register-socket-events.js +++ b/server/rr/rr-register-socket-events.js @@ -1497,7 +1497,8 @@ const registerRREvents = ({ socket, redisHelpers }) => { dmsRoNo, customerNo: String(effectiveCustNo), advisorNo: String(advisorNo), - vin: job?.v_vin || null + vin: job?.v_vin || null, + rrPreview: result?.rrPreview || null }, defaultRRTTL ); @@ -1705,7 +1706,10 @@ const registerRREvents = ({ socket, redisHelpers }) => { jobId: rid, job, bodyshop, - result: finalizeResult + result: finalizeResult, + metaExtra: { + rrPreview: pending?.rrPreview || null + } }); // Clean pending key