const path = require("path"); const queries = require("../graphql-client/queries"); const Dinero = require("dinero.js"); const moment = require("moment"); var builder = require("xmlbuilder2"); const _ = require("lodash"); 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 AHDineroFormat = "0.00"; const AhDateFormat = "MMDDYYYY"; const repairOpCodes = ["OP4", "OP9", "OP10"]; const replaceOpCodes = ["OP2", "OP5", "OP11", "OP12"]; const ftpSetup = { host: process.env.AUTOHOUSE_HOST, port: process.env.AUTOHOUSE_PORT, username: process.env.AUTOHOUSE_USER, password: process.env.AUTOHOUSE_PASSWORD, debug: console.log, algorithms: { serverHostKey: ["ssh-rsa", "ssh-dss"], }, }; exports.default = async (req, res) => { //Query for the List of Bodyshop Clients. console.log("Starting Autohouse datapump request."); const { bodyshops } = await client.request(queries.GET_AUTOHOUSE_SHOPS); const allxmlsToUpload = []; const allErrors = []; try { for (const bodyshop of bodyshops) { console.log("Starting extract for ", bodyshop.shopname); const erroredJobs = []; try { const { jobs } = await client.request(queries.AUTOHOUSE_QUERY, { bodyshopid: bodyshop.id, }); const autoHouseObject = { AutoHouseExport: { RepairOrder: jobs.map((j) => CreateRepairOrderTag( { ...j, bodyshop }, function ({ job, error }) { erroredJobs.push({ job: job, error: error.toString() }); } ) ), }, }; console.log( bodyshop.shopname, "***Number of Failed jobs***: ", erroredJobs.length, JSON.stringify(erroredJobs.map((j) => j.job.ro_number)) ); var ret = builder .create(autoHouseObject, { version: "1.0", encoding: "UTF-8", }) .end({ pretty: true, allowEmptyTags: true }); allxmlsToUpload.push({ xml: ret, filename: `IM_${bodyshop.imexshopid}_${moment().format( "DDMMYYYY_HHMMSS" )}.xml`, }); console.log("Finished extract for shop ", bodyshop.shopname); } catch (error) { //Error at the shop level. console.log("Error at shop level", bodyshop.shopname, error); allErrors.push({ bodyshopid: bodyshop.id, imexshopid: bodyshop.imexshopid, fatal: true, errors: [error.toString()], }); } finally { allErrors.push({ bodyshopid: bodyshop.id, imexshopid: bodyshop.imexshopid, errors: erroredJobs, }); } } let sftp = new Client(); sftp.on("error", (errors) => console.log("Error in FTP client", JSON.stringify(errors)) ); try { //Connect to the FTP and upload all. await sftp.connect(ftpSetup); for (const xmlObj of allxmlsToUpload) { console.log("Uploading", xmlObj.filename); const uploadResult = await sftp.put( Buffer.from(xmlObj.xml), `/${xmlObj.filename}` ); console.log( "🚀 ~ file: autohouse.js ~ line 94 ~ uploadResult", uploadResult ); } //***TODO Change filing naming when creating the cron job. IM_ShopInternalName_DDMMYYYY_HHMMSS.xml } catch (error) { console.log("Error when connecting to FTP", error); } finally { sftp.end(); } res.sendStatus(200); } catch (error) { res.status(200).json(error); } }; const CreateRepairOrderTag = (job, errorCallback) => { //Level 2 const repairCosts = CreateCosts(job); try { const ret = { RepairOrderInformation: { ShopInternalName: job.bodyshop.autohouseid, ID: parseInt(job.ro_number.match(/\d/g).join(""), 10), RO: job.ro_number, Est: parseInt(job.ro_number.match(/\d/g).join(""), 10), //We no longer use estimate id. GUID: job.id, TransType: StatusMapping(job.status, job.bodyshop.md_ro_statuses), ShopName: job.bodyshop.shopname, ShopAddress: job.bodyshop.address1, ShopCity: job.bodyshop.city, ShopState: job.bodyshop.state, ShopZip: job.bodyshop.zip_post, ShopPhone: job.bodyshop.phone, EstimatorID: `${job.est_ct_ln ? job.est_ct_ln : ""}${ job.est_ct_ln ? ", " : "" }${job.est_ct_fn ? job.est_ct_fn : ""}`, EstimatorName: `${job.est_ct_ln ? job.est_ct_ln : ""}${ job.est_ct_ln ? ", " : "" }${job.est_ct_fn ? job.est_ct_fn : ""}`, }, CustomerInformation: { FirstName: job.ownr_fn, LastName: job.ownr_ln, Street: job.ownr_addr1, City: job.ownr_city, State: job.ownr_st, Zip: job.ownr_zip, Phone1: job.ownr_ph1, Phone2: null, Phone2Extension: null, Phone3: null, Phone3Extension: null, FileComments: null, Source: null, Email: job.ownr_ea, RetWhsl: null, Cat: null, InsuredorClaimantFlag: null, }, VehicleInformation: { Year: job.v_model_yr, Make: job.v_make_desc, Model: job.v_model_desc, VIN: job.v_vin, License: job.plate_no, MileageIn: job.kmin, Vehiclecolor: job.v_color, VehicleProductionDate: null, VehiclePaintCode: null, VehicleTrimCode: null, VehicleBodyStyle: null, DriveableFlag: job.driveable ? "Y" : "N", }, InsuranceInformation: { InsuranceCo: job.ins_co_nm, CompanyName: job.ins_co_nm, Address: job.ins_addr1, City: job.ins_addr1, State: job.ins_city, Zip: job.ins_zip, Phone: job.ins_ph1, Fax: null, ClaimType: null, LossType: null, Policy: null, Claim: job.clm_no, InsuredLastName: null, InsuredFirstName: null, ClaimantLastName: null, ClaimantFirstName: null, Assignment: null, InsuranceAgentLastName: null, InsuranceAgentFirstName: null, InsAgentPhone: null, InsideAdjuster: null, OutsideAdjuster: null, }, Dates: { DateofLoss: job.loss_date && moment(job.loss_date).format(AhDateFormat), InitialCustomerContactDate: null, FirstFollowUpDate: null, ReferralDate: null, EstimateAppointmentDate: null, SecondFollowUpDate: null, AssignedDate: null, EstComplete: null, CustomerAuthorizationDate: null, InsuranceAuthorizationDate: null, DateOpened: job.date_open && moment(job.date_open).format(AhDateFormat), ScheduledArrivalDate: job.scheduled_in && moment(job.scheduled_in).format(AhDateFormat), CarinShop: job.actual_in && moment(job.actual_in).format(AhDateFormat), InsInspDate: null, StartDate: null, PartsOrder: null, TeardownHold: null, SupplementSubmittedDate: null, SupplementApprovedDate: null, AssntoBody: null, AssntoMech: null, AssntoPaint: null, AssntoDetail: null, PromiseDate: job.scheduled_completion && moment(job.scheduled_completion).format(AhDateFormat), InsuranceTargetOut: null, CarComplete: job.actual_completion && moment(job.actual_completion).format(AhDateFormat), DeliveryAppointmentDate: job.scheduled_delivery && moment(job.scheduled_delivery).format(AhDateFormat), DateClosed: job.date_invoiced && moment(job.date_invoiced).format(AhDateFormat), CustomerPaidInFullDate: null, InsurancePaidInFullDate: null, CustPickup: job.actual_delivery && moment(job.actual_delivery).format(AhDateFormat), AccountPostedDate: job.date_exported && moment(job.date_exported).format(AhDateFormat), CSIProcessedDate: null, ThankYouLetterSent: null, AdditionalFollowUpDate: null, }, Rates: { BodyRate: job.rate_lab, RefinishRate: job.rate_lar, MechanicalRate: job.rate_lam, StructuralRate: job.rate_las, PMRate: job.rate_mapa, BMRate: job.rate_mash, TaxRate: null, StorageRateperDay: null, DaysStored: null, }, EstimateTotals: { BodyHours: null, RefinishHours: null, MechanicalHours: null, StructuralHours: null, PartsTotal: null, PartsOEM: null, PartsAM: null, PartsReconditioned: null, PartsRecycled: null, PartsOther: null, SubletTotal: null, BodyLaborTotal: null, RefinishLaborTotal: null, MechanicalLaborTotal: null, StructuralLaborTotal: null, MiscellaneousChargeTotal: null, PMTotal: null, BMTotal: null, MiscTotal: null, TowingTotal: null, StorageTotal: null, DetailTotal: null, SalesTaxTotal: null, GrossTotal: null, DeductibleTotal: null, DepreciationTotal: null, Discount: null, CustomerPay: null, InsurancePay: null, Deposit: null, AmountDue: null, }, SupplementTotals: { BodyHours: null, RefinishHours: null, MechanicalHours: null, StructuralHours: null, PartsTotal: null, PartsOEM: null, PartsAM: null, PartsReconditioned: null, PartsRecycled: null, PartsOther: null, SubletTotal: null, BodyLaborTotal: null, RefinishLaborTotal: null, MechanicalLaborTotal: null, StructuralLaborTotal: null, MiscellaneousChargeTotal: null, PMTotal: null, BMTotal: null, MiscTotal: null, TowingTotal: null, StorageTotal: null, DetailTotal: null, SalesTaxTotal: null, GrossTotal: null, DeductibleTotal: null, DepreciationTotal: null, Discount: null, CustomerPay: null, InsurancePay: null, Deposit: null, AmountDue: null, }, RevisedTotals: { BodyHours: job.job_totals.rates.lab.hours, BodyRepairHours: job.joblines .filter((line) => repairOpCodes.includes(line.lbr_op)) .reduce((acc, val) => acc + val.mod_lb_hrs, 0), BodyReplaceHours: job.joblines .filter((line) => replaceOpCodes.includes(line.lbr_op)) .reduce((acc, val) => acc + val.mod_lb_hrs, 0), RefinishHours: job.job_totals.rates.lar.hours, MechanicalHours: job.job_totals.rates.lam.hours, StructuralHours: job.job_totals.rates.las.hours, PartsTotal: Dinero(job.job_totals.parts.parts.total).toFormat( AHDineroFormat ), PartsTotalCost: repairCosts.PartsTotalCost.toFormat(AHDineroFormat), PartsOEM: 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(AHDineroFormat), PartsOEMCost: repairCosts.PartsOemCost.toFormat(AHDineroFormat), PartsAM: Dinero( job.job_totals.parts.parts.list.PAA && job.job_totals.parts.parts.list.PAA.total ).toFormat(AHDineroFormat), PartsAMCost: repairCosts.PartsAMCost.toFormat(AHDineroFormat), PartsReconditioned: null, PartsReconditionedCost: repairCosts.PartsReconditionedCost.toFormat(AHDineroFormat), PartsRecycled: Dinero( job.job_totals.parts.parts.list.PAR && job.job_totals.parts.parts.list.PAR.total ).toFormat(AHDineroFormat), PartsRecycledCost: null, PartsOther: Dinero( job.job_totals.parts.parts.list.PAO && job.job_totals.parts.parts.list.PAO.total ).toFormat(AHDineroFormat), PartsOtherCost: null, SubletTotal: Dinero(job.job_totals.parts.sublets.total).toFormat( AHDineroFormat ), SubletTotalCost: 0, BodyLaborTotal: Dinero(job.job_totals.rates.lab.total).toFormat( AHDineroFormat ), BodyLaborTotalCost: 0, RefinishLaborTotal: Dinero(job.job_totals.rates.lar.total).toFormat( AHDineroFormat ), RefinishLaborTotalCost: 0, MechanicalLaborTotal: Dinero(job.job_totals.rates.lam.total).toFormat( AHDineroFormat ), MechanicalLaborTotalCost: 0, StructuralLaborTotal: Dinero(job.job_totals.rates.las.total).toFormat( AHDineroFormat ), StructuralLaborTotalCost: null, MiscellaneousChargeTotal: null, MiscellaneousChargeTotalCost: null, PMTotal: Dinero(job.job_totals.rates.mapa.total).toFormat( AHDineroFormat ), PMTotalCost: 0, BMTotal: Dinero(job.job_totals.rates.mash.total).toFormat( AHDineroFormat ), BMTotalCost: 0, MiscTotal: 0, MiscTotalCost: 0, TowingTotal: Dinero(job.job_totals.additional.towing).toFormat( AHDineroFormat ), TowingTotalCost: null, StorageTotal: Dinero(job.job_totals.additional.storage).toFormat( AHDineroFormat ), StorageTotalCost: null, DetailTotal: null, DetailTotalCost: null, SalesTaxTotal: Dinero(job.job_totals.totals.local_tax) .add(Dinero(job.job_totals.totals.state_tax)) .add(Dinero(job.job_totals.totals.federal_tax)) .toFormat(AHDineroFormat), SalesTaxTotalCost: null, GrossTotal: Dinero(job.job_totals.totals.net_repairs).toFormat( AHDineroFormat ), DeductibleTotal: job.ded_amt, DepreciationTotal: Dinero( job.job_totals.totals.custPayable.dep_taxes ).toFormat(AHDineroFormat), Discount: Dinero(job.job_totals.additional.adjustments).toFormat( AHDineroFormat ), CustomerPay: Dinero(job.job_totals.totals.custPayable.total).toFormat( AHDineroFormat ), InsurancePay: 0, Deposit: 0, AmountDue: 0, }, Misc: { ProductionStatus: null, StatusDescription: null, Hub50Comment: null, DateofChange: null, BodyTechName: null, TotalLossYN: null, InsScreenCommentsLine1: null, InsScreenCommentsLine2: null, AssignmentCaller: null, AssignmentDivision: null, LocationofPrimaryImpact: "12", LocationofSecondaryImpact: null, PaintTechID: null, PaintTechName: null, ImportType: null, ImportFile: null, GSTTax: null, RepairDelayStatusCode: null, RepairDelaycomment: null, AgentMktgID: null, AgentCity: null, Picture1: null, Picture2: null, ExtNoteDate: null, RentalOrdDate: null, RentalPUDate: null, RentalDueDate: null, RentalActRetDate: null, RentalCompanyID: null, CSIID: null, InsGroupCode: null, }, DetailLines: { DetailLine: job.joblines.length > 0 ? job.joblines.map((jl) => GenerateDetailLines(jl, job.bodyshop.md_order_statuses) ) : [generateNullDetailLine()], }, }; return ret; } catch (error) { console.log("Error calculating job", error); errorCallback({ job, error }); } }; const CreateCosts = (job) => { //Create a mapping based on AH Requirements const billTotalsByCostCenters = job.bills.reduce((bill_acc, bill_val) => { //At the bill level. bill_val.billlines.map((line_val) => { //At the bill line level. //console.log("JobCostingPartsTable -> line_val", line_val); 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; }, {}); const materialsHours = { mapaHrs: 0, mashHrs: 0 }; //If the hourly rates for job costing are set, add them in. if (job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mapa) { if ( !billTotalsByCostCenters[ job.bodyshop.md_responsibility_centers.defaults.costs.MAPA ] ) billTotalsByCostCenters[ job.bodyshop.md_responsibility_centers.defaults.costs.MAPA ] = Dinero(); billTotalsByCostCenters[ job.bodyshop.md_responsibility_centers.defaults.costs.MAPA ] = billTotalsByCostCenters[ job.bodyshop.md_responsibility_centers.defaults.costs.MAPA ].add( Dinero({ amount: (job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mapa * 100) || 0, }).multiply(materialsHours.mapaHrs) ); } 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.actualhrs || ticket_val.productivehrs || 0) ); return ticket_acc; }, {} ); const defaultCosts = job.bodyshop.md_responsibility_centers.defaults.costs; return { PartsTotalCost: Object.keys(billTotalsByCostCenters).reduce((acc, key) => { return acc.add(billTotalsByCostCenters[key]); }, Dinero()), PartsOemCost: (billTotalsByCostCenters[defaultCosts.PAN] || Dinero()).add( billTotalsByCostCenters[defaultCosts.PAP] || Dinero() ), PartsAMCost: billTotalsByCostCenters[defaultCosts.PAA] || Dinero(), PartsReconditionedCost: Dinero(), PartsRecycledCost: billTotalsByCostCenters[defaultCosts.PAR] || Dinero(), PartsOtherCost: billTotalsByCostCenters[defaultCosts.PAO] || Dinero(), SubletTotalCost: billTotalsByCostCenters[defaultCosts.PAS] || Dinero(), BodyLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAB] || Dinero(), RefinishLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAR] || Dinero(), MechanicalLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAM] || Dinero(), StructuralLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAS] || Dinero(), 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(), }; }; const StatusMapping = (status, md_ro_statuses) => { //EST, SCH, ARR, IPR, RDY, DEL, CLO, CAN, UNDEFINED. 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) return "EST"; else if (status === default_scheduled) return "SCH"; else if (status === default_arrived) return "ARR"; else if (status === default_completed) return "RDY"; else if (status === default_delivered) return "DEL"; else if (status === default_invoiced || status === default_exported) return "CLO"; else if (status === default_void) return "CLO"; else if (md_ro_statuses.production_statuses.includes(status)) return "IPR"; else return "UNDEFINED"; // default: return "UNDEFINED" }; const GenerateDetailLines = (line, statuses) => { const ret = { BackOrdered: line.status === statuses.default_bo ? "1" : "0", Cost: line.billlines[0] && (line.billlines[0].actual_cost * line.billlines[0].quantity).toFixed(2), Critical: null, Description: line.line_desc, DiscountMarkup: null, InvoiceNumber: line.billlines[0] && line.billlines[0].bill.invoice_number, IOUPart: null, LineNumber: line.line_no, MarkUp: null, OrderedOn: null, OriginalCost: null, OriginalInvoiceNumber: null, PriceEach: line.billlines[0] && line.billlines[0].actual_cost, PartNumber: _.escape(line.oem_partno), ProfitPercent: null, PurchaseOrderNumber: null, Qty: line.part_qty, Status: line.status, SupplementNumber: null, Type: line.part_type, Vendor: line.billlines[0] && line.billlines[0].bill.vendor.name, VendorPaid: null, VendorPrice: line.billlines[0] && line.billlines[0].actual_price, Deleted: null, ExpectedOn: null, ReceivedOn: null, OrderedBy: null, ShipVia: null, VendorContact: null, EstimateAmount: line.act_price, }; return ret; }; const generateNullDetailLine = () => { return { BackOrdered: "0", Cost: 0, Critical: null, Description: "No Lines on Estimate", DiscountMarkup: null, InvoiceNumber: null, IOUPart: null, LineNumber: 0, MarkUp: null, OrderedOn: null, OriginalCost: null, OriginalInvoiceNumber: null, PriceEach: 0, PartNumber: 0, ProfitPercent: null, PurchaseOrderNumber: null, Qty: 0, Status: null, SupplementNumber: null, Type: null, Vendor: null, VendorPaid: null, VendorPrice: null, Deleted: null, ExpectedOn: null, ReceivedOn: null, OrderedBy: null, ShipVia: null, VendorContact: null, EstimateAmount: null, }; };