497 lines
15 KiB
JavaScript
497 lines
15 KiB
JavaScript
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-timezone");
|
|
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");
|
|
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) {
|
|
const result = 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) {
|
|
const result = 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, bodyshop) {
|
|
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, bodyshop) {
|
|
const { paymentMethods, 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 && error.message,
|
|
method: "InsertCreditMemo"
|
|
});
|
|
throw error;
|
|
}
|
|
}
|