Files
bodyshop/server/accounting/qbo/qbo-payables.js
2022-03-31 12:50:06 -07:00

380 lines
10 KiB
JavaScript

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-timezone");
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 lines = 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
)
);
//QB USA with GST
//This was required for the No. 1 Collision Group.
if (
bill.job.bodyshop.accountingconfig &&
bill.job.bodyshop.accountingconfig.qbo &&
bill.job.bodyshop.accountingconfig.qbo_usa &&
bill.job.bodyshop.region_config.includes("CA_")
) {
lines.push({
DetailType: "AccountBasedExpenseLineDetail",
AccountBasedExpenseLineDetail: {
...(bill.job.class
? { ClassRef: { value: classes[bill.job.class] } }
: {}),
AccountRef: {
value:
accounts[
bill.job.bodyshop.md_responsibility_centers.taxes.federal
.accountdesc
],
},
},
Amount: Dinero({
amount: Math.round(
bill.billlines.reduce((acc, val) => {
return acc + val.actual_cost * val.quantity;
}, 0) * 100
),
})
.percentage(bill.federal_tax_rate)
.toFormat(DineroQbFormat),
});
}
const billQbo = {
VendorRef: {
value: vendor.Id,
},
TxnDate: moment(bill.date)
//.tz(bill.job.bodyshop.timezone)
.format("YYYY-MM-DD"),
...(!bill.is_credit_memo &&
bill.vendor.due_date && {
DueDate: moment(bill.date)
//.tz(bill.job.bodyshop.timezone)
.add(bill.vendor.due_date, "days")
.format("YYYY-MM-DD"),
}),
DocNumber: bill.invoice_number,
//...(bill.job.class ? { ClassRef: { Id: classes[bill.job.class] } } : {}),
PrivateNote: `RO ${bill.job.ro_number || ""}`,
Line: lines,
};
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);
return {
DetailType: "AccountBasedExpenseLineDetail",
AccountBasedExpenseLineDetail: {
...(jobClass ? { ClassRef: { value: 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 in ('Cost of Goods Sold', 'Other Current Liability')`
),
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) => {
classMapping[t.Name] = t.Id;
});
return {
accounts: accountMapping,
taxCodes: taxCodeMapping,
classes: classMapping,
};
}