const urlBuilder = require("./qbo").urlBuilder; const StandardizeName = require("./qbo").StandardizeName; 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 { 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 }); 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 { jobIds, elgen } = req.body; //Query Job Info const BearerToken = req.BearerToken; const client = req.userGraphQLClient; 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?.authResponse?.body || error?.response?.data?.Fault?.Error.map((e) => e.Detail).join(", ") || error?.response?.data || error?.message }); //console.log(error); logger.log("qbo-receivable-create-error", "ERROR", req.user.email, { error: error.message, stack: error.stack }); //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" } }); logger.LogIntegrationCall({ platform: "QBO", method: "POST", name: "QueryCustomer", status: result.response?.status, bodyshopid: job.shopid, jobid: job.id, email: req.user.email }); 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.` ); } 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) }); logger.LogIntegrationCall({ platform: "QBO", method: "POST", name: "InsertCustomer", status: result.response.status, bodyshopid: job.shopid, jobid: job.id, email: req.user.email }); 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" } }); logger.LogIntegrationCall({ platform: "QBO", method: "POST", name: "QueryCustomer", status: result.response?.status, bodyshopid: job.shopid, jobid: job.id, email: req.user.email }); 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 }, ...(job.ownr_ea ? { PrimaryEmailAddr: { Address: job.ownr_ea.trim() } } : {}), ...(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) }); logger.LogIntegrationCall({ platform: "QBO", method: "POST", name: "InsertCustomer", status: result.response?.status, bodyshopid: job.shopid, jobid: job.id, email: req.user.email }); 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" } }); logger.LogIntegrationCall({ platform: "QBO", method: "POST", name: "QueryCustomer", status: result.response?.status, bodyshopid: job.shopid, jobid: job.id, email: req.user.email }); 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.ownr_ea ? { PrimaryEmailAddr: { Address: job.ownr_ea.trim() } } : {}), 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) }); logger.LogIntegrationCall({ platform: "QBO", method: "POST", name: "InsertCustomer", status: result.response?.status, bodyshopid: job.shopid, jobid: job.id, email: req.user.email }); 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, bodyshopid, jobid) { 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" } }); logger.LogIntegrationCall({ platform: "QBO", method: "POST", name: "QueryItems", status: items.response?.status, bodyshopid, jobid: jobid, email: req.user.email }); 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" } }); logger.LogIntegrationCall({ platform: "QBO", method: "POST", name: "QueryTaxCodes", status: taxCodes.response?.status, bodyshopid, jobid: jobid, email: req.user.email }); const classes = await oauthClient.makeApiCall({ url: urlBuilder( qbo_realmId, "query", `select * From Class` ), method: "POST", headers: { "Content-Type": "application/json" } }); logger.LogIntegrationCall({ platform: "QBO", method: "POST", name: "QueryClasses", status: classes.response?.status, bodyshopid, jobid: jobid, email: req.user.email }); 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, job.shopid, job.id); 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 && { 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 || ""}` }, ...(job.ownr_ea ? { BillEmail: { Address: job.ownr_ea.trim() } } : {}) }; 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) }); logger.LogIntegrationCall({ platform: "QBO", method: "POST", name: "InsertInvoice", status: result.response?.status, bodyshopid: job.shopid, jobid: job.id, email: req.user.email }); 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: "InsertInvoice", validationError: JSON.stringify(error?.response?.data), accountmeta: JSON.stringify({ items, taxCodes, classes }) }); throw error; } } async function InsertInvoiceMultiPayerInvoice( oauthClient, qbo_realmId, req, job, bodyshop, parentTierRef, payer, suffix ) { const { items, taxCodes, classes } = await QueryMetaData(oauthClient, qbo_realmId, req, job.shopid); 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 || ""}` }, ...(job.ownr_ea ? { BillEmail: { Address: job.ownr_ea.trim() } } : {}) }; 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) }); logger.LogIntegrationCall({ platform: "QBO", method: "POST", name: "InsertInvoice", status: result.response?.status, bodyshopid: job.shopid, jobid: job.id, email: req.user.email }); 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; } }