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 Dinero = require("dinero.js"); const DineroQbFormat = require("../accounting-constants").DineroQbFormat; 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"); const GraphQLClient = require("graphql-request").GraphQLClient; const findTaxCode = require("../qb-receivables-lines").findTaxCode; 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]; oauthClient.setToken(response.associations[0].qbo_auth); if (!qbo_realmId) { res.status(401).json({ error: "No company associated." }); return; } await refreshOauthToken(oauthClient, req); const BearerToken = req.headers.authorization; const { bills: billsToQuery } = req.body; //Query Job Info const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { headers: { Authorization: BearerToken, }, }); logger.log("qbo-payable-create", "DEBUG", req.user.email, billsToQuery); const result = await client .setHeaders({ Authorization: BearerToken }) .request(queries.QUERY_BILLS_FOR_PAYABLES_EXPORT, { bills: billsToQuery, }); const { bills } = result; const ret = []; for (const bill of bills) { try { let vendorRecord; vendorRecord = await QueryVendorRecord( oauthClient, qbo_realmId, req, bill ); if (!vendorRecord) { vendorRecord = await InsertVendorRecord( oauthClient, qbo_realmId, req, bill ); } const insertResults = await InsertBill( oauthClient, qbo_realmId, req, bill, vendorRecord ); ret.push({ billid: bill.id, success: true }); } catch (error) { ret.push({ billid: bill.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-payable-create-error", "ERROR", req.user.email, { error }); res.status(400).json(error); } }; async function QueryVendorRecord(oauthClient, qbo_realmId, req, bill) { try { const result = await oauthClient.makeApiCall({ url: urlBuilder( qbo_realmId, "query", `select * From vendor where DisplayName = '${StandardizeName( bill.vendor.name )}'` ), method: "POST", headers: { "Content-Type": "application/json", }, }); setNewRefreshToken(req.user.email, result); return ( result.json && result.json.QueryResponse && result.json.QueryResponse.Vendor && result.json.QueryResponse.Vendor[0] ); } catch (error) { logger.log("qbo-payables-error", "DEBUG", req.user.email, bill.id, { error: (error && error.authResponse && error.authResponse.body) || (error && error.message), method: "QueryVendorRecord", }); throw error; } } async function InsertVendorRecord(oauthClient, qbo_realmId, req, bill) { const Vendor = { DisplayName: bill.vendor.name, }; try { const result = await oauthClient.makeApiCall({ url: urlBuilder(qbo_realmId, "vendor"), method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(Vendor), }); setNewRefreshToken(req.user.email, result); return result && result.json && result.json.Vendor; } catch (error) { logger.log("qbo-payables-error", "DEBUG", req.user.email, bill.id, { error: (error && error.authResponse && error.authResponse.body) || (error && error.message), method: "InsertVendorRecord", }); throw error; } } async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor) { const { accounts, taxCodes, classes } = await QueryMetaData( oauthClient, qbo_realmId, req ); const billQbo = { VendorRef: { value: vendor.Id, }, TxnDate: moment(bill.date).format("YYYY-MM-DD"), //DueDate: bill.due_date && moment(bill.due_date).format("YYYY-MM-DD"), DocNumber: bill.invoice_number, ...(bill.job.class ? { ClassRef: { Id: classes[bill.job.class] } } : {}), PrivateNote: `RO ${bill.job.ro_number || ""} OWNER ${ bill.job.ownr_fn || "" } ${bill.job.ownr_ln || ""} ${bill.job.ownr_co_nm || ""}`, Line: bill.billlines.map((il) => generateBillLine( il, accounts, bill.job.class, bill.job.bodyshop.md_responsibility_centers.sales_tax_codes, classes, taxCodes, bill.job.bodyshop.md_responsibility_centers.costs ) ), }; logger.log("qbo-payable-objectlog", "DEBUG", req.user.email, bill.id, { billQbo, }); try { const result = await oauthClient.makeApiCall({ url: urlBuilder( qbo_realmId, bill.is_credit_memo ? "vendorcredit" : "bill" ), method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(billQbo), }); setNewRefreshToken(req.user.email, result); return result && result.json && result.json.Bill; } catch (error) { logger.log("qbo-payables-error", "DEBUG", req.user.email, bill.id, { error: (error && error.authResponse && error.authResponse.body) || (error && error.message), method: "InsertBill", }); throw error; } } // [ // { // DetailType: "AccountBasedExpenseLineDetail", // Amount: 200.0, // Id: "1", // AccountBasedExpenseLineDetail: { // AccountRef: { // value: "7", // }, // }, // }, // ], const generateBillLine = ( billLine, accounts, jobClass, ioSalesTaxCodes, classes, taxCodes, costCenters ) => { const account = costCenters.find((c) => c.name === billLine.cost_center); console.log(account.accountname, accounts[account.accountname]); return { DetailType: "AccountBasedExpenseLineDetail", AccountBasedExpenseLineDetail: { ...(jobClass ? { ClassRef: { Id: classes[jobClass] } } : {}), TaxCodeRef: { value: taxCodes[findTaxCode(billLine.applicable_taxes, ioSalesTaxCodes)], }, AccountRef: { value: accounts[account.accountname], }, }, Amount: Dinero({ amount: Math.round(billLine.actual_cost * 100), }) .multiply(billLine.quantity || 1) .toFormat(DineroQbFormat), }; }; async function QueryMetaData(oauthClient, qbo_realmId, req) { const accounts = await oauthClient.makeApiCall({ url: urlBuilder( qbo_realmId, "query", `select * From Account where AccountType = 'Cost of Goods Sold'` ), method: "POST", headers: { "Content-Type": "application/json", }, }); setNewRefreshToken(req.user.email, accounts); const taxCodes = await oauthClient.makeApiCall({ url: urlBuilder(qbo_realmId, "query", `select * From TaxCode`), 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 accountMapping = {}; accounts.json && accounts.json.QueryResponse && accounts.json.QueryResponse.Account && accounts.json.QueryResponse.Account.forEach((t) => { accountMapping[t.FullyQualifiedName] = t.Id; }); const classMapping = {}; classes.json && classes.json.QueryResponse && classes.json.QueryResponse.Class && classes.json.QueryResponse.Class.forEach((t) => { accountMapping[t.Name] = t.Id; }); return { accounts: accountMapping, taxCodes: taxCodeMapping, classes: classMapping, }; }