144 lines
5.2 KiB
JavaScript
144 lines
5.2 KiB
JavaScript
'use strict';
|
|
|
|
const cron = require('node-cron'),
|
|
debug = require('debug')('agm:invoice_worker'),
|
|
env = require('../helpers/env.js'),
|
|
isProd = env.PRODUCTION,
|
|
{ DBConnection } = require('../helpers/db/connect'),
|
|
models = require('../model'),
|
|
{ InvoiceStatus, InvoiceStatusAction } = require('../helpers/constants'),
|
|
utils = require('../helpers/utils.js'),
|
|
{ isEqual, cloneDeep } = require('lodash'),
|
|
moment = require('moment');
|
|
|
|
// Initialize database connection
|
|
const workerDB = new DBConnection('Invoice Worker');
|
|
|
|
process
|
|
.on('uncaughtException', function (err) {
|
|
debug(err);
|
|
process.exit(1);
|
|
})
|
|
.on('unhandledRejection', (reason, p) => {
|
|
debug(reason, 'Unhandled Rejection at Promise', p);
|
|
});
|
|
|
|
// Initialize the database connection
|
|
workerDB.initialize({ setupExitHandlers: false });
|
|
|
|
// Checking on invoices and update (i.e.: status to automatically transit into the next status such as Draft to Open) them accordlingly
|
|
const processInvoices = {
|
|
schedule: isProd ? '*/1 * * * *' : `*/1 * * * *`,
|
|
status: 0,
|
|
name: "processInvoices"
|
|
};
|
|
const processInvoicesTask = cron.schedule(processInvoices.schedule, async () => {
|
|
// Check and only proceed when is idle and the db connection is connected
|
|
if (!workerDB.isReady() || processInvoices.status)
|
|
return;
|
|
|
|
let result = { nModified: 0 };
|
|
try {
|
|
processInvoices.status = 1;
|
|
|
|
const currDateUTC = moment.utc().endOf('day'); // Shift today to the end of day for easily checking with other dates counting by day
|
|
const bulkUpdateOps = [];
|
|
|
|
//1. Retreive the eligible invoices, by batch, for processing then loop through them while retreiving which is not a proper way.
|
|
const applInvoices = await models.Invoice.aggregate([
|
|
{
|
|
$match: { $or: [{ status: InvoiceStatus.Draft, openDate: { $lte: currDateUTC } }, { status: InvoiceStatus.Open }] }
|
|
},
|
|
{
|
|
$group: { _id: "$byPuid", "invoices": { $push: "$$ROOT" } }
|
|
},
|
|
{
|
|
$project: { "invoices._id": 1, "invoices.status": 1, "invoices.openDate": 1, "invoices.dueDate": 1, "invoices.clients": 1 }
|
|
},
|
|
{
|
|
$sort: { "invoices.openDate": 1, "invoices.dueDate": 1 }
|
|
},
|
|
{ $limit: env.INV_PROCESS_LIMIT }]);
|
|
|
|
let invUpdateOrg, invUpdate, openDate, dueDate;
|
|
/**
|
|
* The cached infoset by applicator userid.
|
|
* Structure: { <applId>: { <invSetting> : { the setting object }, etc. } }. The cached data by applicator for performance optimization (avoid too many round trips to the db servers)
|
|
*/
|
|
const applInfoSet = { nModified: 0 };
|
|
let invSetting, applId;
|
|
for (const applInv of applInvoices) {
|
|
if (!applInv?._id || utils.isEmptyArray(applInv.invoices)) continue;
|
|
|
|
applId = applInv._id;
|
|
invSetting = applInfoSet[applId];
|
|
if (invSetting === undefined) {
|
|
invSetting = await models.InvoiceSetting.findOne({ byPuid: applId, userId: null }, {}, { lean: true });
|
|
applInfoSet[applId] = invSetting;
|
|
}
|
|
|
|
let dueToUncollectibleDays = 0;
|
|
if (invSetting && invSetting.dueToUncollectibleOp === InvoiceStatusAction.MARK_UNCOLLECTIBLE) {
|
|
dueToUncollectibleDays = invSetting.dueToUncollectibleDays || env.INV_MAX_OVERDUE_DAYS;
|
|
}
|
|
|
|
for (const invoice of applInv.invoices) {
|
|
invUpdate = { status: invoice.status };
|
|
invUpdateOrg = cloneDeep(invUpdate);
|
|
|
|
openDate = moment.utc(invoice.openDate);
|
|
dueDate = moment.utc(invoice.dueDate);
|
|
|
|
// Case 1: invoice status is draft and current date >= open date => set status = open
|
|
if (invoice.status == InvoiceStatus.Draft && currDateUTC >= openDate) {
|
|
invUpdate.status = InvoiceStatus.Open;
|
|
}
|
|
|
|
if (invoice.status === InvoiceStatus.Open) {
|
|
// Case 2: Check Open invoice to transit to Paid or Uncollectible (should bases on a number of days param)
|
|
if (dueToUncollectibleDays && currDateUTC.diff(dueDate, "days") > dueToUncollectibleDays) {
|
|
invUpdate.status = InvoiceStatus.Uncollectible;
|
|
} else {
|
|
const clients = invoice.clients;
|
|
let isPaid = true;
|
|
// Check on all clients's amountDue to decide whether the invoice was fully paid
|
|
for (const client of clients) {
|
|
if (client.amountDue == undefined || Number(client.amountDue) > 0) {
|
|
isPaid = false;
|
|
break;
|
|
}
|
|
}
|
|
if (isPaid) {
|
|
invUpdate.status = InvoiceStatus.Paid;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!isEqual(invUpdateOrg, invUpdate)) {
|
|
const invUpdateDoc = { 'updateOne': { 'filter': { '_id': invoice._id, status: { $ne: InvoiceStatus.Paid } }, 'update': invUpdate } };
|
|
bulkUpdateOps.push(invUpdateDoc);
|
|
}
|
|
}
|
|
|
|
if (!utils.isEmptyArray(bulkUpdateOps)) {
|
|
const upResult = await models.Invoice.bulkWrite(bulkUpdateOps);
|
|
(upResult.ok) && (result.nModified += upResult.nModified);
|
|
}
|
|
}
|
|
return result;
|
|
} catch (error) {
|
|
debug(error);
|
|
} finally {
|
|
if (result) {
|
|
debug('Done. processInvoicesTask:', result);
|
|
}
|
|
processInvoices.status = 0;
|
|
}
|
|
},
|
|
{
|
|
scheduled: true,
|
|
timezone: "Etc/UTC",
|
|
name: processInvoices.name,
|
|
runOnInit: true
|
|
});
|