/* ──────────────────────────────────────────────────────────────────────────── K2B DMS — service & body-shop money after the close: ONE calculation for the RO payment after close, its void and refund, the body-shop close and the policy / goodwill adjustment (RC24 "office leftovers", lane svcm). The SAME file is used twice, byte for byte (like RC20's fixedops-calc.js / FopsCalc.jsx.txt): server src/lib/svcm-calc.js — the commands POST /api/fixedops/ro/payment | /void | /refund, /api/fixedops/bodyshop/close and /api/fixedops/policy compute the committed money and the journal with it (the server's answer wins) browser SvcmCalc.jsx.txt — window.SvcmCalc, for the PREVIEW only (the Payments tab, the Body Shop screen, the Policy screen). Money rules: every amount is an INTEGER number of cents (a dollar figure is read through its decimal text, never * 100); a money figure typed by a person is STRICT — "1,234.50" and "$12" are fine, "5,00" (a comma that is not a thousands separator) is REFUSED, never read as 500 (RC24 pitfall 6); tax once on the taxable base, half-up to the cent; a line = qty x unit, half-up. The accounts are exactly the ones the station posts today (DMSCore.jsx.txt ACCT / closeBodyRO / approvePolicyAdjustment, ServiceCore.jsx.txt payLinesAfterClose / reversalLines, fixedops-calc ACCT). Nothing here is new. Pure functions only — no database, no window, no Date. Node 20 CommonJS / browser global. ──────────────────────────────────────────────────────────────────────── */ (function (root, factory) { var api = factory(); if (typeof module === "object" && module && module.exports) module.exports = api; if (root) root.SvcmCalc = api; })(typeof window !== "undefined" ? window : null, function () { "use strict"; var CALC_VERSION = "svcm-1"; /* ── the accounts (the station's map; the body-shop literals 468 / 668 / 300 are what closeBodyRO writes literally) ── */ var ACCT = { cashOnHand: "201", cardDeposit: "202", arCustomer: "220", insuranceRec: "262", warrantyRec: "263", contractRec: "264", svcCourtesy: "0675", policyWork: "0675", usedVehPolicy: "5015", invNewCar: "231", invUsedCar: "240", invUsedTruck: "241", invParts: "242", apTrade: "300", salesTaxPay: "324A", custLabor: "460A", partsCarRO: "467", bodySubletSale: "468", costLabor: "660A", costPartsRO: "667", bodySubletCost: "668", cardFeeIncome: "905E", laborApplied: "999", sunbitRec: "294", sunbitFee: "0775", // RC28 (svc): Sunbit (pay over time) — the defaults; a payment carries the accounts it was taken with }; var PAY_METHODS = ["cash", "card", "invoice", "warranty", "contract", "courtesy", "internal", "sunbit"]; // RC28 (svc): + sunbit var REFUND_METHODS = ["cash", "card", "check"]; var POLICY_ACCOUNTS = [ACCT.policyWork, ACCT.usedVehPolicy]; var BODY_LABOR_COST_PCT_PPM = 400000; // closeBodyRO: laborCost = laborSale x 0.4 (the station's rule, kept) var MAX_CENTS = 1000000000; /* ── numbers ── */ function decText(v) { var s; if (typeof v === "number") { if (!isFinite(v)) return null; s = v.toFixed(6); } else if (typeof v === "string") { s = v.replace(/[,$\s]/g, ""); if (!s) return null; } else return null; var m = /^(-)?(\d*)(?:\.(\d*))?$/.exec(s); if (!m || (m[2] === "" && (m[3] === undefined || m[3] === ""))) return null; return { neg: !!m[1], int: m[2] || "0", frac: m[3] || "" }; } /** Any stored dollar figure → integer cents, half-up on the third decimal. NaN when not a number. (lenient: stored records) */ function toCents(v) { if (v === undefined || v === null || v === "") return 0; var d = decText(v); if (!d) return NaN; var f = (d.frac + "000").slice(0, 3); var c = Number(d.int) * 100 + Number(f.slice(0, 2)) + (Number(f.charAt(2)) >= 5 ? 1 : 0); if (!isFinite(c)) return NaN; return d.neg && c !== 0 ? -c : c; } /** A money figure TYPED by a person / sent by a station: a JSON number with at most 2 decimals, or a plain decimal string ("1234.50", "1,234.50", "$1,234.50", "12") — a comma is accepted ONLY as a thousands separator in groups of three; "5,00", "1,23.4", "12,345,6" are refused (NaN), never read as another amount (pitfall 6). → cents | NaN | 'decimals'. */ function strictCents(v) { if (v === undefined || v === null || v === "") return NaN; if (typeof v === "number") { if (!isFinite(v)) return NaN; if (Math.abs(v * 100 - Math.round(v * 100)) > 1e-6) return "decimals"; return toCents(v); } if (typeof v !== "string") return NaN; var s = v.trim(); var m = /^(-)?\$?\s*(\d{1,3}(?:,\d{3})+|\d+)(?:\.(\d*))?$/.exec(s); if (!m) return NaN; if (m[3] !== undefined && m[3].length > 2) return "decimals"; return toCents((m[1] || "") + m[2].replace(/,/g, "") + "." + (m[3] || "0")); } function toMilli(v) { var d = decText(v); if (!d) return NaN; var fr = d.frac.replace(/0+$/, ""); if (fr.length > 3) return NaN; var n = Number(d.int) * 1000 + Number((fr + "000").slice(0, 3)); return d.neg && n !== 0 ? -n : n; } function mulDivRound(a, b, div) { var neg = (a < 0) !== (b < 0) && a !== 0 && b !== 0; var n = Math.abs(a) * Math.abs(b); var q = Math.floor(n / div), r = n - q * div; if (r * 2 >= div) q += 1; return neg ? -q : q; } function extCents(qty, unitCents) { var m = toMilli(qty); if (!isFinite(m) || !isFinite(unitCents)) return NaN; return mulDivRound(m, unitCents, 1000); } function pctToPpm(p) { var d = decText(p); if (!d || d.neg) return NaN; var fr = d.frac.replace(/0+$/, ""); if (fr.length > 4) return NaN; var ppm = Number(d.int) * 10000 + Number((fr + "0000").slice(0, 4)); return ppm >= 0 && ppm <= 300000 ? ppm : NaN; } function taxCents(baseCents, ppm) { return baseCents && ppm ? mulDivRound(baseCents, ppm, 1000000) : 0; } function dollars(c) { return Math.round(c) / 100; } function fmt(c) { var neg = c < 0; c = Math.abs(Math.round(c)); var s = String(Math.floor(c / 100)).replace(/\B(?=(\d{3})+(?!\d))/g, ","); return (neg ? "-$" : "$") + s + "." + String(c % 100).padStart(2, "0"); } function sumSides(lines) { var d = 0, c = 0; (lines || []).forEach(function (l) { d += l.dr || 0; c += l.cr || 0; }); return { dr: d, cr: c, balanced: d === c }; } function toJournalLines(lines) { return (lines || []).filter(function (l) { return l.dr || l.cr; }).map(function (l) { var o = { acct: l.acct, dr: dollars(l.dr), cr: dollars(l.cr) }; if (l.desc) o.desc = String(l.desc).slice(0, 120); return o; }); } /* ── the card fee (ServiceCore.cardFee / RC20 normPayment): serviceSettings.cardFeePct (default 3.5 %) when cardFeeOn ── */ function cardFee(amountCents, serviceSettings) { var ss = serviceSettings && typeof serviceSettings === "object" ? serviceSettings : {}; var on = ss.cardFeeOn !== undefined && ss.cardFeeOn !== null ? !!ss.cardFeeOn : true; var pct = ss.cardFeePct !== undefined && ss.cardFeePct !== null && ss.cardFeePct !== "" ? ss.cardFeePct : "3.5"; var ppm = on ? pctToPpm(pct) : 0; if (!isFinite(ppm)) ppm = 0; return { feeCents: taxCents(amountCents, ppm), pct: on ? Number(pct) : 0, on: on }; } var METHOD_LABEL = { cash: "Cash", card: "Credit card", check: "Check", invoice: "Invoice (A/R)", warranty: "Warranty", contract: "Service contract", courtesy: "Courtesy — service dept gross", internal: "Internal (charge to unit)", sunbit: "Sunbit (pay over time)" }; /** The payer account of one payment (DMSCore.roPayAcct). unitAcct = the internal unit's inventory account. */ function payAcctOf(method, unitAcct) { return method === "cash" || method === "check" ? ACCT.cashOnHand : method === "card" ? ACCT.cardDeposit : method === "invoice" ? ACCT.arCustomer : method === "warranty" ? ACCT.warrantyRec : method === "contract" ? ACCT.contractRec : method === "courtesy" ? ACCT.svcCourtesy : method === "internal" ? (unitAcct || ACCT.invUsedCar) : method === "sunbit" ? ACCT.sunbitRec : ACCT.arCustomer; } /** RC28 (svc): a Sunbit payment's merchant fee in cents (never charged to the customer; 0 when none). */ function sunbitFeeCents(p) { var mf = toCents(p && p.merchantFee || 0); return isFinite(mf) && mf > 0 ? mf : 0; } function unitInvAcct(u) { if (u && u.invAcct) return String(u.invAcct); if (u && /^n/i.test(String(u.type || ""))) return ACCT.invNewCar; return /truck|silverado|sierra|colorado|tahoe|suburban/i.test(u ? (u.mdl || "") + " " + (u.body || "") : "") ? ACCT.invUsedTruck : ACCT.invUsedCar; } /* ── RO PAYMENT AFTER CLOSE (ServiceCore.payLinesAfterClose): DR payer (amount + fee) · CR A/R 220 amount · CR 905E fee. An "invoice" payment moves the balance inside A/R 220 (sub-ledger only): no journal lines. ── */ function paymentPosting(p, opts) { opts = opts || {}; var acct = payAcctOf(p.method, opts.unitAcct), ar = ACCT.arCustomer; var amt = p.amountCents, fee = p.feeCents || 0; if (p.method === "sunbit") { // RC28 (svc): DR the Sunbit receivable · CR A/R 220 · Sunbit's merchant fee DR the fee expense / CR the receivable var rec = p.recAcct || ACCT.sunbitRec, mf = p.merchantFeeCents || 0; var sl = [{ acct: rec, dr: amt, cr: 0, desc: "Sunbit " + (p.receiptNo || "") + (p.sunbitRef ? " ref " + p.sunbitRef : "") }, { acct: ar, dr: 0, cr: amt, desc: "Applied to " + (opts.roId || "") }]; if (mf) { sl.push({ acct: p.feeAcct || ACCT.sunbitFee, dr: mf, cr: 0, desc: "Sunbit merchant fee" }); sl.push({ acct: rec, dr: 0, cr: mf, desc: "Sunbit merchant fee — kept from the deposit" }); } return sl; } if (acct === ar) return []; var out = [{ acct: acct, dr: amt + fee, cr: 0, desc: (METHOD_LABEL[p.method] || p.method) + " " + (p.receiptNo || "") }, { acct: ar, dr: 0, cr: amt, desc: "Applied to " + (opts.roId || "") }]; if (fee) out.push({ acct: ACCT.cardFeeIncome, dr: 0, cr: fee, desc: "Card fee charged to customer" }); return out; } /* ── VOID / REFUND (ServiceCore.reversalLines): DR A/R 220 amount · DR 905E fee share · CR payer (amount + fee) ── */ function reversalPosting(p, amtCents, feeCents, tag, opts) { opts = opts || {}; var acct = payAcctOf(p.method, opts.unitAcct), ar = ACCT.arCustomer; if (p.method === "sunbit") { // RC28 (svc): the exact mirror of the Sunbit payment's own lines (a refund is done in Sunbit, then the payment is voided) var rec = p.recAcct || ACCT.sunbitRec, mf = sunbitFeeCents(p); var rl = [{ acct: ar, dr: amtCents, cr: 0, desc: tag + " " + (p.receiptNo || "") + " — back to customer A/R" }, { acct: rec, dr: 0, cr: amtCents, desc: tag + " Sunbit " + (p.receiptNo || "") }]; if (mf) { rl.push({ acct: rec, dr: mf, cr: 0, desc: tag + " Sunbit merchant fee" }); rl.push({ acct: p.feeAcct || ACCT.sunbitFee, dr: 0, cr: mf, desc: tag + " Sunbit merchant fee" }); } return rl; } if (acct === ar) return []; if (opts.payerAcct) acct = opts.payerAcct; // a refund paid back another way (cash for a card payment, the bank for a check) var out = [{ acct: ar, dr: amtCents, cr: 0, desc: tag + " " + (p.receiptNo || "") + " — back to customer A/R" }]; if (feeCents) out.push({ acct: ACCT.cardFeeIncome, dr: feeCents, cr: 0, desc: tag + " card fee" }); out.push({ acct: acct, dr: 0, cr: amtCents + feeCents, desc: tag + " " + (METHOD_LABEL[p.method] || p.method) + " " + (p.receiptNo || "") }); return out; } /** The fee share of a partial refund (ServiceCore: fee x amt / amount, half-up). */ function feeShare(feeCents, amtCents, amountCents) { return feeCents && amountCents ? mulDivRound(feeCents, amtCents, amountCents) : 0; } /** The open balance of a closed RO: its committed total minus every live payment (a refund is a negative payment). */ function roOpenBalance(ro) { var total = ro && ro.foInvoice && isFinite(ro.foInvoice.totalCents) ? Number(ro.foInvoice.totalCents) : (ro && ro.total !== undefined && ro.total !== null && ro.total !== "" ? toCents(ro.total) : (toCents(ro && ro.laborSale) || 0) + (toCents(ro && ro.partsSale) || 0) + (toCents(ro && ro.subletSale) || 0) + (toCents(ro && ro.feesSale) || 0) + (toCents(ro && ro.tax) || 0)); if (!isFinite(total)) total = 0; var paid = 0; ((ro && ro.payments) || []).forEach(function (p) { if (!p || p.voided) return; var a = toCents(p.amount); if (isFinite(a)) paid += a; }); return { totalCents: total, paidCents: paid, balanceCents: total - paid }; } /* ── BODY SHOP CLOSE (DMSCore.closeBodyRO / recalcBodyRO, in cents): labor = Σ hours x rate (the line's stored sale when hours / rate are missing); laborCost = 40 % of labor sale (the station's rule); parts = Σ stored line sale, cost = Σ qty x STORED line cost (the job's cost, never today's parts file); sublet sale / cost as stored; approved supplements; tax on PARTS only at the service rate (the station's rule: body-shop tax = parts x rate); total = labor + parts + sublet + supplements + tax. Pending supplements block the close (problems). ── */ function bodyTotals(job, cfg) { cfg = cfg || {}; var ppm = isFinite(cfg.servicePpm) ? cfg.servicePpm : pctToPpm("7.225"); var problems = [], lines = []; var labor = 0, parts = 0, partsCost = 0, sublet = 0, subletCost = 0, supp = 0; ((job && job.lines) || []).forEach(function (l, i) { if (!l) return; var hrs = l.hours === undefined || l.hours === null || l.hours === "" ? null : toMilli(l.hours), rate = toCents(l.rate); // RC28 fix round (finding 1 class): hours / a rate that can't be read are a problem — never a silent fall back to the stored sale if (hrs !== null && !isFinite(hrs)) problems.push("Labor line " + (i + 1) + " (" + (l.desc || l.type || "") + "): the hours \"" + String(l.hours).slice(0, 20) + "\" are not a number with at most 3 decimals."); if (!isFinite(rate)) problems.push("Labor line " + (i + 1) + " (" + (l.desc || l.type || "") + "): the rate \"" + String(l.rate).slice(0, 20) + "\" is not a dollar amount."); var sale = hrs !== null && isFinite(hrs) && isFinite(rate) && rate > 0 ? mulDivRound(hrs, rate, 1000) : toCents(l.sale); if (!isFinite(sale)) { problems.push("Labor line " + (i + 1) + " (" + (l.desc || l.type || "") + ") has an amount that is not a number."); sale = 0; } if (sale < 0) { problems.push("Labor line " + (i + 1) + " is negative (" + fmt(sale) + ") — a body-shop line can't be negative."); sale = 0; } labor += sale; lines.push({ type: "labor", id: String(l.id), kind: l.type || "body", desc: l.desc || "", hours: l.hours || 0, rateCents: isFinite(rate) ? rate : 0, saleCents: sale }); }); ((job && job.partsLines) || []).forEach(function (p, i) { if (!p) return; var q = p.qty === undefined || p.qty === null || p.qty === "" ? 1 : p.qty, qm = toMilli(q); if (!isFinite(qm) || qm <= 0 || qm > 10000000) { problems.push("Part " + (p.part || i + 1) + ": the quantity " + String(q) + " must be a number from 0.001 to 10,000."); return; } var sale = toCents(p.sale), unitCost = toCents(p.cost); if (!isFinite(sale)) { problems.push("Part " + (p.part || i + 1) + " has a price that is not a number."); sale = 0; } if (!isFinite(unitCost)) problems.push("Part " + (p.part || i + 1) + " has a cost that is not a number (\"" + String(p.cost).slice(0, 20) + "\") — correct the line's cost."); // RC28 fix round: never a silent $0.00 cost if (sale < 0) { problems.push("Part " + (p.part || i + 1) + " has a negative price."); sale = 0; } var cost = isFinite(unitCost) && unitCost > 0 ? mulDivRound(qm, unitCost, 1000) : 0; parts += sale; partsCost += cost; lines.push({ type: "part", id: String(p.id), part: p.part || "", desc: p.desc || "", qty: q, qtyMilli: qm, saleCents: sale, costCents: cost, unitCostCents: isFinite(unitCost) ? unitCost : 0 }); }); ((job && job.sublets) || []).forEach(function (s, i) { if (!s) return; var sale = toCents(s.sale), cost = toCents(s.cost); if (!isFinite(sale) || !isFinite(cost)) { problems.push("Sublet " + (s.vendor || i + 1) + " has an amount that is not a number."); sale = isFinite(sale) ? sale : 0; cost = isFinite(cost) ? cost : 0; } if (sale < 0 || cost < 0) { problems.push("Sublet " + (s.vendor || i + 1) + " is negative."); sale = Math.max(0, sale); cost = Math.max(0, cost); } sublet += sale; subletCost += cost; lines.push({ type: "sublet", id: String(s.id), vendor: s.vendor || "", desc: s.desc || "", saleCents: sale, costCents: cost }); }); var pending = 0; ((job && job.supplements) || []).forEach(function (x) { if (!x) return; if (x.status === "Pending") { pending++; return; } if (x.status !== "Approved") return; var a = toCents(x.amount); if (!isFinite(a) || a < 0) { problems.push("Supplement " + (x.desc || x.id) + " has an amount that is not a number."); return; } supp += a; lines.push({ type: "supplement", id: String(x.id), desc: x.desc || "", saleCents: a }); }); if (pending) problems.push(pending + " supplement" + (pending === 1 ? " is" : "s are") + " still pending — approve or deny " + (pending === 1 ? "it" : "them") + " before closing."); var laborCost = mulDivRound(labor, BODY_LABOR_COST_PCT_PPM, 1000000); // cfg.taxOverrideCents: the tax the customer was actually charged on a job a station closed BEFORE RC24 (catch-up posting only) var tax = cfg.taxOverrideCents !== undefined && cfg.taxOverrideCents !== null && isFinite(cfg.taxOverrideCents) ? cfg.taxOverrideCents : taxCents(parts, ppm); var total = labor + parts + sublet + supp + tax; if (total > MAX_CENTS) problems.push("This job is larger than the server allows (" + fmt(MAX_CENTS) + ")."); var isIns = String((job && job.payType) || "") === "Insurance"; var ded = isIns ? toCents(job.deductible) : 0; if (isIns && (!isFinite(ded) || ded < 0)) problems.push("The deductible (\"" + String(job.deductible).slice(0, 20) + "\") is not a dollar amount of $0.00 or more — correct it before closing."); // RC28 fix round: never a silent $0.00 deductible if (!isFinite(ded) || ded < 0) ded = 0; var deductible = isIns ? Math.min(ded, total) : 0; var insurer = isIns ? total - deductible : 0; var customer = isIns ? deductible : total; return { calc: CALC_VERSION, payType: isIns ? "Insurance" : "Customer", lines: lines, problems: problems, pendingSupplements: pending, laborCents: labor, laborCostCents: laborCost, partsCents: parts, partsCostCents: partsCost, subletCents: sublet, subletCostCents: subletCost, supplementCents: supp, taxableCents: parts, taxRatePpm: ppm, taxCents: tax, totalCents: total, deductibleCents: deductible, insurerCents: insurer, customerCents: customer }; } /** Who pays the customer's share: the payments taken at the close (cash / card / invoice) cover it; what stays unpaid goes to A/R 220. */ function bodyAllocate(t, payments) { var paid = 0, fees = 0, problems = []; (payments || []).forEach(function (p) { if (["cash", "card", "invoice"].indexOf(p.method) === -1) { problems.push("A body-shop payment is cash, card or an invoice to the customer's A/R account (not " + String(p.method) + ")."); return; } paid += p.amountCents; fees += p.feeCents || 0; }); if (paid > t.customerCents) problems.push("Payments total " + fmt(paid) + " but the customer's share is " + fmt(t.customerCents) + (t.payType === "Insurance" ? " (the deductible; the rest is billed to the insurer)" : "") + " — over by " + fmt(paid - t.customerCents) + "."); return { ok: problems.length === 0, problems: problems, paidCents: paid, feesCents: fees, customerOwedCents: Math.max(0, t.customerCents - paid) }; } /** The close entry (closeBodyRO's map + the payments and the receivables, never a plug): DR payments to their accounts · DR 262 insurer share · DR 220 what the customer still owes · CR 460A labor (+ approved supplements) · CR 467 parts · CR 468 sublet · CR 324A tax · CR 905E card fees · DR 660A / CR 999 labor cost · DR 667 / CR 242 parts cost (stored job cost) · DR 668 / CR 300 sublet cost (the vendor's bill). */ function bodyClosePosting(t, alloc, payments, opts) { opts = opts || {}; var A = ACCT, out = []; (payments || []).forEach(function (p) { var a = p.amountCents + (p.feeCents || 0); if (!a) return; out.push({ acct: payAcctOf(p.method, null), dr: a, cr: 0, desc: (METHOD_LABEL[p.method] || p.method) + (p.receiptNo ? " " + p.receiptNo : "") + (p.ref ? " ref " + p.ref : "") + (p.account ? " acct " + p.account : ""), pay: p.id || null }); }); if (t.insurerCents) out.push({ acct: A.insuranceRec, dr: t.insurerCents, cr: 0, desc: "Billed to " + (opts.insurer || "insurer") + (opts.claimNo ? " claim " + opts.claimNo : ""), receivable: "Insurance" }); if (alloc.customerOwedCents) out.push({ acct: A.arCustomer, dr: alloc.customerOwedCents, cr: 0, desc: "Customer balance due — A/R", receivable: "Customer" }); if (t.laborCents) out.push({ acct: A.custLabor, dr: 0, cr: t.laborCents, desc: "Body shop labor" }); if (t.supplementCents) out.push({ acct: A.custLabor, dr: 0, cr: t.supplementCents, desc: "Approved supplements" }); if (t.partsCents) out.push({ acct: A.partsCarRO, dr: 0, cr: t.partsCents, desc: "Parts on body RO" }); if (t.subletCents) out.push({ acct: A.bodySubletSale, dr: 0, cr: t.subletCents, desc: "Sublet — body shop" }); if (t.taxCents) out.push({ acct: A.salesTaxPay, dr: 0, cr: t.taxCents, desc: "Sales tax payable" }); if (alloc.feesCents) out.push({ acct: A.cardFeeIncome, dr: 0, cr: alloc.feesCents, desc: "Card fee charged to customer" }); if (t.laborCostCents) { out.push({ acct: A.costLabor, dr: t.laborCostCents, cr: 0, desc: "Cost of body labor" }); out.push({ acct: A.laborApplied, dr: 0, cr: t.laborCostCents, desc: "Labor applied" }); } if (t.partsCostCents) { out.push({ acct: A.costPartsRO, dr: t.partsCostCents, cr: 0, desc: "Cost of parts (job cost)" }); out.push({ acct: A.invParts, dr: 0, cr: t.partsCostCents, desc: "Relieve parts inventory" }); } if (t.subletCostCents) { out.push({ acct: A.bodySubletCost, dr: t.subletCostCents, cr: 0, desc: "Cost of sublet" }); out.push({ acct: A.apTrade, dr: 0, cr: t.subletCostCents, desc: "Sublet vendor payable" }); } return out; } /* ── POLICY / GOODWILL (DMSCore.approvePolicyAdjustment): DR 0675 policy work (5015 when tied to the unit) · CR 460A ── */ function policyPosting(amountCents, account, reason) { return [{ acct: account, dr: amountCents, cr: 0, desc: reason || "Policy / goodwill" }, { acct: ACCT.custLabor, dr: 0, cr: amountCents, desc: "Policy covers service/parts" }]; } return { CALC_VERSION: CALC_VERSION, ACCT: ACCT, PAY_METHODS: PAY_METHODS, REFUND_METHODS: REFUND_METHODS, POLICY_ACCOUNTS: POLICY_ACCOUNTS, METHOD_LABEL: METHOD_LABEL, MAX_CENTS: MAX_CENTS, toCents: toCents, strictCents: strictCents, toMilli: toMilli, extCents: extCents, pctToPpm: pctToPpm, taxCents: taxCents, mulDivRound: mulDivRound, dollars: dollars, fmt: fmt, sumSides: sumSides, toJournalLines: toJournalLines, cardFee: cardFee, payAcctOf: payAcctOf, unitInvAcct: unitInvAcct, sunbitFeeCents: sunbitFeeCents, paymentPosting: paymentPosting, reversalPosting: reversalPosting, feeShare: feeShare, roOpenBalance: roOpenBalance, bodyTotals: bodyTotals, bodyAllocate: bodyAllocate, bodyClosePosting: bodyClosePosting, policyPosting: policyPosting, }; });