const logger = require("../../utils/logger"); const Dinero = require("dinero.js"); 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 moment = require("moment-timezone"); const { QueryInsuranceCo, InsertInsuranceCo, InsertJob, InsertOwner, QueryJob, QueryOwner } = require("../qbo/qbo-receivables"); const { urlBuilder } = require("./qbo"); const { DineroQbFormat } = require("../accounting-constants"); const { findTaxCode } = 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]; oauthClient.setToken(response.associations[0].qbo_auth); if (!qbo_realmId) { res.status(401).json({ error: "No company associated." }); return; } await refreshOauthToken(oauthClient, req); const { payments: paymentsToQuery, elgen } = req.body; const BearerToken = req.BearerToken; const client = req.userGraphQLClient; logger.log("qbo-payment-create", "DEBUG", req.user.email, null, { paymentsToQuery }); const result = await client.setHeaders({ Authorization: BearerToken }).request(queries.QUERY_PAYMENTS_FOR_EXPORT, { payments: paymentsToQuery }); const { payments, bodyshops } = result; const bodyshop = bodyshops[0]; const ret = []; for (const payment of payments) { try { let isThreeTier = bodyshop.accountingconfig.tiers === 3; let twoTierPref = bodyshop.accountingconfig.twotierpref; //Replace this with a for-each loop to check every single Job that's included in the list. //QB Multi AR - If it is in this scenario, overwrite whatever defaults are set since multi AR //will always go Source => RO if (payment.payer !== "Customer" && payment.payer !== "Insurance") { payment.job.ins_co_nm = payment.payer; twoTierPref = "source"; isThreeTier = false; } 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, payment.job); if (!insCoCustomerTier) { //Creating the Insurance Customer. insCoCustomerTier = await InsertInsuranceCo(oauthClient, qbo_realmId, req, payment.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, payment.job, isThreeTier, insCoCustomerTier ); //Query for the owner itself. if (!ownerCustomerTier) { ownerCustomerTier = await InsertOwner( oauthClient, qbo_realmId, req, payment.job, isThreeTier, insCoCustomerTier ); } } //Query for the Job or Create it. jobTier = await QueryJob( oauthClient, qbo_realmId, req, payment.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, payment.job, ownerCustomerTier || insCoCustomerTier); } if (payment.amount > 0) { await InsertPayment(oauthClient, qbo_realmId, req, payment, jobTier, bodyshop); } else { await InsertCreditMemo(oauthClient, qbo_realmId, req, payment, jobTier, bodyshop); } // //No error. Mark the payment exported & insert export log. if (elgen) { await client.setHeaders({ Authorization: BearerToken }).request(queries.QBO_MARK_PAYMENT_EXPORTED, { paymentId: payment.id, payment: { exportedat: moment().tz(bodyshop.timezone) }, logs: [ { bodyshopid: bodyshop.id, paymentid: payment.id, successful: true, useremail: req.user.email } ] }); } ret.push({ paymentid: payment.id, success: true }); } catch (error) { logger.log("qbo-payment-create-error", "ERROR", req.user.email, null, { error: (error && error.authResponse && error.authResponse.body) || (error && error.message) }); //Add the export log error. if (elgen) { await client.setHeaders({ Authorization: BearerToken }).request(queries.INSERT_EXPORT_LOG, { logs: [ { bodyshopid: bodyshop.id, paymentid: payment.id, successful: false, message: JSON.stringify([ (error && error.authResponse && error.authResponse.body) || (error && error.message) ]), useremail: req.user.email } ] }); } ret.push({ paymentid: payment.id, success: false, errorMessage: (error && error.authResponse && error.authResponse.body) || (error && error.message) }); } } res.status(200).json(ret); } catch (error) { //console.log(error); logger.log("qbo-payment-create-error", "ERROR", req.user.email, null, { error: error.message, stack: error.stack }); res.status(400).json(error); } }; async function InsertPayment(oauthClient, qbo_realmId, req, payment, parentRef) { const { paymentMethods, invoices } = await QueryMetaData( oauthClient, qbo_realmId, req, payment.job.ro_number, false, parentRef, payment.job.shopid ); if (invoices && invoices.length !== 1) { throw new Error(`More than 1 invoice with DocNumber ${payment.job.ro_number} found.`); } const paymentQbo = { CustomerRef: { value: parentRef.Id }, TxnDate: moment(payment.date) //.tz(bodyshop.timezone) .format("YYYY-MM-DD"), //DueDate: bill.due_date && moment(bill.due_date).format("YYYY-MM-DD"), DocNumber: payment.paymentnum, TotalAmt: Dinero({ amount: Math.round(payment.amount * 100) }).toFormat(DineroQbFormat), PaymentMethodRef: { value: paymentMethods[payment.type] }, PrivateNote: payment.memo ? payment.memo.length > 4000 ? payment.memo.substring(0, 4000).trim() : payment.memo.trim() : "", PaymentRefNum: payment.transactionid, ...(invoices && invoices.length === 1 && invoices[0] ? { Line: [ { Amount: Dinero({ amount: Math.round(payment.amount * 100) }).toFormat(DineroQbFormat), LinkedTxn: [ { TxnId: invoices[0].Id, TxnType: "Invoice" } ] } ] } : {}) }; logger.log("qbo-payments-objectlog", "DEBUG", req.user.email, payment.id, { paymentQbo }); try { const result = await oauthClient.makeApiCall({ url: urlBuilder(qbo_realmId, "payment"), method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(paymentQbo) }); logger.LogIntegrationCall({ platform: "QBO", method: "POST", name: "InsertPayment", paymentid: payment.id, status: result.response?.status, bodyshopid: payment.job.shopid, email: req.user.email }); setNewRefreshToken(req.user.email, result); return result && result.Bill; } catch (error) { logger.log("qbo-payables-error", "DEBUG", req.user.email, payment.id, { error: error && error.message, method: "InsertPayment" }); throw error; } } async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditMemo, parentTierRef, bodyshopid) { const invoice = await oauthClient.makeApiCall({ url: urlBuilder( qbo_realmId, "query", `select * From Invoice where DocNumber like '${ro_number}%'` ), method: "POST", headers: { "Content-Type": "application/json" } }); logger.LogIntegrationCall({ platform: "QBO", method: "POST", name: "QueryInvoice", status: invoice.response?.status, bodyshopid, email: req.user.email }); const paymentMethods = await oauthClient.makeApiCall({ url: urlBuilder( qbo_realmId, "query", `select * From PaymentMethod` ), method: "POST", headers: { "Content-Type": "application/json" } }); logger.LogIntegrationCall({ platform: "QBO", method: "POST", name: "QueryPaymentMethod", status: paymentMethods.response?.status, bodyshopid, email: req.user.email }); setNewRefreshToken(req.user.email, paymentMethods); // const classes = await oauthClient.makeApiCall({ // url: urlBuilder(qbo_realmId, "query", `select * From Class`), // method: "POST", // headers: { // "Content-Type": "application/json", // }, // }); const paymentMethodMapping = {}; paymentMethods.json && paymentMethods.json.QueryResponse && paymentMethods.json.QueryResponse.PaymentMethod && paymentMethods.json.QueryResponse.PaymentMethod.forEach((t) => { paymentMethodMapping[t.Name] = t.Id; }); // const accountMapping = {}; // accounts.json && // accounts.json.QueryResponse && // accounts.json.QueryResponse.Account.forEach((t) => { // accountMapping[t.Name] = t.Id; // }); // const classMapping = {}; // classes.json && // classes.json.QueryResponse && // classes.json.QueryResponse.Class.forEach((t) => { // accountMapping[t.Name] = t.Id; // }); let ret = {}; if (isCreditMemo) { const taxCodes = await oauthClient.makeApiCall({ url: urlBuilder( qbo_realmId, "query", `select * From TaxCode` ), method: "POST", headers: { "Content-Type": "application/json" } }); logger.LogIntegrationCall({ platform: "QBO", method: "POST", name: "QueryTaxCode", status: taxCodes.response?.status, bodyshopid, email: req.user.email }); const items = await oauthClient.makeApiCall({ url: urlBuilder( qbo_realmId, "query", `select * From Item` ), method: "POST", headers: { "Content-Type": "application/json" } }); logger.LogIntegrationCall({ platform: "QBO", method: "POST", name: "QueryItems", status: items.response?.status, bodyshopid, email: req.user.email }); setNewRefreshToken(req.user.email, items); const itemMapping = {}; items.json && items.json.QueryResponse && items.json.QueryResponse.Item && items.json.QueryResponse.Item.forEach((t) => { itemMapping[t.Name] = t.Id; }); const taxCodeMapping = {}; taxCodes.json && taxCodes.json.QueryResponse && taxCodes.json.QueryResponse.TaxCode && taxCodes.json.QueryResponse.TaxCode.forEach((t) => { taxCodeMapping[t.Name] = t.Id; }); ret = { ...ret, items: itemMapping, taxCodes: taxCodeMapping }; } return { ...ret, paymentMethods: paymentMethodMapping, invoices: invoice.json && invoice.json.QueryResponse && invoice.json.QueryResponse.Invoice && (parentTierRef ? [invoice.json.QueryResponse.Invoice.find((x) => x.CustomerRef.value === parentTierRef.Id)] : [invoice.json.QueryResponse.Invoice[0]]) }; } async function InsertCreditMemo(oauthClient, qbo_realmId, req, payment, parentRef) { const { invoices, items, taxCodes } = await QueryMetaData( oauthClient, qbo_realmId, req, payment.job.ro_number, true, parentRef, payment.job.shopid ); if (invoices && invoices.length !== 1) { throw new Error(`More than 1 invoice with DocNumber ${payment.ro_number} found.`); } const paymentQbo = { CustomerRef: { value: parentRef.Id }, TxnDate: moment(payment.date) //.tz(bodyshop.timezone) .format("YYYY-MM-DD"), DocNumber: payment.paymentnum, ...(invoices && invoices[0] ? { InvoiceRef: { value: invoices[0].Id } } : {}), PaymentRefNum: payment.transactionid, Line: [ { DetailType: "SalesItemLineDetail", Amount: Dinero({ amount: Math.round(payment.amount * -100) }).toFormat(DineroQbFormat), SalesItemLineDetail: { ItemRef: { value: items[payment.job.bodyshop.md_responsibility_centers.refund.accountitem] }, Qty: 1, TaxCodeRef: { value: taxCodes[ findTaxCode( { local: false, federal: false, state: false }, payment.job.bodyshop.md_responsibility_centers.sales_tax_codes ) ] } } } ] }; logger.log("qbo-payments-objectlog", "DEBUG", req.user.email, payment.id, { paymentQbo }); try { const result = await oauthClient.makeApiCall({ url: urlBuilder(qbo_realmId, "creditmemo"), method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(paymentQbo) }); logger.LogIntegrationCall({ platform: "QBO", method: "POST", name: "InsertCreditMemo", paymentid: payment.id, status: result.response?.status, bodyshopid: req.user.bodyshopid, email: req.user.email }); setNewRefreshToken(req.user.email, result); return result && result.Bill; } catch (error) { logger.log("qbo-payables-error", "DEBUG", req.user.email, payment.id, { error: error, validationError: JSON.stringify(error?.response?.data), accountmeta: JSON.stringify({ items, taxCodes }), method: "InsertCreditMemo" }); throw error; } }