IO-256 Exporting of payments

This commit is contained in:
Patrick Fic
2021-10-12 19:07:43 -07:00
parent fff9073f9d
commit 4d52a5c44a
10 changed files with 415 additions and 94 deletions

View File

@@ -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 && (
<QboAuthorizeComponent />
)}
<Input
value={state.search}
onChange={handleSearch}

View File

@@ -1,6 +1,6 @@
import { useLazyQuery } from "@apollo/client";
import { Input, Modal } from "antd";
import React, { useState, useEffect } from "react";
import { useLazyQuery, useQuery } from "@apollo/client";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { QUERY_SEARCH_OWNER_BY_IDX } from "../../graphql/owners.queries";
import AlertComponent from "../alert/alert.component";

View File

@@ -33,55 +33,64 @@ export function PaymentExportButton({
const handleQbxml = async () => {
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(),
},

View File

@@ -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(

View File

@@ -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);

View File

@@ -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;

View File

@@ -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,
};
}

View File

@@ -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`),

View File

@@ -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;

View File

@@ -142,9 +142,6 @@ const generatePayment = (payment, isThreeTier, twoTierPref) => {
payment.stripeid || ""
} ${payment.payer ? ` PAID BY ${payment.payer}` : ""}`,
IsAutoApply: true,
// AppliedToTxnAdd:{
// T
// }
},
},
},