Checkpoint
This commit is contained in:
@@ -1,14 +1,30 @@
|
||||
// server/rr/rr-job-exports.js
|
||||
|
||||
const { buildRRRepairOrderPayload } = require("./rr-job-helpers");
|
||||
const { buildClientAndOpts } = require("./rr-lookup");
|
||||
const CreateRRLogEvent = require("./rr-logger-event");
|
||||
const { extractRrResponsibilityCenters } = require("./rr-responsibility-centers");
|
||||
const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
|
||||
|
||||
/**
|
||||
* Step 1: Export a job to Reynolds & Reynolds as a *new* Repair Order.
|
||||
*
|
||||
* This is the "create" phase only:
|
||||
* - We always call createRepairOrder
|
||||
* - We always call client.createRepairOrder(payload, opts)
|
||||
* - Any follow-up / finalUpdate is handled by finalizeRRRepairOrder
|
||||
*
|
||||
* When in RR mode (bodyshop.rr_dealerid truthy), we also:
|
||||
* - Extract responsibility center config (for logging / debugging)
|
||||
* - Run CdkCalculateAllocations to produce the allocations array
|
||||
* (with profitCenter/costCenter + rr_gogcode / rr_item_type / rr_cust_txbl_flag)
|
||||
* - Derive a default RR OpCode + TaxCode (if configured)
|
||||
* - Pass bodyshop, allocations, opCode, taxCode into buildRRRepairOrderPayload
|
||||
* so it can create the payload fields used by reynolds-rome-client:
|
||||
* - header fields (CustNo, AdvNo, DeptType, Vin, etc.)
|
||||
* - RO.rolabor (ops[])
|
||||
* - RO.rogg (ops[])
|
||||
* - RO.tax (TaxCodeInfo)
|
||||
*
|
||||
* @param args
|
||||
* @returns {Promise<{success, data: *, roStatus: *, statusBlocks, customerNo: string, svId: string|null, roNo: *}>}
|
||||
*/
|
||||
@@ -43,13 +59,96 @@ const exportJobToRR = async (args) => {
|
||||
const story = txEnvelope?.story ? String(txEnvelope.story).trim() : null;
|
||||
const makeOverride = txEnvelope?.makeOverride ? String(txEnvelope.makeOverride).trim() : null;
|
||||
|
||||
// Build RO payload for create
|
||||
// RR-only extras
|
||||
let rrCentersConfig = null;
|
||||
let allocations = null;
|
||||
let opCode = null;
|
||||
let taxCode = null;
|
||||
|
||||
if (bodyshop.rr_dealerid) {
|
||||
// 1) Responsibility center config (for visibility / debugging)
|
||||
try {
|
||||
rrCentersConfig = extractRrResponsibilityCenters(bodyshop);
|
||||
|
||||
CreateRRLogEvent(socket, "SILLY", "RR responsibility centers resolved", {
|
||||
hasCenters: !!bodyshop.md_responsibility_centers,
|
||||
profitCenters: Object.keys(rrCentersConfig?.profitsByName || {}),
|
||||
costCenters: Object.keys(rrCentersConfig?.costsByName || {}),
|
||||
dmsCostDefaults: rrCentersConfig?.dmsCostDefaults || {},
|
||||
dmsProfitDefaults: rrCentersConfig?.dmsProfitDefaults || {}
|
||||
});
|
||||
} catch (e) {
|
||||
CreateRRLogEvent(socket, "ERROR", "Failed to resolve RR responsibility centers", {
|
||||
message: e?.message,
|
||||
stack: e?.stack
|
||||
});
|
||||
}
|
||||
|
||||
// 2) Allocations (sales + cost by center, with rr_* metadata already attached)
|
||||
try {
|
||||
allocations = await CdkCalculateAllocations(socket, job.id);
|
||||
|
||||
CreateRRLogEvent(socket, "SILLY", "RR allocations resolved", {
|
||||
hasAllocations: Array.isArray(allocations),
|
||||
count: Array.isArray(allocations) ? allocations.length : 0
|
||||
});
|
||||
} catch (e) {
|
||||
CreateRRLogEvent(socket, "ERROR", "Failed to calculate RR allocations", {
|
||||
message: e?.message,
|
||||
stack: e?.stack
|
||||
});
|
||||
allocations = null; // We still proceed with a header-only RO if this fails.
|
||||
}
|
||||
|
||||
// 3) OpCode (global, but overridable)
|
||||
// - baseOpCode can come from bodyshop or rrCentersConfig (you'll map it in onboarding)
|
||||
// - txEnvelope can carry an explicit override field (opCode/opcode/op_code)
|
||||
const baseOpCode =
|
||||
bodyshop.rr_default_opcode ||
|
||||
bodyshop.rr_opcode ||
|
||||
rrCentersConfig?.defaultOpCode ||
|
||||
rrCentersConfig?.rrDefaultOpCode ||
|
||||
"51DOZ"; // TODO Change / implement default handling policy
|
||||
|
||||
const opCodeOverride = txEnvelope?.opCode || txEnvelope?.opcode || txEnvelope?.op_code || null;
|
||||
|
||||
if (opCodeOverride || baseOpCode) {
|
||||
opCode = String(opCodeOverride || baseOpCode).trim() || null;
|
||||
}
|
||||
|
||||
// 4) TaxCode (for header-level tax, e.g. state/prov tax)
|
||||
const baseTaxCode =
|
||||
bodyshop.rr_default_taxcode ||
|
||||
bodyshop.rr_tax_code ||
|
||||
rrCentersConfig?.defaultTaxCode ||
|
||||
rrCentersConfig?.rrDefaultTaxCode ||
|
||||
"TEST"; // TODO Change / implement default handling policy
|
||||
|
||||
if (baseTaxCode) {
|
||||
taxCode = String(baseTaxCode).trim() || null;
|
||||
}
|
||||
|
||||
CreateRRLogEvent(socket, "SILLY", "RR op/tax config resolved", {
|
||||
opCode,
|
||||
taxCode
|
||||
});
|
||||
}
|
||||
|
||||
// Build RO payload for create.
|
||||
//
|
||||
// NOTE:
|
||||
// - bodyshop + allocations + opCode + taxCode are used only to build the
|
||||
// value object expected by reynolds-rome-client (header + rogg + rolabor + tax).
|
||||
const payload = buildRRRepairOrderPayload({
|
||||
bodyshop,
|
||||
job,
|
||||
selectedCustomer: { customerNo: String(selected), custNo: String(selected) },
|
||||
advisorNo: String(advisorNo),
|
||||
story,
|
||||
makeOverride
|
||||
makeOverride,
|
||||
allocations,
|
||||
opCode,
|
||||
taxCode
|
||||
});
|
||||
|
||||
const response = await client.createRepairOrder(payload, finalOpts);
|
||||
@@ -81,6 +180,9 @@ const exportJobToRR = async (args) => {
|
||||
* Step 2: Finalize an RR Repair Order by sending finalUpdate: "Y".
|
||||
* This is the *update* phase.
|
||||
*
|
||||
* We intentionally do NOT send Rogog/Rolabor here — all of that is pushed on
|
||||
* create; finalize is just a header-level update (FinalUpdate + estimateType).
|
||||
*
|
||||
* @param args
|
||||
* @returns {Promise<{success, data: *, roStatus: *, statusBlocks}>}
|
||||
*/
|
||||
@@ -94,7 +196,9 @@ const finalizeRRRepairOrder = async (args) => {
|
||||
|
||||
// The external (Outsd) RO is our deterministic fallback and correlation id.
|
||||
const externalRo = job?.ro_number ?? job?.id;
|
||||
if (externalRo == null) throw new Error("finalizeRRRepairOrder: outsdRoNo (job.ro_number/id) is required");
|
||||
if (externalRo == null) {
|
||||
throw new Error("finalizeRRRepairOrder: outsdRoNo (job.ro_number/id) is required");
|
||||
}
|
||||
|
||||
// Prefer DMS RO for update; fall back to external when DMS RO isn't known
|
||||
const roNoToSend = roNo ? String(roNo) : String(externalRo);
|
||||
|
||||
@@ -34,6 +34,225 @@ const blocksFromCombinedSearchResult = (res) => {
|
||||
return Array.isArray(data) ? data : [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a Dinero.js object or number into an "N2" string ("123.45").
|
||||
* @param value
|
||||
* @returns {string}
|
||||
*/
|
||||
const asN2 = (dineroLike) => {
|
||||
if (!dineroLike) return "0.00";
|
||||
|
||||
// Handle Dinero v1/v2-ish or raw objects
|
||||
if (typeof dineroLike.toUnit === "function") {
|
||||
return dineroLike.toUnit().toFixed(2);
|
||||
}
|
||||
|
||||
const precision = dineroLike.precision ?? 2;
|
||||
const amount = (dineroLike.amount ?? 0) / Math.pow(10, precision);
|
||||
return amount.toFixed(2);
|
||||
};
|
||||
|
||||
/**
|
||||
* Build RO.GOG structure for the reynolds-rome-client `createRepairOrder` payload
|
||||
* from CDK allocations.
|
||||
* @param {Array} allocations
|
||||
* @param {Object} opts
|
||||
* @param {string} opts.opCode - RR OpCode for the job (global, overridable)
|
||||
* @param {string} [opts.payType="Cust"] - PayType (always "Cust" per Marc)
|
||||
* @param {string} [opts.roNo] - Optional RoNo to echo on <Rogog RoNo="">
|
||||
* @returns {null|{roNo?: string, ops: Array}}
|
||||
*/
|
||||
const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo } = {}) => {
|
||||
if (!Array.isArray(allocations) || !allocations.length || !opCode) return null;
|
||||
|
||||
const ops = [];
|
||||
|
||||
for (const alloc of allocations) {
|
||||
const pc = alloc?.profitCenter || {};
|
||||
const breakOut = pc.rr_gogcode;
|
||||
const itemType = pc.rr_item_type;
|
||||
|
||||
// Only centers that have been configured for RR GOG are included
|
||||
if (!breakOut || !itemType) continue;
|
||||
|
||||
const saleN2 = asN2(alloc.sale);
|
||||
const costN2 = asN2(alloc.cost);
|
||||
|
||||
const itemDesc = pc.accountdesc || pc.accountname || alloc.center || "";
|
||||
const jobNo = String(ops.length + 1); // 1-based JobNo
|
||||
|
||||
ops.push({
|
||||
opCode,
|
||||
jobNo,
|
||||
lines: [
|
||||
{
|
||||
breakOut,
|
||||
itemType,
|
||||
itemDesc,
|
||||
custQty: "1.0",
|
||||
// warrQty: "0.0",
|
||||
// intrQty: "0.0",
|
||||
custPayTypeFlag: "C",
|
||||
// warrPayTypeFlag: "W",
|
||||
// intrPayTypeFlag: "I",
|
||||
custTxblNtxblFlag: pc.rr_cust_txbl_flag || "T",
|
||||
// warrTxblNtxblFlag: "N",
|
||||
// intrTxblNtxblFlag: "N",
|
||||
amount: {
|
||||
payType,
|
||||
amtType: "Unit",
|
||||
custPrice: saleN2,
|
||||
dlrCost: costN2
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
if (!ops.length) return null;
|
||||
|
||||
return {
|
||||
roNo,
|
||||
ops
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Build RO.ROLABOR structure for the reynolds-rome-client `createRepairOrder` payload
|
||||
* from an already-built RO.GOG structure.
|
||||
* @param {Object} rogg - result of buildRogogFromAllocations
|
||||
* @param {Object} opts
|
||||
* @param {string} [opts.payType="Cust"]
|
||||
* @returns {null|{ops: Array}}
|
||||
*/
|
||||
const buildRolaborFromRogog = (rogg, { payType = "Cust" } = {}) => {
|
||||
if (!rogg || !Array.isArray(rogg.ops)) return null;
|
||||
|
||||
const ops = rogg.ops.map((op) => {
|
||||
const firstLine = op.lines?.[0] || {};
|
||||
|
||||
return {
|
||||
opCode: op.opCode,
|
||||
jobNo: op.jobNo,
|
||||
custPayTypeFlag: firstLine.custPayTypeFlag || "C",
|
||||
// warrPayTypeFlag: firstLine.warrPayTypeFlag || "W",
|
||||
// intrPayTypeFlag: firstLine.intrPayTypeFlag || "I",
|
||||
custTxblNtxblFlag: firstLine.custTxblNtxblFlag || "N",
|
||||
// warrTxblNtxblFlag: firstLine.warrTxblNtxblFlag || "N",
|
||||
// intrTxblNtxblFlag: firstLine.intrTxblNtxblFlag || "N",
|
||||
// vlrCode: undefined,
|
||||
bill: {
|
||||
payType,
|
||||
jobTotalHrs: "0",
|
||||
billTime: "0",
|
||||
billRate: "0"
|
||||
},
|
||||
amount: {
|
||||
payType,
|
||||
amtType: "Job",
|
||||
custPrice: "0",
|
||||
totalAmt: "0"
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
if (!ops.length) return null;
|
||||
|
||||
return { ops };
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a header-level TaxCodeInfo payload from allocations (e.g. PROVINCIAL SALES TAX line).
|
||||
*
|
||||
* Shape returned matches what `buildCreateRepairOrder` expects for:
|
||||
*
|
||||
* payload.tax = {
|
||||
* payType,
|
||||
* taxCode,
|
||||
* txblGrossAmt,
|
||||
* grossTaxAmt
|
||||
* }
|
||||
*
|
||||
* NOTE: We are currently NOT wiring this into the payload (see buildRRRepairOrderPayload)
|
||||
* so that TaxCodeInfo is suppressed in the XML, but we keep this helper around for
|
||||
* future use.
|
||||
*
|
||||
* @param {Array} allocations
|
||||
* @param {Object} opts
|
||||
* @param {string} opts.taxCode - RR tax code (configured per dealer)
|
||||
* @param {string} [opts.payType="Cust"]
|
||||
* @returns {null|{payType, taxCode, txblGrossAmt, grossTaxAmt}}
|
||||
*/
|
||||
const buildTaxFromAllocations = (allocations, { taxCode, payType = "Cust" } = {}) => {
|
||||
if (!taxCode || !Array.isArray(allocations) || !allocations.length) return null;
|
||||
|
||||
const taxAlloc = allocations.find((a) => a && a.tax);
|
||||
if (!taxAlloc || !taxAlloc.sale) return null;
|
||||
|
||||
const grossTaxNum = parseFloat(asN2(taxAlloc.sale));
|
||||
if (!Number.isFinite(grossTaxNum)) return null;
|
||||
|
||||
const rate = typeof taxAlloc.profitCenter?.rate === "number" ? taxAlloc.profitCenter.rate : null;
|
||||
|
||||
let taxableGrossNum = grossTaxNum;
|
||||
if (rate && rate > 0) {
|
||||
const r = rate / 100;
|
||||
taxableGrossNum = grossTaxNum / r;
|
||||
}
|
||||
|
||||
return {
|
||||
payType,
|
||||
taxCode,
|
||||
txblGrossAmt: taxableGrossNum.toFixed(2),
|
||||
grossTaxAmt: grossTaxNum.toFixed(2)
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a minimal Rolabor structure in the new normalized shape.
|
||||
*
|
||||
* Useful for tests or for scenarios where you want a single zero-dollar
|
||||
* Rolabor op but don't have GOG data. Shape matches payload.rolabor for the
|
||||
* reynolds-rome-client builders.
|
||||
*
|
||||
* @param {Object} opts
|
||||
* @param {string} opts.opCode
|
||||
* @param {number|string} [opts.jobNo=1]
|
||||
* @param {string} [opts.payType="Cust"]
|
||||
* @returns {null|{ops: Array}}
|
||||
*/
|
||||
const buildRolaborSkeleton = ({ opCode, jobNo = 1, payType = "Cust" } = {}) => {
|
||||
if (!opCode) return null;
|
||||
|
||||
return {
|
||||
ops: [
|
||||
{
|
||||
opCode,
|
||||
jobNo: String(jobNo),
|
||||
custPayTypeFlag: "C",
|
||||
warrPayTypeFlag: "W",
|
||||
intrPayTypeFlag: "I",
|
||||
custTxblNtxblFlag: "N",
|
||||
warrTxblNtxblFlag: "N",
|
||||
intrTxblNtxblFlag: "N",
|
||||
vlrCode: undefined,
|
||||
bill: {
|
||||
payType,
|
||||
jobTotalHrs: "0",
|
||||
billTime: "0",
|
||||
billRate: "0"
|
||||
},
|
||||
amount: {
|
||||
payType,
|
||||
amtType: "Job",
|
||||
custPrice: "0",
|
||||
totalAmt: "0"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
// ---------- Public API ----------
|
||||
|
||||
/**
|
||||
@@ -63,25 +282,38 @@ const QueryJobData = async (ctx = {}, jobId) => {
|
||||
|
||||
/**
|
||||
* Build Repair Order payload for RR from job and customer data.
|
||||
* @param {Object} args
|
||||
* @param job
|
||||
* @param selectedCustomer
|
||||
* @param advisorNo
|
||||
* @param story
|
||||
* @param makeOverride
|
||||
* @returns {{outsdRoNo: string, repairOrderNumber: string, departmentType: string, vin: string, customerNo: string, advisorNo: string, mileageIn: *|null}}
|
||||
* @param bodyshop
|
||||
* @param allocations
|
||||
* @param {string} [opCode] - RR OpCode for this RO (global default / override)
|
||||
* @param {string} [taxCode] - RR tax code for header tax (e.g. state/prov code)
|
||||
* @returns {Object}
|
||||
*/
|
||||
const buildRRRepairOrderPayload = ({ job, selectedCustomer, advisorNo, story }) => {
|
||||
const buildRRRepairOrderPayload = ({
|
||||
job,
|
||||
selectedCustomer,
|
||||
advisorNo,
|
||||
story,
|
||||
makeOverride,
|
||||
allocations,
|
||||
opCode,
|
||||
taxCode
|
||||
} = {}) => {
|
||||
const customerNo = selectedCustomer?.customerNo
|
||||
? String(selectedCustomer.customerNo).trim()
|
||||
: selectedCustomer?.custNo
|
||||
? String(selectedCustomer.custNo).trim()
|
||||
: null;
|
||||
|
||||
if (!customerNo) throw new Error("No RR customer selected (customerNo/CustNo missing)");
|
||||
if (!customerNo) throw new Error("No RR customer selected (customerNo/custNo missing)");
|
||||
|
||||
const adv = advisorNo != null && String(advisorNo).trim() !== "" ? String(advisorNo).trim() : null;
|
||||
|
||||
if (!adv) throw new Error("advisorNo is required for RR export");
|
||||
if (!adv) throw new Error("advisorNo is required for RR export");
|
||||
|
||||
const vinRaw = job?.v_vin;
|
||||
const vin =
|
||||
@@ -98,9 +330,9 @@ const buildRRRepairOrderPayload = ({ job, selectedCustomer, advisorNo, story })
|
||||
|
||||
const roStr = String(ro);
|
||||
|
||||
const output = {
|
||||
// Base payload shape expected by reynolds-rome-client (buildCreateRepairOrder)
|
||||
const payload = {
|
||||
outsdRoNo: roStr,
|
||||
repairOrderNumber: roStr,
|
||||
departmentType: "B",
|
||||
vin,
|
||||
customerNo: String(customerNo),
|
||||
@@ -109,10 +341,57 @@ const buildRRRepairOrderPayload = ({ job, selectedCustomer, advisorNo, story })
|
||||
};
|
||||
|
||||
if (story) {
|
||||
output.roComment = String(story).trim();
|
||||
payload.roComment = String(story).trim();
|
||||
}
|
||||
|
||||
return output;
|
||||
if (makeOverride) {
|
||||
// Passed through so the template can override DMS Make if needed
|
||||
payload.makeOverride = String(makeOverride).trim();
|
||||
}
|
||||
|
||||
const haveAllocations = Array.isArray(allocations) && allocations.length > 0;
|
||||
|
||||
if (haveAllocations) {
|
||||
const effectiveOpCode = (opCode && String(opCode).trim()) || null;
|
||||
const effectiveTaxCode = (taxCode && String(taxCode).trim()) || null;
|
||||
|
||||
if (effectiveOpCode) {
|
||||
// Build RO.GOG and RO.LABOR in the new normalized shape
|
||||
const rogg = buildRogogFromAllocations(allocations, {
|
||||
opCode: effectiveOpCode,
|
||||
payType: "Cust"
|
||||
});
|
||||
|
||||
if (rogg) {
|
||||
payload.rogg = rogg;
|
||||
|
||||
const rolabor = buildRolaborFromRogog(rogg, { payType: "Cust" });
|
||||
if (rolabor) {
|
||||
payload.rolabor = rolabor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- TAX HEADER TEMPORARILY DISABLED ---
|
||||
// We intentionally do NOT attach payload.tax right now so that the Mustache
|
||||
// section that renders <TaxCodeInfo> stays false and no TaxCodeInfo is sent.
|
||||
//
|
||||
// Keeping this commented-out for future enablement once RR confirms header
|
||||
// tax handling behaviour.
|
||||
//
|
||||
// if (effectiveTaxCode) {
|
||||
// const taxInfo = buildTaxFromAllocations(allocations, {
|
||||
// taxCode: effectiveTaxCode,
|
||||
// payType: "Cust"
|
||||
// });
|
||||
//
|
||||
// if (taxInfo) {
|
||||
// payload.tax = taxInfo;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -220,5 +499,10 @@ module.exports = {
|
||||
makeCustomerSearchPayloadFromJob,
|
||||
makeVehicleSearchPayloadFromJob,
|
||||
normalizeCustomerCandidates,
|
||||
normalizeVehicleCandidates
|
||||
normalizeVehicleCandidates,
|
||||
// exporting these so you can unit-test them directly if you want
|
||||
buildRogogFromAllocations,
|
||||
buildTaxFromAllocations,
|
||||
buildRolaborSkeleton,
|
||||
buildRolaborFromRogog
|
||||
};
|
||||
|
||||
131
server/rr/rr-responsibility-centers.js
Normal file
131
server/rr/rr-responsibility-centers.js
Normal file
@@ -0,0 +1,131 @@
|
||||
// Helpers to use md_responsibility_centers (including RR GOG metadata)
|
||||
// to build the data needed for ROLABOR + ROGOG lines.
|
||||
|
||||
/**
|
||||
* Extracts responsibility center configuration into a convenient structure.
|
||||
*
|
||||
* Expects bodyshop.md_responsibility_centers to look like:
|
||||
*
|
||||
* {
|
||||
* costs: [{ name, ... }],
|
||||
* profits: [{ name, rr_gogcode, rr_item_type, rr_cust_txbl_flag, ... }],
|
||||
* dms_defaults: {
|
||||
* costs: { LAB: "Body Labor Cost", ... },
|
||||
* profits: { LAB: "Body Labor Sales", ... }
|
||||
* },
|
||||
* defaults: {
|
||||
* costs: { LAB: "Some Cost Center", ... },
|
||||
* profits: { LAB: "Some Profit Center", ... }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
function extractRrResponsibilityCenters(bodyshop = {}) {
|
||||
const centers = bodyshop.md_responsibility_centers || {};
|
||||
const { costs = [], profits = [], dms_defaults = {}, defaults = {} } = centers;
|
||||
|
||||
const indexByName = (arr = []) =>
|
||||
(arr || []).reduce((acc, center) => {
|
||||
if (center && center.name) {
|
||||
acc[center.name] = center;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
costsByName: indexByName(costs),
|
||||
profitsByName: indexByName(profits),
|
||||
dmsCostDefaults: (dms_defaults && dms_defaults.costs) || {},
|
||||
dmsProfitDefaults: (dms_defaults && dms_defaults.profits) || {},
|
||||
defaultCosts: defaults.costs || {},
|
||||
defaultProfits: defaults.profits || {}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve cost + profit centers for a given internal code (ATS/LAB/etc.).
|
||||
* Prioritizes dms_defaults, then defaults.
|
||||
*
|
||||
* @param {string} code
|
||||
* @param {ReturnType<typeof extractRrResponsibilityCenters>} cfg
|
||||
* @returns {{ costCenter: object|null, profitCenter: object|null }}
|
||||
*/
|
||||
function resolveCentersForCode(code, cfg) {
|
||||
if (!code || !cfg) return { costCenter: null, profitCenter: null };
|
||||
|
||||
const costCenterName = cfg.dmsCostDefaults[code] || cfg.defaultCosts[code] || null;
|
||||
const profitCenterName = cfg.dmsProfitDefaults[code] || cfg.defaultProfits[code] || null;
|
||||
|
||||
const costCenter = costCenterName && cfg.costsByName[costCenterName] ? cfg.costsByName[costCenterName] : null;
|
||||
const profitCenter =
|
||||
profitCenterName && cfg.profitsByName[profitCenterName] ? cfg.profitsByName[profitCenterName] : null;
|
||||
|
||||
return { costCenter, profitCenter };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a single RR GOG line payload from a jobline + code.
|
||||
*
|
||||
* NOTE: This returns a neutral JS object. You still need to map it into
|
||||
* the exact Mustache / XML shape used for AllGogLineItmInfo.
|
||||
*
|
||||
* @param {Object} params
|
||||
* @param {Object} params.jobline // jobline record from Hasura
|
||||
* @param {string} params.code // e.g. "LAB", "ATS", ...
|
||||
* @param {object} params.centersConfig // result of extractRrResponsibilityCenters()
|
||||
* @param {number} params.sale // CustPrice
|
||||
* @param {number} params.cost // DollarCost
|
||||
*/
|
||||
function buildRrGogLine({ jobline, code, centersConfig, sale, cost }) {
|
||||
const { profitCenter } = resolveCentersForCode(code, centersConfig);
|
||||
|
||||
if (!profitCenter || !profitCenter.rr_gogcode) {
|
||||
throw new Error('RR: missing rr_gogcode for profit center mapping of code "' + code + '"');
|
||||
}
|
||||
|
||||
return {
|
||||
// For your own reference:
|
||||
code,
|
||||
joblineId: jobline && jobline.id,
|
||||
|
||||
// Fields you ultimately care about for ROGOG:
|
||||
BreakOut: profitCenter.rr_gogcode, // GOG code / BreakOut
|
||||
ItemType: profitCenter.rr_item_type || "G", // P / G / F / ...
|
||||
CustTxblNTxblFlag: profitCenter.rr_cust_txbl_flag || "T", // T/N
|
||||
CustPrice: Number(sale || 0),
|
||||
DollarCost: Number(cost || 0),
|
||||
|
||||
// Surface full profit center in case templates / logs need it:
|
||||
profitCenter
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a minimal RR labor (ROLABOR) line "shell".
|
||||
* Amounts are zero if all financials are in GOG.
|
||||
*
|
||||
* @param {Object} params
|
||||
* @param {Object} params.job // job record (for RO header context)
|
||||
* @param {string} params.opCode // global opcode (already resolved)
|
||||
* @param {Object} [params.profitCenter] // optional profit center for context
|
||||
*/
|
||||
function buildRrLaborLine({ job, opCode, profitCenter }) {
|
||||
if (!opCode) {
|
||||
throw new Error("RR: opCode is required to build ROLABOR line");
|
||||
}
|
||||
|
||||
return {
|
||||
jobId: job && job.id,
|
||||
OpCode: opCode,
|
||||
PayType: "Cust", // always Cust per your notes
|
||||
CustPrice: 0,
|
||||
DollarCost: 0,
|
||||
profitCenter
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
extractRrResponsibilityCenters,
|
||||
resolveCentersForCode,
|
||||
buildRrGogLine,
|
||||
buildRrLaborLine
|
||||
};
|
||||
Reference in New Issue
Block a user