import async from 'async'; import { Knex } from 'knex'; import { uniq } from 'lodash'; import { ILedger, ISaveAccountsBalanceQueuePayload, } from './types/Ledger.types'; import { Inject, Injectable } from '@nestjs/common'; import { Account } from '../Accounts/models/Account.model'; import { AccountRepository } from '../Accounts/repositories/Account.repository'; @Injectable() export class LedegrAccountsStorage { /** * @param {typeof Account} accountModel * @param {AccountRepository} accountRepository - */ constructor( @Inject(Account.name) private accountModel: typeof Account, @Inject(AccountRepository.name) private accountRepository: AccountRepository, ) {} /** * Retrieve depepants ids of the give accounts ids. * @param {number[]} accountsIds * @param depGraph * @returns {number[]} */ private getDependantsAccountsIds = ( accountsIds: number[], depGraph, ): number[] => { const depAccountsIds = []; accountsIds.forEach((accountId: number) => { const depAccountIds = depGraph.dependantsOf(accountId); depAccountsIds.push(accountId, ...depAccountIds); }); return uniq(depAccountsIds); }; /** * * @param {number[]} accountsIds * @returns {number[]} */ private findDependantsAccountsIds = async ( accountsIds: number[], trx?: Knex.Transaction, ): Promise => { const accountsGraph = await this.accountRepository.getDependencyGraph( null, trx, ); return this.getDependantsAccountsIds(accountsIds, accountsGraph); }; /** * Atomic mutation for accounts balances. * @param {number} tenantId * @param {ILedger} ledger * @param {Knex.Transaction} trx - * @returns {Promise} */ public saveAccountsBalance = async ( ledger: ILedger, trx?: Knex.Transaction, ): Promise => { // Initiate a new queue for accounts balance mutation. const saveAccountsBalanceQueue = async.queue( this.saveAccountBalanceTask, 10, ); const effectedAccountsIds = ledger.getAccountsIds(); const dependAccountsIds = await this.findDependantsAccountsIds( effectedAccountsIds, trx, ); dependAccountsIds.forEach((accountId: number) => { saveAccountsBalanceQueue.push({ ledger, accountId, trx }); }); if (dependAccountsIds.length > 0) { await saveAccountsBalanceQueue.drain(); } }; /** * Async task mutates the given account balance. * @param {ISaveAccountsBalanceQueuePayload} task * @returns {Promise} */ private saveAccountBalanceTask = async ( task: ISaveAccountsBalanceQueuePayload, ): Promise => { const { tenantId, ledger, accountId, trx } = task; await this.saveAccountBalanceFromLedger(tenantId, ledger, accountId, trx); }; /** * Saves specific account balance from the given ledger. * @param {number} tenantId * @param {ILedger} ledger * @param {number} accountId * @param {Knex.Transaction} trx - * @returns {Promise} */ private saveAccountBalanceFromLedger = async ( tenantId: number, ledger: ILedger, accountId: number, trx?: Knex.Transaction, ): Promise => { const account = await this.accountModel.query(trx).findById(accountId); // Filters the ledger entries by the current account. const accountLedger = ledger.whereAccountId(accountId); // Retrieves the given tenant metadata. const tenantMeta = await TenantMetadata.query().findOne({ tenantId }); // Detarmines whether the account has foreign currency. const isAccountForeign = account.currencyCode !== tenantMeta.baseCurrency; // Calculates the closing foreign balance by the given currency if account was has // foreign currency otherwise get closing balance. const closingBalance = isAccountForeign ? accountLedger .whereCurrencyCode(account.currencyCode) .getForeignClosingBalance() : accountLedger.getClosingBalance(); await this.saveAccountBalance(tenantId, accountId, closingBalance, trx); }; /** * Saves the account balance. * @param {number} tenantId * @param {number} accountId * @param {number} change * @param {Knex.Transaction} trx - * @returns {Promise} */ private saveAccountBalance = async ( accountId: number, change: number, trx?: Knex.Transaction, ) => { // Ensure the account has atleast zero in amount. await this.accountModel .query(trx) .findById(accountId) .whereNull('amount') .patch({ amount: 0 }); await this.accountModel.changeAmount( { id: accountId }, 'amount', change, trx, ); }; }