463 lines
13 KiB
JavaScript
463 lines
13 KiB
JavaScript
const urlBuilder = require("./qbo").urlBuilder;
|
|
const StandardizeName = require("./qbo").StandardizeName;
|
|
|
|
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 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
|
|
});
|
|
|
|
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 { bills: billsToQuery, elgen } = req.body;
|
|
|
|
const BearerToken = req.BearerToken;
|
|
const client = req.userGraphQLClient;
|
|
|
|
logger.log("qbo-payable-create", "DEBUG", req.user.email, null, { billsToQuery });
|
|
|
|
const result = await client
|
|
.setHeaders({ Authorization: BearerToken })
|
|
.request(queries.QUERY_BILLS_FOR_PAYABLES_EXPORT, {
|
|
bills: billsToQuery
|
|
});
|
|
|
|
const { bills, bodyshops } = result;
|
|
const ret = [];
|
|
const bodyshop = bodyshops[0];
|
|
|
|
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);
|
|
}
|
|
|
|
await InsertBill(oauthClient, qbo_realmId, req, bill, vendorRecord, bodyshop);
|
|
|
|
// //No error. Mark the job exported & insert export log.
|
|
if (elgen) {
|
|
await client.setHeaders({ Authorization: BearerToken }).request(queries.QBO_MARK_BILL_EXPORTED, {
|
|
billId: bill.id,
|
|
bill: {
|
|
exported: true,
|
|
exported_at: moment().tz(bodyshop.timezone)
|
|
},
|
|
logs: [
|
|
{
|
|
bodyshopid: bodyshop.id,
|
|
billid: bill.id,
|
|
successful: true,
|
|
useremail: req.user.email
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
ret.push({ billid: bill.id, success: true });
|
|
} catch (error) {
|
|
logger.log("qbo-paybles-create-error", "ERROR", req.user.email, null, {
|
|
error:
|
|
(error?.authResponse && error.authResponse.body) ||
|
|
error.response?.data?.Fault?.Error.map((e) => e.Detail).join(", ") ||
|
|
error?.message
|
|
});
|
|
ret.push({
|
|
billid: bill.id,
|
|
success: false,
|
|
errorMessage:
|
|
(error && error.authResponse && error.authResponse.body) ||
|
|
error.response?.data?.Fault?.Error.map((e) => e.Detail).join(", ") ||
|
|
(error && error.message)
|
|
});
|
|
|
|
//Add the export log error.
|
|
if (elgen) {
|
|
await client.setHeaders({ Authorization: BearerToken }).request(queries.INSERT_EXPORT_LOG, {
|
|
logs: [
|
|
{
|
|
bodyshopid: bodyshop.id,
|
|
billid: bill.id,
|
|
successful: false,
|
|
message: JSON.stringify([
|
|
(error && error.authResponse && error.authResponse.body) || (error && error.message)
|
|
]),
|
|
useremail: req.user.email
|
|
}
|
|
]
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
res.status(200).json(ret);
|
|
} catch (error) {
|
|
//console.log(error);
|
|
logger.log("qbo-payable-create-error", "ERROR", req.user.email, null, {
|
|
error: error.message,
|
|
stack: error.stack
|
|
});
|
|
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"
|
|
}
|
|
});
|
|
logger.LogIntegrationCall({
|
|
platform: "QBO",
|
|
method: "POST",
|
|
name: "QueryVendorRecord",
|
|
billid: bill.id,
|
|
status: result.response?.status,
|
|
bodyshopid: bill.job.shopid,
|
|
email: req.user.email
|
|
});
|
|
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: StandardizeName(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)
|
|
});
|
|
logger.LogIntegrationCall({
|
|
platform: "QBO",
|
|
method: "POST",
|
|
name: "InsertVendorRecord",
|
|
billid: bill.id,
|
|
status: result.response?.status,
|
|
bodyshopid: bill.job.shopid,
|
|
email: req.user.email
|
|
});
|
|
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, bodyshop) {
|
|
const { accounts, taxCodes, classes } = await QueryMetaData(oauthClient, qbo_realmId, req, bill.job.shopid);
|
|
|
|
const lines = bill.billlines.map((il) =>
|
|
generateBillLine(
|
|
il,
|
|
accounts,
|
|
bill.job.class,
|
|
bodyshop.md_responsibility_centers.sales_tax_codes,
|
|
classes,
|
|
taxCodes,
|
|
bodyshop.md_responsibility_centers.costs,
|
|
bodyshop.accountingconfig,
|
|
bodyshop.region_config
|
|
)
|
|
);
|
|
|
|
//QB USA with GST
|
|
//This was required for the No. 1 Collision Group.
|
|
if (
|
|
bodyshop.accountingconfig &&
|
|
bodyshop.accountingconfig.qbo &&
|
|
bodyshop.accountingconfig.qbo_usa &&
|
|
bodyshop.region_config.includes("CA_")
|
|
) {
|
|
lines.push({
|
|
DetailType: "AccountBasedExpenseLineDetail",
|
|
|
|
AccountBasedExpenseLineDetail: {
|
|
...(bill.job.class ? { ClassRef: { value: classes[bill.job.class] } } : {}),
|
|
AccountRef: {
|
|
value: accounts[bodyshop.md_responsibility_centers.taxes.federal_itc.accountdesc]
|
|
}
|
|
},
|
|
|
|
Amount: Dinero({
|
|
amount: Math.round(
|
|
bill.billlines.reduce((acc, val) => {
|
|
return acc + (val.applicable_taxes?.federal ? (val.actual_cost * val.quantity ?? 0) : 0);
|
|
}, 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] } } : {}),
|
|
...(!(
|
|
bodyshop.accountingconfig &&
|
|
bodyshop.accountingconfig.qbo &&
|
|
bodyshop.accountingconfig.qbo_usa &&
|
|
bodyshop.region_config.includes("CA_")
|
|
)
|
|
? { GlobalTaxCalculation: "TaxExcluded" }
|
|
: {}),
|
|
...(bodyshop.accountingconfig.qbo_departmentid &&
|
|
bodyshop.accountingconfig.qbo_departmentid.trim() !== "" && {
|
|
DepartmentRef: { value: bodyshop.accountingconfig.qbo_departmentid }
|
|
}),
|
|
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)
|
|
});
|
|
logger.LogIntegrationCall({
|
|
platform: "QBO",
|
|
method: "POST",
|
|
name: "InsertBill",
|
|
billid: bill.id,
|
|
status: result.response?.status,
|
|
bodyshopid: bill.job.shopid,
|
|
email: req.user.email
|
|
});
|
|
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 && error.authResponse && error.authResponse.body) || (error && error.message),
|
|
validationError: JSON.stringify(error?.response?.data),
|
|
accountmeta: JSON.stringify({ accounts, taxCodes, classes }),
|
|
method: "InsertBill"
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// [
|
|
// {
|
|
// DetailType: "AccountBasedExpenseLineDetail",
|
|
// Amount: 200.0,
|
|
// Id: "1",
|
|
// AccountBasedExpenseLineDetail: {
|
|
// AccountRef: {
|
|
// value: "7",
|
|
// },
|
|
// },
|
|
// },
|
|
// ],
|
|
|
|
const generateBillLine = (
|
|
billLine,
|
|
accounts,
|
|
jobClass,
|
|
ioSalesTaxCodes,
|
|
classes,
|
|
taxCodes,
|
|
costCenters,
|
|
accountingconfig,
|
|
region_config
|
|
) => {
|
|
const account = costCenters.find((c) => c.name === billLine.cost_center);
|
|
return {
|
|
DetailType: "AccountBasedExpenseLineDetail",
|
|
|
|
AccountBasedExpenseLineDetail: {
|
|
...(jobClass ? { ClassRef: { value: classes[jobClass] } } : {}),
|
|
TaxCodeRef:
|
|
accountingconfig.qbo && accountingconfig.qbo_usa && region_config.includes("CA_")
|
|
? {}
|
|
: {
|
|
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, bodyshopid) {
|
|
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"
|
|
}
|
|
});
|
|
logger.LogIntegrationCall({
|
|
platform: "QBO",
|
|
method: "POST",
|
|
name: "QueryAccountType",
|
|
status: accounts.response?.status,
|
|
bodyshopid,
|
|
email: req.user.email
|
|
});
|
|
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"
|
|
}
|
|
});
|
|
logger.LogIntegrationCall({
|
|
platform: "QBO",
|
|
method: "POST",
|
|
name: "QueryTaxCode",
|
|
status: taxCodes.status,
|
|
bodyshopid,
|
|
email: req.user.email
|
|
});
|
|
const classes = await oauthClient.makeApiCall({
|
|
url: urlBuilder(
|
|
qbo_realmId,
|
|
"query",
|
|
`select *
|
|
From Class`
|
|
),
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
}
|
|
});
|
|
logger.LogIntegrationCall({
|
|
platform: "QBO",
|
|
method: "POST",
|
|
name: "QueryClasses",
|
|
status: classes.status,
|
|
bodyshopid,
|
|
email: req.user.email
|
|
});
|
|
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
|
|
};
|
|
}
|