'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: { : { : { 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 });