const path = require("path"); const queries = require("../graphql-client/queries"); const Dinero = require("dinero.js"); const moment = require("moment-timezone"); var builder = require("xmlbuilder2"); const _ = require("lodash"); const logger = require("../utils/logger"); const fs = require("fs"); require("dotenv").config({ path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) }); let Client = require("ssh2-sftp-client"); const client = require("../graphql-client/graphql-client").client; const { sendServerEmail } = require("../email/sendemail"); const CCDineroFormat = "0,0.00"; const AhDateFormat = "MM/DD/YYYY"; const repairOpCodes = ["OP4", "OP9", "OP10"]; const replaceOpCodes = ["OP2", "OP5", "OP11", "OP12"]; const ftpSetup = { host: process.env.CLAIMSCORP_HOST, port: process.env.CLAIMSCORP_PORT, username: process.env.CLAIMSCORP_USER, password: process.env.CLAIMSCORP_PASSWORD, debug: process.env.NODE_ENV !== "production" ? (message, ...data) => logger.log(message, "DEBUG", "api", null, data) : () => {}, algorithms: { serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"] } }; exports.default = async (req, res) => { // Only process if in production environment. if (process.env.NODE_ENV !== "production") { res.sendStatus(403); return; } // Only process if the appropriate token is provided. if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) { res.sendStatus(401); return; } // Send immediate response and continue processing. res.status(202).json({ success: true, message: "Processing request ...", timestamp: new Date().toISOString() }); try { logger.log("claimscorp-start", "DEBUG", "api", null, null); const allXMLResults = []; const allErrors = []; const { bodyshops } = await client.request(queries.GET_CLAIMSCORP_SHOPS); //Query for the List of Bodyshop Clients. const specificShopIds = req.body.bodyshopIds; // ['uuid]; const { start, end, skipUpload } = req.body; //YYYY-MM-DD const shopsToProcess = specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops; logger.log("claimscorp-shopsToProcess-generated", "DEBUG", "api", null, null); if (shopsToProcess.length === 0) { logger.log("claimscorp-shopsToProcess-empty", "DEBUG", "api", null, null); return; } await processShopData(shopsToProcess, start, end, skipUpload, allXMLResults, allErrors); await sendServerEmail({ subject: `ClaimsCorp Report ${moment().format("MM-DD-YY")}`, text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify( allXMLResults.map((x) => ({ imexshopid: x.imexshopid, filename: x.filename, count: x.count, result: x.result })), null, 2 )}` }); logger.log("claimscorp-end", "DEBUG", "api", null, null); } catch (error) { logger.log("claimscorp-error", "ERROR", "api", null, { error: error.message, stack: error.stack }); } }; async function processShopData(shopsToProcess, start, end, skipUpload, allXMLResults, allErrors) { for (const bodyshop of shopsToProcess) { const erroredJobs = []; try { logger.log("claimscorp-start-shop-extract", "DEBUG", "api", bodyshop.id, { shopname: bodyshop.shopname }); const { jobs, bodyshops_by_pk } = await client.request(queries.CLAIMSCORP_QUERY, { bodyshopid: bodyshop.id, start: start ? moment(start).startOf("day") : moment().subtract(5, "days").startOf("day"), ...(end && { end: moment(end).endOf("day") }) }); const claimsCorpObject = { DataFeed: { ShopInfo: { ShopID: bodyshops_by_pk.claimscorpid, ShopName: bodyshops_by_pk.shopname, RO: jobs.map((j) => CreateRepairOrderTag({ ...j, bodyshop: bodyshops_by_pk }, function ({ job, error }) { erroredJobs.push({ job: job, error: error.toString() }); }) ) } } }; if (erroredJobs.length > 0) { logger.log("claimscorp-failed-jobs", "ERROR", "api", bodyshop.id, { count: erroredJobs.length, jobs: JSON.stringify(erroredJobs.map((j) => j.job.ro_number)) }); } const xmlObj = { bodyshopid: bodyshop.id, imexshopid: bodyshop.imexshopid, xml: builder.create({}, claimsCorpObject).end({ allowEmptyTags: true }), filename: `${bodyshop.claimscorpid}-${moment().format("YYYYMMDDTHHMMss")}.xml`, count: claimsCorpObject.DataFeed.ShopInfo.RO.length }; if (skipUpload) { fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml); } else { await uploadViaSFTP(xmlObj); } allXMLResults.push({ bodyshopid: bodyshop.id, imexshopid: bodyshop.imexshopid, claimscorpid: bodyshop.claimscorpid, count: xmlObj.count, filename: xmlObj.filename, result: xmlObj.result }); logger.log("claimscorp-end-shop-extract", "DEBUG", "api", bodyshop.id, { shopname: bodyshop.shopname }); } catch (error) { //Error at the shop level. logger.log("claimscorp-error-shop", "ERROR", "api", bodyshop.id, { error: error.message, stack: error.stack }); allErrors.push({ bodyshopid: bodyshop.id, imexshopid: bodyshop.imexshopid, claimscorpid: bodyshop.claimscorpid, fatal: true, errors: [error.toString()] }); } finally { allErrors.push({ bodyshopid: bodyshop.id, imexshopid: bodyshop.imexshopid, claimscorpid: bodyshop.claimscorpid, errors: erroredJobs.map((ej) => ({ ro_number: ej.job?.ro_number, jobid: ej.job?.id, error: ej.error })) }); } } } async function uploadViaSFTP(xmlObj) { const sftp = new Client(); sftp.on("error", (errors) => logger.log("claimscorp-sftp-connection-error", "ERROR", "api", xmlObj.bodyshopid, { error: errors.message, stack: errors.stack }) ); try { //Connect to the FTP and upload all. await sftp.connect(ftpSetup); try { xmlObj.result = await sftp.put(Buffer.from(xmlObj.xml), `${xmlObj.filename}`); logger.log("claimscorp-sftp-upload", "DEBUG", "api", xmlObj.bodyshopid, { imexshopid: xmlObj.imexshopid, filename: xmlObj.filename, result: xmlObj.result }); } catch (error) { logger.log("claimscorp-sftp-upload-error", "ERROR", "api", xmlObj.bodyshopid, { filename: xmlObj.filename, error: error.message, stack: error.stack }); throw error; } } catch (error) { logger.log("claimscorp-sftp-error", "ERROR", "api", xmlObj.bodyshopid, { error: error.message, stack: error.stack }); throw error; } finally { sftp.end(); } } const CreateRepairOrderTag = (job, errorCallback) => { //Level 2 if (!job.job_totals) { errorCallback({ jobid: job.id, job: job, ro_number: job.ro_number, error: { toString: () => "No job totals for RO." } }); return {}; } const repairCosts = CreateCosts(job); //Calculate detail only lines. const detailAdjustments = job.joblines .filter((jl) => jl.ah_detail_line && jl.mod_lbr_ty) .reduce( (acc, val) => { return { hours: acc.hours + val.mod_lb_hrs, amount: acc.amount.add( Dinero({ amount: Math.round((job.job_totals.rates[val.mod_lbr_ty.toLowerCase()].rate || 0) * val.mod_lb_hrs * 100) }) ) }; }, { hours: 0, amount: Dinero() } ); try { const ret = { RoNumber: job.ro_number, Customer: { CustomerZip: (job.ownr_zip && job.ownr_zip.substring(0, 3)) || "", CustomerState: job.ownr_st || "" }, Vehicle: { Year: job.v_model_yr ? parseInt(job.v_model_yr.match(/\d/g)) ? parseInt(job.v_model_yr.match(/\d/g).join(""), 10) : "" : "", Make: job.v_make_desc || "", Model: job.v_model_desc || "", BodyStyle: (job.vehicle && job.vehicle.v_bstyle) || "", Color: job.v_color || "", VIN: job.v_vin || "" }, Carrier: { UniqueID: job.ins_co_nm || "", InsuranceCompany: job.ins_co_nm || "" }, Claim: job.clm_no || "", Contacts: { PC: job.employee_csr_rel ? `${ job.employee_csr_rel.last_name ? job.employee_csr_rel.last_name : "" }${job.employee_csr_rel.last_name ? ", " : ""}${ job.employee_csr_rel.first_name ? job.employee_csr_rel.first_name : "" }` : "", Phone1: "", Phone2: "", Estimator: `${job.est_ct_ln ? job.est_ct_ln : ""}${ job.est_ct_ln ? ", " : "" }${job.est_ct_fn ? job.est_ct_fn : ""}`, BodyTechnician: job.employee_body_rel ? `${ job.employee_body_rel.last_name ? job.employee_body_rel.last_name : "" }${job.employee_body_rel.last_name ? ", " : ""}${ job.employee_body_rel.first_name ? job.employee_body_rel.first_name : "" }` : "", PaintTechnician: job.employee_refinish_rel ? `${ job.employee_refinish_rel.last_name ? job.employee_refinish_rel.last_name : "" }${job.employee_refinish_rel.last_name ? ", " : ""}${ job.employee_refinish_rel.first_name ? job.employee_refinish_rel.first_name : "" }` : "" }, Dates: { DateCreated: (job.date_estimated && moment(job.date_estimated).format(AhDateFormat)) || "", DateLoss: (job.loss_date && moment(job.loss_date).format(AhDateFormat)) || "", DateFNOL: "", DateContact: "", DateEstimated: (job.date_estimated && moment(job.date_estimated).format(AhDateFormat)) || "", DateScheduled: (job.scheduled_in && moment(job.scheduled_in).tz(job.bodyshop.timezone).format(AhDateFormat)) || "", DateArrived: (job.actual_in && moment(job.actual_in).tz(job.bodyshop.timezone).format(AhDateFormat)) || "", DateFirstPartsOrdered: (job.parts_orders && job.parts_orders[0] && moment(job.parts_orders[0].created_at).tz(job.bodyshop.timezone).format(AhDateFormat)) || "", DateStart: job.date_repairstarted ? (job.date_repairstarted && moment(job.date_repairstarted).tz(job.bodyshop.timezone).format(AhDateFormat)) || "" : (job.date_repairstarted && moment(job.actual_in).tz(job.bodyshop.timezone).format(AhDateFormat)) || "", BodyStart: "", BodyEnd: "", FrameStart: "", FrameEnd: "", PrepStart: "", PrepEnd: "", SprayStart: "", SprayEnd: "", DateReady: (job.actual_completion && moment(job.actual_completion).tz(job.bodyshop.timezone).format(AhDateFormat)) || "", DateScheduledDelivery: (job.scheduled_delivery && moment(job.scheduled_delivery).tz(job.bodyshop.timezone).format(AhDateFormat)) || "", DateDelivered: (job.actual_delivery && moment(job.actual_delivery).tz(job.bodyshop.timezone).format(AhDateFormat)) || "", DateClosed: (job.date_invoiced && moment(job.date_invoiced).tz(job.bodyshop.timezone).format(AhDateFormat)) || "", BilledDate: "", PaidInFullDate: "", ROStatus: job.tlos_ind ? "TOT" : StatusMapping(job.status, job.bodyshop.md_ro_statuses) }, Sales: { Body: Dinero(job.job_totals.rates.lab.total) .add(Dinero(job.job_totals.rates.laa.total)) .add(Dinero(job.job_totals.rates.lad.total)) .add(Dinero(job.job_totals.rates.las.total)) .toFormat(CCDineroFormat), Paint: Dinero(job.job_totals.rates.lar.total).toFormat(CCDineroFormat), Prep: Dinero().toFormat(CCDineroFormat), Frame: Dinero(job.job_totals.rates.laf.total).toFormat(CCDineroFormat), Mech: Dinero(job.job_totals.rates.lam.total).toFormat(CCDineroFormat), Glass: Dinero(job.job_totals.rates.lag.total).toFormat(CCDineroFormat), Elec: Dinero(job.job_totals.rates.lae.total).toFormat(CCDineroFormat), Detail: detailAdjustments.amount.toFormat(CCDineroFormat), Reassem: Dinero().toFormat(CCDineroFormat), OtherLabor: Dinero(job.job_totals.rates.la1.total) .add(Dinero(job.job_totals.rates.la2.total)) .add(Dinero(job.job_totals.rates.la3.total)) .add(Dinero(job.job_totals.rates.la4.total)) .add(Dinero(job.job_totals.rates.lau.total)) .subtract(detailAdjustments.amount) .toFormat(CCDineroFormat), BMatl: Dinero(job.job_totals.rates.mash.total).toFormat(CCDineroFormat), PMatl: Dinero(job.job_totals.rates.mapa.total).toFormat(CCDineroFormat), OEM: Dinero(job.job_totals.parts.parts.list.PAN && job.job_totals.parts.parts.list.PAN.total) .add(Dinero(job.job_totals.parts.parts.list.PAP && job.job_totals.parts.parts.list.PAP.total)) .toFormat(CCDineroFormat), LKQ: Dinero(job.job_totals.parts.parts.list.PAL && job.job_totals.parts.parts.list.PAL.total).toFormat( CCDineroFormat ), AM: Dinero(job.job_totals.parts.parts.list.PAA && job.job_totals.parts.parts.list.PAA.total).toFormat( CCDineroFormat ), MechParts: Dinero().toFormat(CCDineroFormat), OtherParts: Dinero(job.job_totals.parts.parts.list.PAO && job.job_totals.parts.parts.list.PAO.total).toFormat( CCDineroFormat ), OtherSales: Dinero(job.job_totals.additional.storage).toFormat(CCDineroFormat), Sublet: Dinero(job.job_totals.parts.sublets.total).toFormat(CCDineroFormat), Towing: Dinero(job.job_totals.additional.towing).toFormat(CCDineroFormat), Storage: "0.00", Rental: job.job_totals.additional.additionalCostItems.includes("ATS Amount") === true ? Dinero( job.job_totals.additional.additionalCostItems[ job.job_totals.additional.additionalCostItems.indexOf("ATS Amount") ].total ).toFormat(CCDineroFormat) : Dinero().toFormat(CCDineroFormat), HazWaste: Dinero().toFormat(CCDineroFormat), Discounts: Dinero(job.job_totals.additional.adjustments).toFormat(CCDineroFormat), Tax: Dinero(job.job_totals.totals.local_tax) .add(Dinero(job.job_totals.totals.state_tax)) .add(Dinero(job.job_totals.totals.federal_tax)) .add(Dinero(job.job_totals.additional.pvrt)) .toFormat(CCDineroFormat), NetSaleTotal: Dinero(job.job_totals.totals.subtotal).toFormat(CCDineroFormat), SaleTotal: Dinero(job.job_totals.totals.total_repairs).toFormat(CCDineroFormat) }, SaleHours: { Body: job.job_totals.rates.lab.hours.toFixed(2), BodyRepairHours: job.joblines .filter((line) => repairOpCodes.includes(line.lbr_op)) .reduce((acc, val) => acc + val.mod_lb_hrs, 0) .toFixed(2), BodyReplacehours: job.joblines .filter((line) => replaceOpCodes.includes(line.lbr_op)) .reduce((acc, val) => acc + val.mod_lb_hrs, 0) .toFixed(2), Paint: job.job_totals.rates.lar.hours.toFixed(2), Prep: "0.00", Frame: job.job_totals.rates.laf.hours.toFixed(2), Mech: job.job_totals.rates.lam.hours.toFixed(2), Glass: job.job_totals.rates.lag.hours.toFixed(2), Elec: job.job_totals.rates.lae.hours.toFixed(2), Detail: detailAdjustments.hours, Reassem: "0.00", Other: ( job.job_totals.rates.la1.hours + job.job_totals.rates.la2.hours + job.job_totals.rates.la3.hours + job.job_totals.rates.la4.hours + job.job_totals.rates.lau.hours - detailAdjustments.hours ).toFixed(2), TotalHours: job.joblines.reduce((acc, val) => acc + val.mod_lb_hrs, 0).toFixed(2) }, Costs: { Body: repairCosts.BodyLaborTotalCost.toFormat(CCDineroFormat), Paint: repairCosts.RefinishLaborTotalCost.toFormat(CCDineroFormat), Prep: Dinero().toFormat(CCDineroFormat), Frame: repairCosts.FrameLaborTotalCost.toFormat(CCDineroFormat), Mech: repairCosts.MechanicalLaborTotalCost.toFormat(CCDineroFormat), Glass: repairCosts.GlassLaborTotalCost.toFormat(CCDineroFormat), Elec: repairCosts.ElectricalLaborTotalCost.toFormat(CCDineroFormat), Detail: Dinero().toFormat(CCDineroFormat), Reassem: Dinero().toFormat(CCDineroFormat), OtherLabor: repairCosts.LaborMiscTotalCost.toFormat(CCDineroFormat), Bmatl: repairCosts.BMTotalCost.toFormat(CCDineroFormat), Pmatl: repairCosts.PMTotalCost.toFormat(CCDineroFormat), OEM: repairCosts.PartsOemCost.toFormat(CCDineroFormat), LKQ: repairCosts.PartsRecycledCost.toFormat(CCDineroFormat), AM: repairCosts.PartsAMCost.toFormat(CCDineroFormat), MechParts: Dinero().toFormat(CCDineroFormat), OtherParts: Dinero().toFormat(CCDineroFormat), //Check Synergy OtherCost: repairCosts.PartsOtherCost.toFormat(CCDineroFormat), Sublet: repairCosts.SubletTotalCost.toFormat(CCDineroFormat), Towing: repairCosts.TowingTotalCost.toFormat(CCDineroFormat), Storage: repairCosts.StorageTotalCost.toFormat(CCDineroFormat), Rental: Dinero().toFormat(CCDineroFormat), HazWaste: Dinero().toFormat(CCDineroFormat), CostTotal: repairCosts.TotalCost.toFormat(CCDineroFormat) }, CostHours: { Body: repairCosts.BodyLaborTotalHrs.toFixed(2), Paint: repairCosts.RefinishLaborTotalHrs.toFixed(2), Prep: "0.00", Frame: repairCosts.FrameLaborTotalHrs.toFixed(2), Mech: repairCosts.MechanicalLaborTotalHrs.toFixed(2), Glass: repairCosts.GlassLaborTotalHrs.toFixed(2), Elec: repairCosts.ElectricalLaborTotalHrs.toFixed(2), Detail: "0.00", Other: repairCosts.LaborMiscTotalHrs.toFixed(2), CostTotalHours: repairCosts.TotalHrs.toFixed(2) } }; return ret; } catch (error) { logger.log("claimscorp-job-calculate-error", "ERROR", "api", null, { error: error.message, stack: error.stack }); errorCallback({ jobid: job.id, ro_number: job.ro_number, error }); } }; const CreateCosts = (job) => { //Create a mapping based on AH Requirements //For DMS, the keys in the object below are the CIECA part types. const billTotalsByCostCenters = job.bills.reduce((bill_acc, bill_val) => { //At the bill level. bill_val.billlines.map((line_val) => { //At the bill line level. if (!bill_acc[line_val.cost_center]) bill_acc[line_val.cost_center] = Dinero(); bill_acc[line_val.cost_center] = bill_acc[line_val.cost_center].add( Dinero({ amount: Math.round((line_val.actual_cost || 0) * 100) }) .multiply(line_val.quantity) .multiply(bill_val.is_credit_memo ? -1 : 1) ); return null; }); return bill_acc; }, {}); //If the hourly rates for job costing are set, add them in. if ( job.bodyshop.jc_hourly_rates && (job.bodyshop.jc_hourly_rates.mapa || typeof job.bodyshop.jc_hourly_rates.mapa === "number" || isNaN(job.bodyshop.jc_hourly_rates.mapa) === false) ) { if (!billTotalsByCostCenters[job.bodyshop.md_responsibility_centers.defaults.costs.MAPA]) billTotalsByCostCenters[job.bodyshop.md_responsibility_centers.defaults.costs.MAPA] = Dinero(); if (job.bodyshop.use_paint_scale_data === true) { if (job.mixdata.length > 0) { billTotalsByCostCenters[job.bodyshop.md_responsibility_centers.defaults.costs.MAPA] = Dinero({ amount: Math.round(((job.mixdata[0] && job.mixdata[0].totalliquidcost) || 0) * 100) }); } else { billTotalsByCostCenters[job.bodyshop.md_responsibility_centers.defaults.costs.MAPA] = billTotalsByCostCenters[ job.bodyshop.md_responsibility_centers.defaults.costs.MAPA ].add( Dinero({ amount: Math.round((job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mapa * 100) || 0) }).multiply(job.job_totals.rates.mapa.hours) ); } } else { billTotalsByCostCenters[job.bodyshop.md_responsibility_centers.defaults.costs.MAPA] = billTotalsByCostCenters[ job.bodyshop.md_responsibility_centers.defaults.costs.MAPA ].add( Dinero({ amount: Math.round((job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mapa * 100) || 0) }).multiply(job.job_totals.rates.mapa.hours) ); } } if (job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mash) { if (!billTotalsByCostCenters[job.bodyshop.md_responsibility_centers.defaults.costs.MASH]) billTotalsByCostCenters[job.bodyshop.md_responsibility_centers.defaults.costs.MASH] = Dinero(); billTotalsByCostCenters[job.bodyshop.md_responsibility_centers.defaults.costs.MASH] = billTotalsByCostCenters[ job.bodyshop.md_responsibility_centers.defaults.costs.MASH ].add( Dinero({ amount: Math.round((job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mash * 100) || 0) }).multiply(job.job_totals.rates.mash.hours) ); } //Uses CIECA Labor types. const ticketTotalsByCostCenter = job.timetickets.reduce((ticket_acc, ticket_val) => { //At the invoice level. if (!ticket_acc[ticket_val.cost_center]) ticket_acc[ticket_val.cost_center] = Dinero(); ticket_acc[ticket_val.cost_center] = ticket_acc[ticket_val.cost_center].add( Dinero({ amount: Math.round((ticket_val.rate || 0) * 100) }).multiply((ticket_val.flat_rate ? ticket_val.productivehrs : ticket_val.actualhrs) || 0) ); return ticket_acc; }, {}); const ticketHrsByCostCenter = job.timetickets.reduce((ticket_acc, ticket_val) => { //At the invoice level. if (!ticket_acc[ticket_val.cost_center]) ticket_acc[ticket_val.cost_center] = 0; ticket_acc[ticket_val.cost_center] = ticket_acc[ticket_val.cost_center] + (ticket_val.flat_rate ? ticket_val.productivehrs : ticket_val.actualhrs) || 0; return ticket_acc; }, {}); //CIECA STANDARD MAPPING OBJECT. const ciecaObj = { ATS: "ATS", LA1: "LA1", LA2: "LA2", LA3: "LA3", LA4: "LA4", LAA: "LAA", LAB: "LAB", LAD: "LAD", LAE: "LAE", LAF: "LAF", LAG: "LAG", LAM: "LAM", LAR: "LAR", LAS: "LAS", LAU: "LAU", PAA: "PAA", PAC: "PAC", PAG: "PAG", PAL: "PAL", PAM: "PAM", PAN: "PAN", PAO: "PAO", PAP: "PAP", PAR: "PAR", PAS: "PAS", TOW: "TOW", MAPA: "MAPA", MASH: "MASH", PASL: "PASL" }; const defaultCosts = job.bodyshop.cdk_dealerid || job.bodyshop.pbs_serialnumber ? ciecaObj : job.bodyshop.md_responsibility_centers.defaults.costs; return { PartsTotalCost: Object.keys(billTotalsByCostCenters).reduce((acc, key) => { if ( key !== defaultCosts.PAS && key !== defaultCosts.PASL && key !== defaultCosts.MAPA && key !== defaultCosts.MASH && key !== defaultCosts.TOW ) return acc.add(billTotalsByCostCenters[key]); return acc; }, Dinero()), PartsOemCost: (billTotalsByCostCenters[defaultCosts.PAN] || Dinero()).add( billTotalsByCostCenters[defaultCosts.PAP] || Dinero() ), PartsAMCost: billTotalsByCostCenters[defaultCosts.PAA] || Dinero(), PartsReconditionedCost: billTotalsByCostCenters[defaultCosts.PAM] || Dinero(), PartsRecycledCost: billTotalsByCostCenters[defaultCosts.PAL] || Dinero(), PartsOtherCost: billTotalsByCostCenters[defaultCosts.PAO] || Dinero(), SubletTotalCost: billTotalsByCostCenters[defaultCosts.PAS] || Dinero(billTotalsByCostCenters[defaultCosts.PASL] || Dinero()), BodyLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAB] || Dinero(), BodyLaborTotalHrs: ticketHrsByCostCenter[defaultCosts.LAB] || 0, RefinishLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAR] || Dinero(), RefinishLaborTotalHrs: ticketHrsByCostCenter[defaultCosts.LAR] || 0, MechanicalLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAM] || Dinero(), MechanicalLaborTotalHrs: ticketHrsByCostCenter[defaultCosts.LAM] || 0, StructuralLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAS] || Dinero(), StructuralLaborTotalHrs: ticketHrsByCostCenter[defaultCosts.LAS] || 0, ElectricalLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAE] || Dinero(), ElectricalLaborTotalHrs: ticketHrsByCostCenter[defaultCosts.LAE] || 0, FrameLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAF] || Dinero(), FrameLaborTotalHrs: ticketHrsByCostCenter[defaultCosts.LAF] || 0, GlassLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAG] || Dinero(), GlassLaborTotalHrs: ticketHrsByCostCenter[defaultCosts.LAG] || 0, DetailLaborTotalCost: Dinero(), // ticketTotalsByCostCenter[defaultCosts.LAD] || Dinero(), LaborMiscTotalCost: (ticketTotalsByCostCenter[defaultCosts.LA1] || Dinero()) .add(ticketTotalsByCostCenter[defaultCosts.LA2] || Dinero()) .add(ticketTotalsByCostCenter[defaultCosts.LA2] || Dinero()) .add(ticketTotalsByCostCenter[defaultCosts.LA3] || Dinero()) .add(ticketTotalsByCostCenter[defaultCosts.LA4] || Dinero()) .add(ticketTotalsByCostCenter[defaultCosts.LAU] || Dinero()), LaborMiscTotalHrs: (ticketHrsByCostCenter[defaultCosts.LA1] || 0) + (ticketHrsByCostCenter[defaultCosts.LA2] || 0) + (ticketHrsByCostCenter[defaultCosts.LA3] || 0) + (ticketHrsByCostCenter[defaultCosts.LA4] || 0) + (ticketHrsByCostCenter[defaultCosts.LAU] || 0), PMTotalCost: billTotalsByCostCenters[defaultCosts.MAPA] || Dinero(), BMTotalCost: billTotalsByCostCenters[defaultCosts.MASH] || Dinero(), MiscTotalCost: billTotalsByCostCenters[defaultCosts.PAO] || Dinero(), TowingTotalCost: billTotalsByCostCenters[defaultCosts.TOW] || Dinero(), StorageTotalCost: Dinero(), DetailTotal: Dinero(), DetailTotalCost: Dinero(), SalesTaxTotalCost: Dinero(), LabourTotalCost: Object.keys(ticketTotalsByCostCenter).reduce((acc, key) => { return acc.add(ticketTotalsByCostCenter[key]); }, Dinero()), TotalCost: Object.keys(billTotalsByCostCenters).reduce((acc, key) => { return acc.add(billTotalsByCostCenters[key]); }, Dinero()), TotalHrs: job.timetickets.reduce((acc, ticket_val) => { return acc + (ticket_val.flat_rate ? ticket_val.productivehrs : ticket_val.actualhrs) || 0; }, 0) }; }; const StatusMapping = (status, md_ro_statuses) => { //Possible return statuses CLO, CAN, OPN const { default_imported, default_open, default_scheduled, default_arrived, default_completed, default_delivered, default_invoiced, default_exported, default_void } = md_ro_statuses; if ( status === default_open || status === default_imported || status === default_scheduled || status === default_arrived || status === default_completed || status === default_delivered || md_ro_statuses.production_statuses.includes(status) ) return "OPN"; else if (status === default_invoiced || status === default_exported) return "CLO"; else if (status === default_void) return "CAN"; else return "UNDEFINED"; };