import { Service, Inject } from 'typedi'; import async from 'async'; import { Knex } from 'knex'; import { uniq } from 'lodash'; import { ILedger, ISaveAccountsBalanceQueuePayload } from '@/interfaces'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import { TenantMetadata } from '@/system/models'; @Service() export class LedegrAccountsStorage { @Inject() tenancy: HasTenancyService; /** * 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} tenantId * @param {number[]} accountsIds * @returns {number[]} */ private findDependantsAccountsIds = async ( tenantId: number, accountsIds: number[], trx?: Knex.Transaction ): Promise => { const { accountRepository } = this.tenancy.repositories(tenantId); const accountsGraph = await 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 ( tenantId: number, 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( tenantId, effectedAccountsIds, trx ); dependAccountsIds.forEach((accountId: number) => { saveAccountsBalanceQueue.push({ tenantId, 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 } = this.tenancy.models(tenantId); const account = await Account.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 ( tenantId: number, accountId: number, change: number, trx?: Knex.Transaction ) => { const { Account } = this.tenancy.models(tenantId); // Ensure the account has atleast zero in amount. await Account.query(trx) .findById(accountId) .whereNull('amount') .patch({ amount: 0 }); await Account.changeAmount({ id: accountId }, 'amount', change, trx); }; }