const urlBuilder = require("./qbo").urlBuilder; const StandardizeName = require("./qbo").StandardizeName; const path = require("path"); require("dotenv").config({ path: path.resolve( process.cwd(), `.env.${process.env.NODE_ENV || "development"}` ), }); const logger = require("../../utils/logger"); const apiGqlClient = require("../../graphql-client/graphql-client").client; const queries = require("../../graphql-client/queries"); const { refresh: refreshOauthToken, setNewRefreshToken, } = require("./qbo-callback"); const OAuthClient = require("intuit-oauth"); const CreateInvoiceLines = require("../qb-receivables-lines").default; const moment = require("moment-timezone"); const GraphQLClient = require("graphql-request").GraphQLClient; const { generateOwnerTier } = require("../qbxml/qbxml-utils"); const { createMultiQbPayerLines } = require("../qb-receivables-lines"); exports.default = async (req, res) => { const oauthClient = new OAuthClient({ clientId: process.env.QBO_CLIENT_ID, clientSecret: process.env.QBO_SECRET, environment: process.env.NODE_ENV === "production" ? "production" : "sandbox", redirectUri: process.env.QBO_REDIRECT_URI, logging: true, }); try { //Fetch the API Access Tokens & Set them for the session. const response = await apiGqlClient.request(queries.GET_QBO_AUTH, { email: req.user.email, }); const { qbo_realmId } = response.associations[0]; if (!qbo_realmId) { res.status(401).json({ error: "No company associated." }); return; } oauthClient.setToken(response.associations[0].qbo_auth); await refreshOauthToken(oauthClient, req); const BearerToken = req.headers.authorization; const { jobIds, elgen } = req.body; //Query Job Info const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { headers: { Authorization: BearerToken, }, }); logger.log("qbo-receivable-create", "DEBUG", req.user.email, jobIds); const result = await client .setHeaders({ Authorization: BearerToken }) .request(queries.QUERY_JOBS_FOR_RECEIVABLES_EXPORT, { ids: jobIds, }); const { jobs, bodyshops } = result; const bodyshop = bodyshops[0]; const ret = []; for (const job of jobs) { //const job = jobs[0]; try { const isThreeTier = bodyshop.accountingconfig.tiers === 3; const twoTierPref = bodyshop.accountingconfig.twotierpref; //Replace this with a for-each loop to check every single Job that's included in the list. let insCoCustomerTier, ownerCustomerTier, jobTier; if (isThreeTier || (!isThreeTier && twoTierPref === "source")) { //Insert the insurance company tier. //Query for top level customer, the insurance company name. insCoCustomerTier = await QueryInsuranceCo( oauthClient, qbo_realmId, req, job ); if (!insCoCustomerTier) { //Creating the Insurance Customer. insCoCustomerTier = await InsertInsuranceCo( oauthClient, qbo_realmId, req, job, bodyshop ); } } if (isThreeTier || (!isThreeTier && twoTierPref === "name")) { //Insert the name/owner and account for whether the source should be the ins co in 3 tier.. ownerCustomerTier = await QueryOwner( oauthClient, qbo_realmId, req, job, isThreeTier, insCoCustomerTier ); //Query for the owner itself. if (!ownerCustomerTier) { ownerCustomerTier = await InsertOwner( oauthClient, qbo_realmId, req, job, isThreeTier, insCoCustomerTier ); } } //Query for the Job or Create it. jobTier = await QueryJob( oauthClient, qbo_realmId, req, job, isThreeTier ? ownerCustomerTier : twoTierPref === "source" ? insCoCustomerTier : ownerCustomerTier ); // Need to validate that the job tier is associated to the right individual? if (!jobTier) { jobTier = await InsertJob( oauthClient, qbo_realmId, req, job, ownerCustomerTier || insCoCustomerTier ); } if (!req.body.custDataOnly) { await InsertInvoice( oauthClient, qbo_realmId, req, job, bodyshop, jobTier ); if (job.qb_multiple_payers && job.qb_multiple_payers.length > 0) { for (const [index, payer] of job.qb_multiple_payers.entries()) { //do the thing. //Create the source level. let insCoCustomerTier, ownerCustomerTier, jobTier; //Insert the insurance company tier. //Query for top level customer, the insurance company name. insCoCustomerTier = await QueryInsuranceCo( oauthClient, qbo_realmId, req, { ...job, ins_co_nm: payer.name } ); if (!insCoCustomerTier) { //Creating the Insurance Customer. insCoCustomerTier = await InsertInsuranceCo( oauthClient, qbo_realmId, req, { ...job, ins_co_nm: payer.name }, bodyshop ); } //Query for the Job or Create it. jobTier = await QueryJob( oauthClient, qbo_realmId, req, job, insCoCustomerTier ); // Need to validate that the job tier is associated to the right individual? if (!jobTier) { jobTier = await InsertJob( oauthClient, qbo_realmId, req, job, insCoCustomerTier ); } //Create the RO level await InsertInvoiceMultiPayerInvoice( oauthClient, qbo_realmId, req, job, bodyshop, jobTier, payer, `-${index + 1}` ); } } // //No error. Mark the job exported & insert export log. if (elgen) { const result = await client .setHeaders({ Authorization: BearerToken }) .request(queries.QBO_MARK_JOB_EXPORTED, { jobId: job.id, job: { status: bodyshop.md_ro_statuses.default_exported || "Exported*", date_exported: moment().tz(bodyshop.timezone), }, logs: [ { bodyshopid: bodyshop.id, jobid: job.id, successful: true, useremail: req.user.email, }, ], }); } } ret.push({ jobid: job.id, success: true }); } catch (error) { ret.push({ jobid: job.id, success: false, errorMessage: (error && error.authResponse && error.authResponse.body) || (error && error.message), }); //Add the export log error. if (elgen) { const result = await client .setHeaders({ Authorization: BearerToken }) .request(queries.INSERT_EXPORT_LOG, { logs: [ { bodyshopid: bodyshop.id, jobid: job.id, successful: false, message: JSON.stringify([ (error && error.authResponse && error.authResponse.body) || (error && error.message), ]), useremail: req.user.email, }, ], }); } } } res.status(200).json(ret); } catch (error) { console.log(error); logger.log("qbo-receivable-create-error", "ERROR", req.user.email, { error: error.message, stack: error.stack, }); res.status(400).json(error); } }; async function QueryInsuranceCo(oauthClient, qbo_realmId, req, job) { try { const result = await oauthClient.makeApiCall({ url: urlBuilder( qbo_realmId, "query", `select * From Customer where DisplayName = '${StandardizeName( job.ins_co_nm.trim() )}' and Active = true` ), method: "POST", headers: { "Content-Type": "application/json", }, }); setNewRefreshToken(req.user.email, result); return ( result.json && result.json.QueryResponse && result.json.QueryResponse.Customer && result.json.QueryResponse.Customer[0] ); } catch (error) { logger.log("qbo-receivables-error", "DEBUG", req.user.email, job.id, { error, method: "QueryInsuranceCo", }); throw error; } } exports.QueryInsuranceCo = QueryInsuranceCo; async function InsertInsuranceCo(oauthClient, qbo_realmId, req, job, bodyshop) { const insCo = bodyshop.md_ins_cos.find((i) => i.name === job.ins_co_nm); if (!insCo) { throw new Error( `Insurance Company '${job.ins_co_nm}' not found in shop configuration. Please make sure it exists or change the insurance company name on the job to one that exists.` ); return; } const Customer = { DisplayName: job.ins_co_nm.trim(), BillWithParent: true, BillAddr: { City: job.ownr_city, Line1: insCo.street1, Line2: insCo.street2, PostalCode: insCo.zip, CountrySubDivisionCode: insCo.state, }, }; try { const result = await oauthClient.makeApiCall({ url: urlBuilder(qbo_realmId, "customer"), method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(Customer), }); setNewRefreshToken(req.user.email, result); return result && result.json.Customer; } catch (error) { logger.log("qbo-receivables-error", "DEBUG", req.user.email, job.id, { error, method: "InsertInsuranceCo", }); throw error; } } exports.InsertInsuranceCo = InsertInsuranceCo; async function QueryOwner( oauthClient, qbo_realmId, req, job, isThreeTier, parentTierRef ) { const ownerName = generateOwnerTier(job, true, null); const result = await oauthClient.makeApiCall({ url: urlBuilder( qbo_realmId, "query", `select * From Customer where DisplayName = '${StandardizeName( ownerName )}' and Active = true` ), method: "POST", headers: { "Content-Type": "application/json", }, }); setNewRefreshToken(req.user.email, result); return ( result.json && result.json.QueryResponse && result.json.QueryResponse.Customer && result.json.QueryResponse.Customer.find( (x) => x.ParentRef?.value === parentTierRef?.Id ) ); } exports.QueryOwner = QueryOwner; async function InsertOwner( oauthClient, qbo_realmId, req, job, isThreeTier, parentTierRef ) { const ownerName = generateOwnerTier(job, true, null); const Customer = { DisplayName: ownerName, BillWithParent: true, BillAddr: { City: job.ownr_city, Line1: job.ownr_addr1, Line2: job.ownr_addr2, PostalCode: job.ownr_zip, CountrySubDivisionCode: job.ownr_st, }, ...(isThreeTier ? { Job: true, ParentRef: { value: parentTierRef.Id, }, } : {}), }; try { const result = await oauthClient.makeApiCall({ url: urlBuilder(qbo_realmId, "customer"), method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(Customer), }); setNewRefreshToken(req.user.email, result); return result && result.json.Customer; } catch (error) { logger.log("qbo-receivables-error", "DEBUG", req.user.email, job.id, { error, method: "InsertOwner", }); throw error; } } exports.InsertOwner = InsertOwner; async function QueryJob(oauthClient, qbo_realmId, req, job, parentTierRef) { const result = await oauthClient.makeApiCall({ url: urlBuilder( qbo_realmId, "query", `select * From Customer where DisplayName = '${job.ro_number}' and Active = true` ), method: "POST", headers: { "Content-Type": "application/json", }, }); setNewRefreshToken(req.user.email, result); return ( result.json && result.json.QueryResponse && result.json.QueryResponse.Customer && (parentTierRef ? result.json.QueryResponse.Customer.find( (x) => x.ParentRef.value === parentTierRef.Id ) : result.json.QueryResponse.Customer[0]) ); } exports.QueryJob = QueryJob; async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) { const Customer = { DisplayName: job.ro_number, BillWithParent: true, BillAddr: { City: job.ownr_city, Line1: job.ownr_addr1, Line2: job.ownr_addr2, PostalCode: job.ownr_zip, CountrySubDivisionCode: job.ownr_st, }, Job: true, ParentRef: { value: parentTierRef.Id, }, }; try { const result = await oauthClient.makeApiCall({ url: urlBuilder(qbo_realmId, "customer"), method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(Customer), }); setNewRefreshToken(req.user.email, result); return result && result.json.Customer; } catch (error) { logger.log("qbo-receivables-error", "DEBUG", req.user.email, job.id, { error, method: "InsertOwner", }); throw error; } } exports.InsertJob = InsertJob; async function QueryMetaData(oauthClient, qbo_realmId, req) { const items = await oauthClient.makeApiCall({ url: urlBuilder( qbo_realmId, "query", `select * From Item where active=true maxresults 1000` ), method: "POST", headers: { "Content-Type": "application/json", }, }); setNewRefreshToken(req.user.email, items); const taxCodes = await oauthClient.makeApiCall({ url: urlBuilder( qbo_realmId, "query", `select * From TaxCode where active=true` ), method: "POST", headers: { "Content-Type": "application/json", }, }); const classes = await oauthClient.makeApiCall({ url: urlBuilder(qbo_realmId, "query", `select * From Class`), method: "POST", headers: { "Content-Type": "application/json", }, }); const taxCodeMapping = {}; taxCodes.json && taxCodes.json.QueryResponse && taxCodes.json.QueryResponse.TaxCode && taxCodes.json.QueryResponse.TaxCode.forEach((t) => { taxCodeMapping[t.Name] = t.Id; }); const itemMapping = {}; items.json && items.json.QueryResponse && items.json.QueryResponse.Item && items.json.QueryResponse.Item.forEach((t) => { itemMapping[t.Name] = t.Id; }); const classMapping = {}; classes.json && classes.json.QueryResponse && classes.json.QueryResponse.Class && classes.json.QueryResponse.Class.forEach((t) => { classMapping[t.Name] = t.Id; }); return { items: itemMapping, taxCodes: taxCodeMapping, classes: classMapping, }; } async function InsertInvoice( oauthClient, qbo_realmId, req, job, bodyshop, parentTierRef ) { const { items, taxCodes, classes } = await QueryMetaData( oauthClient, qbo_realmId, req ); const InvoiceLineAdd = CreateInvoiceLines({ bodyshop, jobs_by_pk: job, qbo: true, items, taxCodes, classes, }); const invoiceObj = { Line: InvoiceLineAdd, TxnDate: moment(job.date_invoiced) .tz(bodyshop.timezone) .format("YYYY-MM-DD"), DocNumber: job.ro_number, ...(job.class ? { ClassRef: { value: classes[job.class] } } : {}), CustomerMemo: { value: `${job.clm_no ? `Claim No: ${job.clm_no}` : ``}${ job.po_number ? `PO No: ${job.po_number}` : `` } Vehicle:${job.v_model_yr || ""} ${job.v_make_desc || ""} ${ job.v_model_desc || "" } ${job.v_vin || ""} ${job.plate_no || ""} `.trim(), }, CustomerRef: { value: parentTierRef.Id, }, ...(bodyshop.accountingconfig.qbo_departmentid && bodyshop.accountingconfig.qbo_departmentid.trim() !== "" && { DepartmentRef: { value: bodyshop.accountingconfig.qbo_departmentid }, }), CustomField: [ ...(bodyshop.accountingconfig.ReceivableCustomField1 ? [ { DefinitionId: "1", StringValue: job[bodyshop.accountingconfig.ReceivableCustomField1], Type: "StringType", }, ] : []), ...(bodyshop.accountingconfig.ReceivableCustomField2 ? [ { DefinitionId: "2", StringValue: job[bodyshop.accountingconfig.ReceivableCustomField2], Type: "StringType", }, ] : []), ...(bodyshop.accountingconfig.ReceivableCustomField3 ? [ { DefinitionId: "3", StringValue: job[bodyshop.accountingconfig.ReceivableCustomField3], Type: "StringType", }, ] : []), ], ...(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && bodyshop.accountingconfig.qbo_usa && bodyshop.region_config.includes("CA_") && { TxnTaxDetail: { TxnTaxCodeRef: { value: taxCodes[ bodyshop.md_responsibility_centers.taxes.state.accountitem ], }, }, }), ...(bodyshop.accountingconfig.printlater ? { PrintStatus: "NeedToPrint" } : {}), ...(bodyshop.accountingconfig.emaillater && job.ownr_ea ? { EmailStatus: "NeedToSend" } : {}), BillAddr: { Line3: `${job.ownr_city || ""}, ${job.ownr_st || ""} ${ job.ownr_zip || "" }`.trim(), Line2: job.ownr_addr1 || "", Line1: `${job.ownr_fn || ""} ${job.ownr_ln || ""} ${ job.ownr_co_nm || "" }`, }, }; logger.log("qbo-receivable-objectlog", "DEBUG", req.user.email, job.id, { invoiceObj, }); try { const result = await oauthClient.makeApiCall({ url: urlBuilder(qbo_realmId, "invoice"), method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(invoiceObj), }); setNewRefreshToken(req.user.email, result); return result && result.json && result.json.Invoice; } catch (error) { logger.log("qbo-receivables-error", "DEBUG", req.user.email, job.id, { error, method: "InsertOwner", }); throw error; } } async function InsertInvoiceMultiPayerInvoice( oauthClient, qbo_realmId, req, job, bodyshop, parentTierRef, payer, suffix ) { const { items, taxCodes, classes } = await QueryMetaData( oauthClient, qbo_realmId, req ); const InvoiceLineAdd = createMultiQbPayerLines({ bodyshop, jobs_by_pk: job, qbo: true, items, taxCodes, classes, payer, suffix, }); const invoiceObj = { Line: InvoiceLineAdd, TxnDate: moment(job.date_invoiced) .tz(bodyshop.timezone) .format("YYYY-MM-DD"), DocNumber: job.ro_number + suffix, ...(job.class ? { ClassRef: { value: classes[job.class] } } : {}), CustomerMemo: { value: `${job.clm_no ? `Claim No: ${job.clm_no}` : ``}${ job.po_number ? `PO No: ${job.po_number}` : `` } Vehicle:${job.v_model_yr || ""} ${job.v_make_desc || ""} ${ job.v_model_desc || "" } ${job.v_vin || ""} ${job.plate_no || ""} `.trim(), }, CustomerRef: { value: parentTierRef.Id, }, ...(bodyshop.accountingconfig.qbo_departmentid && bodyshop.accountingconfig.qbo_departmentid.trim() !== "" && { DepartmentRef: { value: bodyshop.accountingconfig.qbo_departmentid }, }), CustomField: [ ...(bodyshop.accountingconfig.ReceivableCustomField1 ? [ { DefinitionId: "1", StringValue: job[bodyshop.accountingconfig.ReceivableCustomField1], Type: "StringType", }, ] : []), ...(bodyshop.accountingconfig.ReceivableCustomField2 ? [ { DefinitionId: "2", StringValue: job[bodyshop.accountingconfig.ReceivableCustomField2], Type: "StringType", }, ] : []), ...(bodyshop.accountingconfig.ReceivableCustomField3 ? [ { DefinitionId: "3", StringValue: job[bodyshop.accountingconfig.ReceivableCustomField3], Type: "StringType", }, ] : []), ], ...(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && bodyshop.accountingconfig.qbo_usa && bodyshop.region_config.includes("CA_") && { TxnTaxDetail: { TxnTaxCodeRef: { value: taxCodes[ bodyshop.md_responsibility_centers.taxes.state.accountitem ], }, }, }), ...(bodyshop.accountingconfig.printlater ? { PrintStatus: "NeedToPrint" } : {}), ...(bodyshop.accountingconfig.emaillater && job.ownr_ea ? { EmailStatus: "NeedToSend" } : {}), BillAddr: { Line3: `${job.ownr_city || ""}, ${job.ownr_st || ""} ${ job.ownr_zip || "" }`.trim(), Line2: job.ownr_addr1 || "", Line1: `${job.ownr_fn || ""} ${job.ownr_ln || ""} ${ job.ownr_co_nm || "" }`, }, }; logger.log("qbo-receivable-objectlog", "DEBUG", req.user.email, job.id, { invoiceObj, }); try { const result = await oauthClient.makeApiCall({ url: urlBuilder(qbo_realmId, "invoice"), method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(invoiceObj), }); setNewRefreshToken(req.user.email, result); return result && result.json && result.json.Invoice; } catch (error) { logger.log("qbo-receivables-error", "DEBUG", req.user.email, job.id, { error, method: "InsertOwner", }); throw error; } }