diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index 2e44c4b14..7b3e5a4b7 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -3273,6 +3273,7 @@ - ca_gst_registrant - cat_no - category + - cieca_pfl - cieca_stl - cieca_ttl - ciecaid @@ -3314,6 +3315,7 @@ - date_repairstarted - date_scheduled - date_towin + - date_void - ded_amt - ded_note - ded_status @@ -3495,7 +3497,6 @@ - v_model_yr - v_vin - vehicleid - - date_void - voided select_permissions: - role: user @@ -3539,6 +3540,7 @@ - ca_gst_registrant - cat_no - category + - cieca_pfl - cieca_stl - cieca_ttl - ciecaid @@ -3580,6 +3582,7 @@ - date_repairstarted - date_scheduled - date_towin + - date_void - ded_amt - ded_note - ded_status @@ -3762,7 +3765,6 @@ - v_model_yr - v_vin - vehicleid - - date_void - voided filter: bodyshop: @@ -3816,6 +3818,7 @@ - ca_gst_registrant - cat_no - category + - cieca_pfl - cieca_stl - cieca_ttl - ciecaid @@ -3857,6 +3860,7 @@ - date_repairstarted - date_scheduled - date_towin + - date_void - ded_amt - ded_note - ded_status @@ -4039,7 +4043,6 @@ - v_model_yr - v_vin - vehicleid - - date_void - voided filter: bodyshop: diff --git a/hasura/migrations/1693351091138_alter_table_public_bodyshops_add_column_claimscorpid/down.sql b/hasura/migrations/1693351091138_alter_table_public_bodyshops_add_column_claimscorpid/down.sql new file mode 100644 index 000000000..9e77d16e0 --- /dev/null +++ b/hasura/migrations/1693351091138_alter_table_public_bodyshops_add_column_claimscorpid/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."bodyshops" add column "claimscorpid" text +-- null; diff --git a/hasura/migrations/1693351091138_alter_table_public_bodyshops_add_column_claimscorpid/up.sql b/hasura/migrations/1693351091138_alter_table_public_bodyshops_add_column_claimscorpid/up.sql new file mode 100644 index 000000000..1f72bae75 --- /dev/null +++ b/hasura/migrations/1693351091138_alter_table_public_bodyshops_add_column_claimscorpid/up.sql @@ -0,0 +1,2 @@ +alter table "public"."bodyshops" add column "claimscorpid" text + null; diff --git a/server.js b/server.js index fa1c4e157..5c7de4764 100644 --- a/server.js +++ b/server.js @@ -216,6 +216,7 @@ app.post("/qbo/payments", fb.validateFirebaseIdToken, qbo.payments); var data = require("./server/data/data"); app.post("/data/ah", data.autohouse); +app.post("/data/cc", data.claimscorp); app.post("/record-handler/arms", data.arms); var taskHandler = require("./server/tasks/tasks"); diff --git a/server/data/claimscorp.js b/server/data/claimscorp.js new file mode 100644 index 000000000..50e49fd91 --- /dev/null +++ b/server/data/claimscorp.js @@ -0,0 +1,847 @@ +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 = "MMDDYYYY"; + +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: (message, ...data) => logger.log(message, "DEBUG", "api", null, data), + algorithms: { + serverHostKey: ["ssh-rsa", "ssh-dss"], + }, +}; + +exports.default = async (req, res) => { + //Query for the List of Bodyshop Clients. + logger.log("claimscorp-start", "DEBUG", "api", null, null); + const { bodyshops } = await client.request(queries.GET_CLAIMSCORP_SHOPS); + + const specificShopIds = req.body.bodyshopIds; // ['uuid] + const { start, end, skipUpload } = req.body; //YYYY-MM-DD + if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) { + res.sendStatus(401); + return; + } + const allxmlsToUpload = []; + const allErrors = []; + try { + for (const bodyshop of specificShopIds + ? bodyshops.filter((b) => specificShopIds.includes(b.id)) + : bodyshops) { + logger.log("claimscorp-start-shop-extract", "DEBUG", "api", bodyshop.id, { + shopname: bodyshop.shopname, + }); + const erroredJobs = []; + try { + 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).startOf("day") }), + } + ); + + const claimsCorpObject = { + ClaimsCorpExport: { + 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)), + }); + } + + var ret = builder + .create( + { + // version: "1.0", + // encoding: "UTF-8", + //keepNullNodes: true, + }, + claimsCorpObject + ) + .end({ allowEmptyTags: true }); + + allxmlsToUpload.push({ + count: claimsCorpObject.ClaimsCorpExport.RO.length, + xml: ret, + filename: `${bodyshop.claimscorpid}-MIS-${moment().format( + "YYYYMMDDTHHMMss" + )}.xml`, + }); + + 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, + }); + + 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, + })), + }); + } + } + + if (skipUpload) { + for (const xmlObj of allxmlsToUpload) { + fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml); + } + + res.json(allxmlsToUpload); + sendServerEmail({ + subject: `ClaimsCorp Report ${moment().format("MM-DD-YY")}`, + text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} + Uploaded: ${JSON.stringify( + allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })), + null, + 2 + )} + `, + }); + return; + } + + let sftp = new Client(); + sftp.on("error", (errors) => + logger.log("claimscorp-sftp-error", "ERROR", "api", null, { + ...errors, + }) + ); + try { + //Connect to the FTP and upload all. + + await sftp.connect(ftpSetup); + + for (const xmlObj of allxmlsToUpload) { + logger.log("claimscorp-sftp-upload", "DEBUG", "api", null, { + filename: xmlObj.filename, + }); + + const uploadResult = await sftp.put( + Buffer.from(xmlObj.xml), + `/${xmlObj.filename}` + ); + logger.log("claimscorp-sftp-upload-result", "DEBUG", "api", null, { + uploadResult, + }); + } + + //***TODO Change filing naming when creating the cron job. IM_ShopInternalName_DDMMYYYY_HHMMSS.xml + } catch (error) { + logger.log("claimscorp-sftp-error", "ERROR", "api", null, { + ...error, + }); + } finally { + sftp.end(); + } + sendServerEmail({ + subject: `ClaimsCorp Report ${moment().format("MM-DD-YY")}`, + text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} + Uploaded: ${JSON.stringify( + allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })), + null, + 2 + )} + `, + }); + res.sendStatus(200); + } catch (error) { + res.status(200).json(error); + } +}; + +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: { + InsuranceCo: job.ins_co_nm || "", + CompanyName: 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: "", + EstimatorName: `${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)) || + "", + DateofLoss: + (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)) || + "", + StartDate: 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), + Refinish: Dinero(job.job_totals.rates.lar.total).toFormat( + CCDineroFormat + ), + Prep: Dinero().toFormat(CCDineroFormat), + Frame: Dinero(job.job_totals.rates.laf.total).toFormat(CCDineroFormat), + Mechanical: 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 + ), + 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", + FrameHours: job.job_totals.rates.laf.hours.toFixed(2), + MechanicalHours: job.job_totals.rates.lam.hours.toFixed(2), + GlassHours: job.job_totals.rates.lag.hours.toFixed(2), + ElectricalHours: job.job_totals.rates.lae.hours.toFixed(2), + DetailHours: 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: Dinero(job.job_totals.rates.laf.total).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 + OtherCosts: 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, + }); + + 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"; +}; diff --git a/server/data/data.js b/server/data/data.js index c0293175c..077f9f134 100644 --- a/server/data/data.js +++ b/server/data/data.js @@ -1,2 +1,3 @@ exports.autohouse = require("./autohouse").default; -exports.arms = require("./arms").default; +exports.claimscorp = require("./claimscorp").default; +exports.arms = require("./arms").default; \ No newline at end of file diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index 89bd8f654..eb71fae42 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -838,6 +838,179 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz, $bodyshop } `; +exports.CLAIMSCORP_QUERY = `query CLAIMSCORP_EXPORT($start: timestamptz, $bodyshopid: uuid!, $end: timestamptz) { + bodyshops_by_pk(id: $bodyshopid){ + id + shopname + address1 + city + state + zip_post + country + phone + md_ro_statuses + md_order_statuses + claimscorpid + md_responsibility_centers + jc_hourly_rates + cdk_dealerid + pbs_serialnumber + use_paint_scale_data + timezone + } + jobs(where: {_and: [{converted: {_eq: true}}, {updated_at: {_gt: $start}}, {updated_at: {_lte: $end}}, {shopid: {_eq: $bodyshopid}}]}) { + id + created_at + ro_number + status + est_ct_fn + est_ct_ln + ownr_st + ownr_zip + tlos_ind + v_color + v_model_yr + v_model_desc + v_make_desc + v_vin + vehicle { + v_bstyle + } + ins_co_nm + clm_no + loss_date + asgn_date + date_estimated + date_open + scheduled_in + actual_in + scheduled_completion + actual_completion + scheduled_delivery + actual_delivery + date_invoiced + date_exported + rate_la1 + rate_la2 + rate_la3 + rate_la4 + rate_laa + rate_lab + rate_lad + rate_lae + rate_laf + rate_lag + rate_lam + rate_lar + rate_las + rate_lau + rate_ma2s + rate_ma2t + rate_ma3s + rate_mabl + rate_macs + rate_mahw + rate_matd + rate_mapa + rate_mash + job_totals + parts_tax_rates + date_repairstarted + joblines(where: {removed: {_eq: false}}) { + id + line_no + line_ind + status + line_ind + db_price + act_price + mod_lb_hrs + mod_lbr_ty + line_desc + prt_dsmk_m + prt_dsmk_p + part_qty + part_type + oem_partno + lbr_op + profitcenter_part + profitcenter_labor + ah_detail_line + parts_order_lines(order_by: {parts_order: {order_date: desc_nulls_last}} limit: 1){ + parts_order{ + id + order_date + } + } + billlines(order_by: {bill: {date: desc_nulls_last}} limit: 1) { + actual_cost + actual_price + quantity + bill { + vendor { + name + } + invoice_number + date + } + } + } + bills { + id + federal_tax_rate + local_tax_rate + state_tax_rate + is_credit_memo + billlines { + actual_cost + cost_center + id + quantity + } + } + employee_body_rel { + first_name + last_name + employee_number + id + } + employee_csr_rel { + first_name + last_name + employee_number + id + } + employee_prep_rel { + first_name + last_name + employee_number + id + } + employee_refinish_rel { + first_name + last_name + employee_number + id + } + parts_orders(limit: 1, order_by: {created_at: desc}) { + created_at + } + timetickets { + id + rate + cost_center + actualhrs + productivehrs + flat_rate + } + mixdata(limit: 1, order_by: {updated_at: desc}) { + jobid + totalliquidcost + } + } +} +`; + exports.ENTEGRAL_EXPORT = ` query ENTEGRAL_EXPORT($bodyshopid: uuid!) { jobs(where: {_and: [{converted: {_eq: true}}, {shopid: {_eq: $bodyshopid}}]}) { @@ -1384,6 +1557,27 @@ exports.GET_AUTOHOUSE_SHOPS = `query GET_AUTOHOUSE_SHOPS { } `; +exports.GET_CLAIMSCORP_SHOPS = `query GET_CLAIMSCORP_SHOPS { + bodyshops(where: {claimscorpid: {_is_null: false}}){ + id + shopname + address1 + city + state + zip_post + country + phone + md_ro_statuses + md_order_statuses + claimscorpid + md_responsibility_centers + jc_hourly_rates + imexshopid + timezone + } +} +`; + exports.GET_ENTEGRAL_SHOPS = `query GET_AUTOHOUSE_SHOPS { bodyshops(where: {entegral_id: {_is_null: false}}){ id