agmission/Development/server/workers/invoice_worker.js

140 lines
5.1 KiB
JavaScript

'use strict';
const cron = require('node-cron'),
debug = require('debug')('agm:invoice_worker'),
env = require('../helpers/env.js'),
isProd = env.PRODUCTION,
dbConn = require('../helpers/db/connect.js')(),
// dbConn = require('../helpers/db/connect-remote.js')(),
models = require('../model'),
{ InvoiceStatus, InvoiceStatusAction } = require('../helpers/constants'),
utils = require('../helpers/utils.js'),
{ isEqual, cloneDeep } = require('lodash'),
moment = require('moment');
process
.on('uncaughtException', function (err) {
debug(err);
process.exit(1);
})
.on('unhandledRejection', (reason, p) => {
debug(reason, 'Unhandled Rejection at Promise', p);
});
// 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 (!dbConn || dbConn.readyState !== 1 || 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
});