From 4d52a5c44a9bfcde871975a013da5d0ef2d69f07 Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Tue, 12 Oct 2021 19:07:43 -0700 Subject: [PATCH] IO-256 Exporting of payments --- .../accounting-payments-table.component.jsx | 23 +- .../owner-find-modal.container.jsx | 4 +- .../payment-export-button.component.jsx | 102 ++++--- .../payments-export-all-button.component.jsx | 72 +++-- server.js | 1 + server/accounting/qbo/qbo-payables.js | 9 +- server/accounting/qbo/qbo-payments.js | 274 ++++++++++++++++++ server/accounting/qbo/qbo-receivables.js | 20 +- server/accounting/qbo/qbo.js | 1 + server/accounting/qbxml/qbxml-payments.js | 3 - 10 files changed, 415 insertions(+), 94 deletions(-) diff --git a/client/src/components/accounting-payments-table/accounting-payments-table.component.jsx b/client/src/components/accounting-payments-table/accounting-payments-table.component.jsx index 2edc4a0a4..53a51bdae 100644 --- a/client/src/components/accounting-payments-table/accounting-payments-table.component.jsx +++ b/client/src/components/accounting-payments-table/accounting-payments-table.component.jsx @@ -8,8 +8,26 @@ import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter"; import { alphaSort, dateSort } from "../../utils/sorters"; import PaymentExportButton from "../payment-export-button/payment-export-button.component"; import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component"; +import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; -export default function AccountingPayablesTableComponent({ +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, +}); + +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(AccountingPayablesTableComponent); + +export function AccountingPayablesTableComponent({ + bodyshop, loading, payments, }) { @@ -163,6 +181,9 @@ export default function AccountingPayablesTableComponent({ loadingCallback={setTransInProgress} completedCallback={setSelectedPayments} /> + {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && ( + + )} { logImEXEvent("accounting_payment_export"); - setLoading(true); - if (!!loadingCallback) loadingCallback(true); - - let QbXmlResponse; - try { - QbXmlResponse = await axios.post( - "/accounting/qbxml/payments", - { payments: [paymentId] }, - { - headers: { - Authorization: `Bearer ${await auth.currentUser.getIdToken()}`, - }, - } - ); - console.log("handle -> XML", QbXmlResponse); - } catch (error) { - console.log("Error getting QBXML from Server.", error); - notification["error"]({ - message: t("payments.errors.exporting", { - error: "Unable to retrieve QBXML. " + JSON.stringify(error.message), - }), - }); - if (loadingCallback) loadingCallback(false); - setLoading(false); - return; - } - + //Check if it's a QBO Setup. let PartnerResponse; - - try { - PartnerResponse = await axios.post( - "http://localhost:1337/qb/", - //"http://609feaeae986.ngrok.io/qb/", - QbXmlResponse.data - ); - } catch (error) { - console.log("Error connecting to quickbooks or partner.", error); - notification["error"]({ - message: t("payments.errors.exporting-partner"), + if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) { + PartnerResponse = await axios.post(`/qbo/payments`, { + withCredentials: true, + payments: [paymentId], }); - if (!!loadingCallback) loadingCallback(false); - setLoading(false); - return; + } else { + //Default is QBD + + if (!!loadingCallback) loadingCallback(true); + + let QbXmlResponse; + try { + QbXmlResponse = await axios.post( + "/accounting/qbxml/payments", + { payments: [paymentId] }, + { + headers: { + Authorization: `Bearer ${await auth.currentUser.getIdToken()}`, + }, + } + ); + console.log("handle -> XML", QbXmlResponse); + } catch (error) { + console.log("Error getting QBXML from Server.", error); + notification["error"]({ + message: t("payments.errors.exporting", { + error: "Unable to retrieve QBXML. " + JSON.stringify(error.message), + }), + }); + if (loadingCallback) loadingCallback(false); + setLoading(false); + return; + } + + try { + PartnerResponse = await axios.post( + "http://localhost:1337/qb/", + QbXmlResponse.data + ); + } catch (error) { + console.log("Error connecting to quickbooks or partner.", error); + notification["error"]({ + message: t("payments.errors.exporting-partner"), + }); + if (!!loadingCallback) loadingCallback(false); + setLoading(false); + return; + } } console.log("handleQbxml -> PartnerResponse", PartnerResponse); const failedTransactions = PartnerResponse.data.filter((r) => !r.success); - + const successfulTransactions = PartnerResponse.data.filter( + (r) => r.success + ); if (failedTransactions.length > 0) { //Uh oh. At least one was no good. failedTransactions.map((ft) => @@ -123,7 +132,14 @@ export function PaymentExportButton({ const paymentUpdateResponse = await updatePayment({ variables: { - paymentIdList: [paymentId], + paymentIdList: successfulTransactions.map( + (st) => + st[ + bodyshop.accountingconfig && bodyshop.accountingconfig.qbo + ? "paymentid" + : "id" + ] + ), payment: { exportedat: new Date(), }, diff --git a/client/src/components/payments-export-all-button/payments-export-all-button.component.jsx b/client/src/components/payments-export-all-button/payments-export-all-button.component.jsx index 693a14339..3e4085c44 100644 --- a/client/src/components/payments-export-all-button/payments-export-all-button.component.jsx +++ b/client/src/components/payments-export-all-button/payments-export-all-button.component.jsx @@ -33,42 +33,50 @@ export function PaymentsExportAllButton({ const handleQbxml = async () => { setLoading(true); if (!!loadingCallback) loadingCallback(true); - - let QbXmlResponse; - try { - QbXmlResponse = await axios.post("/accounting/qbxml/payments", { + let PartnerResponse; + if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) { + PartnerResponse = await axios.post(`/qbo/payments`, { + withCredentials: true, payments: paymentIds, }); - } catch (error) { - console.log("Error getting QBXML from Server.", error); - notification["error"]({ - message: t("payments.errors.exporting", { - error: "Unable to retrieve QBXML. " + JSON.stringify(error.message), - }), - }); - if (loadingCallback) loadingCallback(false); - setLoading(false); - return; + } else { + let QbXmlResponse; + try { + QbXmlResponse = await axios.post("/accounting/qbxml/payments", { + payments: paymentIds, + }); + } catch (error) { + console.log("Error getting QBXML from Server.", error); + notification["error"]({ + message: t("payments.errors.exporting", { + error: "Unable to retrieve QBXML. " + JSON.stringify(error.message), + }), + }); + if (loadingCallback) loadingCallback(false); + setLoading(false); + return; + } + + try { + PartnerResponse = await axios.post( + "http://localhost:1337/qb/", + QbXmlResponse.data + ); + } catch (error) { + console.log("Error connecting to quickbooks or partner.", error); + notification["error"]({ + message: t("payments.errors.exporting-partner"), + }); + if (!!loadingCallback) loadingCallback(false); + setLoading(false); + return; + } } - let PartnerResponse; - - try { - PartnerResponse = await axios.post( - "http://localhost:1337/qb/", - QbXmlResponse.data - ); - } catch (error) { - console.log("Error connecting to quickbooks or partner.", error); - notification["error"]({ - message: t("payments.errors.exporting-partner"), - }); - if (!!loadingCallback) loadingCallback(false); - setLoading(false); - return; - } - - const groupedData = _.groupBy(PartnerResponse.data, "id"); + const groupedData = _.groupBy( + PartnerResponse.data, + bodyshop.accountingconfig.qbo ? "paymentid" : "id" + ); const proms = []; Object.keys(groupedData).forEach((key) => { proms.push( diff --git a/server.js b/server.js index 368fdf632..28a7da70a 100644 --- a/server.js +++ b/server.js @@ -150,6 +150,7 @@ app.post("/qbo/authorize", fb.validateFirebaseIdToken, qbo.authorize); app.get("/qbo/callback", qbo.callback); app.post("/qbo/receivables", fb.validateFirebaseIdToken, qbo.receivables); app.post("/qbo/payables", fb.validateFirebaseIdToken, qbo.payables); +app.post("/qbo/payments", fb.validateFirebaseIdToken, qbo.payments); var data = require("./server/data/data"); app.post("/data/ah", data.autohouse); diff --git a/server/accounting/qbo/qbo-payables.js b/server/accounting/qbo/qbo-payables.js index c68531b49..57d357c50 100644 --- a/server/accounting/qbo/qbo-payables.js +++ b/server/accounting/qbo/qbo-payables.js @@ -78,7 +78,8 @@ exports.default = async (req, res) => { ret.push({ billid: bill.id, success: false, - errorMessage: error.message, + errorMessage: + (error && error.authResponse.body) || JSON.stringify(error), }); } } @@ -113,7 +114,7 @@ async function QueryVendorRecord(oauthClient, req, bill) { ); } catch (error) { logger.log("qbo-payables-error", "DEBUG", req.user.email, bill.id, { - error: JSON.stringify(error), + error: (error && error.authResponse.body) || JSON.stringify(error), method: "QueryVendorRecord", }); throw error; @@ -136,7 +137,7 @@ async function InsertVendorRecord(oauthClient, req, bill) { return result && result.Vendor; } catch (error) { logger.log("qbo-payables-error", "DEBUG", req.user.email, bill.id, { - error: JSON.stringify(error), + error: (error && error.authResponse.body) || JSON.stringify(error), method: "InsertVendorRecord", }); throw error; @@ -185,7 +186,7 @@ async function InsertBill(oauthClient, req, bill, vendor) { return result && result.Bill; } catch (error) { logger.log("qbo-payables-error", "DEBUG", req.user.email, bill.id, { - error: JSON.stringify(error), + error: (error && error.authResponse.body) || JSON.stringify(error), method: "InsertBill", }); throw error; diff --git a/server/accounting/qbo/qbo-payments.js b/server/accounting/qbo/qbo-payments.js index e69de29bb..99ae12fc9 100644 --- a/server/accounting/qbo/qbo-payments.js +++ b/server/accounting/qbo/qbo-payments.js @@ -0,0 +1,274 @@ +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 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 { + QueryInsuranceCo, + InsertInsuranceCo, + InsertJob, + InsertOwner, + QueryJob, + QueryOwner, +} = require("../qbo/qbo-receivables"); +const { urlBuilder } = require("./qbo"); +const { DineroQbFormat } = require("../accounting-constants"); + +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, + }); + + oauthClient.setToken(response.associations[0].qbo_auth); + + await refreshOauthToken(oauthClient, req); + + const BearerToken = req.headers.authorization; + const { payments: paymentsToQuery } = req.body; + //Query Job Info + const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { + headers: { + Authorization: BearerToken, + }, + }); + logger.log("qbo-payment-create", "DEBUG", req.user.email, 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 { + 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 || twoTierPref === "source") { + //Insert the insurance company tier. + //Query for top level customer, the insurance company name. + insCoCustomerTier = await QueryInsuranceCo( + oauthClient, + req, + payment.job + ); + if (!insCoCustomerTier) { + //Creating the Insurance Customer. + insCoCustomerTier = await InsertInsuranceCo( + oauthClient, + req, + payment.job, + bodyshop + ); + } + } + + if (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, req, payment.job); + //Query for the owner itself. + if (!ownerCustomerTier) { + ownerCustomerTier = await InsertOwner( + oauthClient, + req, + payment.job, + isThreeTier, + insCoCustomerTier + ); + } + } + + //Query for the Job or Create it. + jobTier = await QueryJob(oauthClient, req, payment.job); + + // Need to validate that the job tier is associated to the right individual? + + if (!jobTier) { + jobTier = await InsertJob( + oauthClient, + req, + payment.job, + isThreeTier, + ownerCustomerTier + ); + } + + await InsertPayment(oauthClient, req, payment, jobTier); + ret.push({ paymentid: payment.id, success: true }); + } catch (error) { + logger.log("qbo-payment-create-error", "ERROR", req.user.email, { + error: (error && error.authResponse.body) || JSON.stringify(error), + }); + + ret.push({ + paymentid: payment.id, + success: false, + errorMessage: + (error && error.authResponse.body) || JSON.stringify(error), + }); + } + } + + res.status(200).json(ret); + } catch (error) { + console.log(error); + logger.log("qbo-payment-create-error", "ERROR", req.user.email, { error }); + res.status(400).json(error); + } +}; + +async function InsertPayment(oauthClient, req, payment, parentRef) { + const { paymentMethods, invoices } = await QueryMetaData( + oauthClient, + req, + payment.job.ro_number + ); + + if (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).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], + }, + Line: [ + { + Amount: Dinero({ + amount: Math.round(payment.amount * 100), + }).toFormat(DineroQbFormat), + LinkedTxn: [ + { + TxnId: invoices[0].Id, + TxnType: "Invoice", + }, + ], + }, + ], + }; + try { + const result = await oauthClient.makeApiCall({ + url: urlBuilder(req.cookies.qbo_realmId, "payment"), + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(paymentQbo), + }); + setNewRefreshToken(req.user.email, result); + return result && result.Bill; + } catch (error) { + logger.log("qbo-payables-error", "DEBUG", req.user.email, payment.id, { + error: JSON.stringify(error), + method: "InsertPayment", + }); + throw error; + } +} +async function QueryMetaData(oauthClient, req, ro_number) { + const invoice = await oauthClient.makeApiCall({ + url: urlBuilder( + req.cookies.qbo_realmId, + "query", + `select * From Invoice where DocNumber = '${ro_number}'` + ), + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + + const paymentMethods = await oauthClient.makeApiCall({ + url: urlBuilder( + req.cookies.qbo_realmId, + "query", + `select * From PaymentMethod` + ), + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + setNewRefreshToken(req.user.email, paymentMethods); + + // const classes = await oauthClient.makeApiCall({ + // url: urlBuilder(req.cookies.qbo_realmId, "query", `select * From Class`), + // method: "POST", + // headers: { + // "Content-Type": "application/json", + // }, + // }); + + const paymentMethodMapping = {}; + + paymentMethods.json && + paymentMethods.json.QueryResponse && + 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; + // }); + + return { + paymentMethods: paymentMethodMapping, + invoices: + invoice.json && + invoice.json.QueryResponse && + invoice.json.QueryResponse.Invoice, + }; +} diff --git a/server/accounting/qbo/qbo-receivables.js b/server/accounting/qbo/qbo-receivables.js index f23873544..3a9eafb31 100644 --- a/server/accounting/qbo/qbo-receivables.js +++ b/server/accounting/qbo/qbo-receivables.js @@ -80,7 +80,7 @@ exports.default = async (req, res) => { ); } } - console.log(insCoCustomerTier); + if (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, req, job); @@ -95,7 +95,7 @@ exports.default = async (req, res) => { ); } } - console.log(ownerCustomerTier); + //Query for the Job or Create it. jobTier = await QueryJob(oauthClient, req, job); @@ -110,14 +110,15 @@ exports.default = async (req, res) => { ownerCustomerTier ); } - console.log(jobTier); + await InsertInvoice(oauthClient, req, job, bodyshop, jobTier); ret.push({ jobid: job.id, success: true }); } catch (error) { ret.push({ jobid: job.id, success: false, - errorMessage: error.message, + errorMessage: + (error && error.authResponse.body) || JSON.stringify(error), }); } } @@ -160,6 +161,7 @@ async function QueryInsuranceCo(oauthClient, req, job) { throw error; } } +exports.QueryInsuranceCo = QueryInsuranceCo; async function InsertInsuranceCo(oauthClient, req, job, bodyshop) { const insCo = bodyshop.md_ins_cos.find((i) => i.name === job.ins_co_nm); @@ -192,7 +194,7 @@ async function InsertInsuranceCo(oauthClient, req, job, bodyshop) { throw error; } } - +exports.InsertInsuranceCo = InsertInsuranceCo; async function QueryOwner(oauthClient, req, job) { const ownerName = generateOwnerTier(job, true, null); const result = await oauthClient.makeApiCall({ @@ -214,7 +216,7 @@ async function QueryOwner(oauthClient, req, job) { result.json.QueryResponse.Customer[0] ); } - +exports.QueryOwner = QueryOwner; async function InsertOwner(oauthClient, req, job, isThreeTier, parentTierRef) { const ownerName = generateOwnerTier(job, true, null); const Customer = { @@ -254,7 +256,7 @@ async function InsertOwner(oauthClient, req, job, isThreeTier, parentTierRef) { throw error; } } - +exports.InsertOwner = InsertOwner; async function QueryJob(oauthClient, req, job) { const result = await oauthClient.makeApiCall({ url: urlBuilder( @@ -275,7 +277,7 @@ async function QueryJob(oauthClient, req, job) { result.json.QueryResponse.Customer[0] ); } - +exports.QueryJob = QueryJob; async function InsertJob(oauthClient, req, job, isThreeTier, parentTierRef) { const Customer = { DisplayName: job.ro_number, @@ -314,7 +316,7 @@ async function InsertJob(oauthClient, req, job, isThreeTier, parentTierRef) { throw error; } } - +exports.InsertJob = InsertJob; async function QueryMetaData(oauthClient, req) { const items = await oauthClient.makeApiCall({ url: urlBuilder(req.cookies.qbo_realmId, "query", `select * From Item`), diff --git a/server/accounting/qbo/qbo.js b/server/accounting/qbo/qbo.js index 190e5aa07..ab46a86c0 100644 --- a/server/accounting/qbo/qbo.js +++ b/server/accounting/qbo/qbo.js @@ -22,3 +22,4 @@ exports.authorize = require("./qbo-authorize").default; exports.refresh = require("./qbo-callback").refresh; exports.receivables = require("./qbo-receivables").default; exports.payables = require("./qbo-payables").default; +exports.payments = require("./qbo-payments").default; diff --git a/server/accounting/qbxml/qbxml-payments.js b/server/accounting/qbxml/qbxml-payments.js index ef53099cf..ec429aa86 100644 --- a/server/accounting/qbxml/qbxml-payments.js +++ b/server/accounting/qbxml/qbxml-payments.js @@ -142,9 +142,6 @@ const generatePayment = (payment, isThreeTier, twoTierPref) => { payment.stripeid || "" } ${payment.payer ? ` PAID BY ${payment.payer}` : ""}`, IsAutoApply: true, - // AppliedToTxnAdd:{ - // T - // } }, }, },