diff --git a/packages/server/src/api/controllers/Banking/BankingRulesController.ts b/packages/server/src/api/controllers/Banking/BankingRulesController.ts index 08c83edff..e7608f56d 100644 --- a/packages/server/src/api/controllers/Banking/BankingRulesController.ts +++ b/packages/server/src/api/controllers/Banking/BankingRulesController.ts @@ -43,8 +43,6 @@ export class BankingRulesController extends BaseController { body('assign_account_id').isInt({ min: 0 }), body('assign_payee').isString().optional({ nullable: true }), body('assign_memo').isString().optional({ nullable: true }), - - body('recognition').isBoolean().toBoolean().optional({ nullable: true }), ]; } diff --git a/packages/server/src/loaders/jobs.ts b/packages/server/src/loaders/jobs.ts index c64a8fe72..d3a4dbc1a 100644 --- a/packages/server/src/loaders/jobs.ts +++ b/packages/server/src/loaders/jobs.ts @@ -13,7 +13,9 @@ import { PaymentReceiveMailNotificationJob } from '@/services/Sales/PaymentRecei import { PlaidFetchTransactionsJob } from '@/services/Banking/Plaid/PlaidFetchTransactionsJob'; import { ImportDeleteExpiredFilesJobs } from '@/services/Import/jobs/ImportDeleteExpiredFilesJob'; import { SendVerifyMailJob } from '@/services/Authentication/jobs/SendVerifyMailJob'; -import { RegonizeTransactionsJob } from '@/services/Banking/RegonizeTranasctions/RecognizeTransactionsJob'; +import { ReregonizeTransactionsJob } from '@/services/Banking/RegonizeTranasctions/jobs/RerecognizeTransactionsJob'; +import { RegonizeTransactionsJob } from '@/services/Banking/RegonizeTranasctions/jobs/RecognizeTransactionsJob'; +import { RevertRegonizeTransactionsJob } from '@/services/Banking/RegonizeTranasctions/jobs/RevertRecognizedTransactionsJob'; export default ({ agenda }: { agenda: Agenda }) => { new ResetPasswordMailJob(agenda); @@ -31,6 +33,8 @@ export default ({ agenda }: { agenda: Agenda }) => { new ImportDeleteExpiredFilesJobs(agenda); new SendVerifyMailJob(agenda); new RegonizeTransactionsJob(agenda); + new ReregonizeTransactionsJob(agenda); + new RevertRegonizeTransactionsJob(agenda); agenda.start().then(() => { agenda.every('1 hours', 'delete-expired-imported-files', {}); diff --git a/packages/server/src/services/Banking/Plaid/subscribers/RecognizeSyncedBankTransactions.ts b/packages/server/src/services/Banking/Plaid/subscribers/RecognizeSyncedBankTransactions.ts index 42104aafc..e68aecffe 100644 --- a/packages/server/src/services/Banking/Plaid/subscribers/RecognizeSyncedBankTransactions.ts +++ b/packages/server/src/services/Banking/Plaid/subscribers/RecognizeSyncedBankTransactions.ts @@ -35,7 +35,8 @@ export class RecognizeSyncedBankTranasctions extends EventSubscriber { runAfterTransaction(trx, async () => { await this.recognizeTranasctionsService.recognizeTransactions( tenantId, - batch + null, + { batch } ); }); }; diff --git a/packages/server/src/services/Banking/RegonizeTranasctions/RecognizeTranasctionsService.ts b/packages/server/src/services/Banking/RegonizeTranasctions/RecognizeTranasctionsService.ts index f7e81d4f6..6c3ddb052 100644 --- a/packages/server/src/services/Banking/RegonizeTranasctions/RecognizeTranasctionsService.ts +++ b/packages/server/src/services/Banking/RegonizeTranasctions/RecognizeTranasctionsService.ts @@ -1,11 +1,13 @@ import { Knex } from 'knex'; import { Inject, Service } from 'typedi'; +import { castArray, isEmpty } from 'lodash'; import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTransaction'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import { transformToMapBy } from '@/utils'; import { PromisePool } from '@supercharge/promise-pool'; import { BankRule } from '@/models/BankRule'; import { bankRulesMatchTransaction } from './_utils'; +import { RecognizeTransactionsCriteria } from './_types'; @Service() export class RecognizeTranasctionsService { @@ -48,24 +50,42 @@ export class RecognizeTranasctionsService { /** * Regonized the uncategorized transactions. * @param {number} tenantId - + * @param {number|Array} ruleId - The target rule id/ids. + * @param {RecognizeTransactionsCriteria} * @param {Knex.Transaction} trx - */ public async recognizeTransactions( tenantId: number, - batch: string = '', + ruleId?: number | Array, + transactionsCriteria?: RecognizeTransactionsCriteria, trx?: Knex.Transaction ) { const { UncategorizedCashflowTransaction, BankRule } = this.tenancy.models(tenantId); const uncategorizedTranasctions = - await UncategorizedCashflowTransaction.query().onBuild((query) => { - query.where('recognized_transaction_id', null); - query.where('categorized', false); + await UncategorizedCashflowTransaction.query(trx).onBuild((query) => { + query.modify('notRecognized'); + query.modify('notCategorized'); - if (batch) query.where('batch', batch); + // Filter the transactions based on the given criteria. + if (transactionsCriteria?.batch) { + query.where('batch', transactionsCriteria.batch); + } + if (transactionsCriteria?.accountId) { + query.where('accountId', transactionsCriteria.accountId); + } }); - const bankRules = await BankRule.query().withGraphFetched('conditions'); + + const bankRules = await BankRule.query(trx).onBuild((q) => { + const rulesIds = castArray(ruleId); + + if (!isEmpty(rulesIds)) { + q.whereIn('id', rulesIds); + } + q.withGraphFetched('conditions'); + }); + const bankRulesByAccountId = transformToMapBy( bankRules, 'applyIfAccountId' diff --git a/packages/server/src/services/Banking/RegonizeTranasctions/RevertRecognizedTransactions.ts b/packages/server/src/services/Banking/RegonizeTranasctions/RevertRecognizedTransactions.ts new file mode 100644 index 000000000..2d8247506 --- /dev/null +++ b/packages/server/src/services/Banking/RegonizeTranasctions/RevertRecognizedTransactions.ts @@ -0,0 +1,72 @@ +import { Inject, Service } from 'typedi'; +import { castArray } from 'lodash'; +import { Knex } from 'knex'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import UnitOfWork from '@/services/UnitOfWork'; +import { RevertRecognizedTransactionsCriteria } from './_types'; + +@Service() +export class RevertRecognizedTransactions { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private uow: UnitOfWork; + + /** + * Revert and unlinks the recognized transactions based on the given bank rule + * and transactions criteria.. + * @param {number} tenantId - Tenant id. + * @param {number|Array} bankRuleId - Bank rule id. + * @param {RevertRecognizedTransactionsCriteria} transactionsCriteria - + * @param {Knex.Transaction} trx - Knex transaction. + * @returns {Promise} + */ + public async revertRecognizedTransactions( + tenantId: number, + ruleId?: number | Array, + transactionsCriteria?: RevertRecognizedTransactionsCriteria, + trx?: Knex.Transaction + ): Promise { + const { UncategorizedCashflowTransaction, RecognizedBankTransaction } = + this.tenancy.models(tenantId); + + const rulesIds = castArray(ruleId); + + return this.uow.withTransaction( + tenantId, + async (trx: Knex.Transaction) => { + // Retrieves all the recognized transactions of the banbk rule. + const uncategorizedTransactions = + await UncategorizedCashflowTransaction.query(trx).onBuild((q) => { + q.withGraphJoined('recognizedTransaction'); + q.whereNotNull('recognizedTransaction.id'); + + if (rulesIds.length > 0) { + q.whereIn('recognizedTransaction.bankRuleId', rulesIds); + } + if (transactionsCriteria?.accountId) { + q.where('accountId', transactionsCriteria.accountId); + } + if (transactionsCriteria?.batch) { + q.where('batch', transactionsCriteria.batch); + } + }); + const uncategorizedTransactionIds = uncategorizedTransactions.map( + (r) => r.id + ); + // Unlink the recongized transactions out of uncategorized transactions. + await UncategorizedCashflowTransaction.query(trx) + .whereIn('id', uncategorizedTransactionIds) + .patch({ + recognizedTransactionId: null, + }); + // Delete the recognized bank transactions that assocaited to bank rule. + await RecognizedBankTransaction.query(trx) + .whereIn('uncategorizedTransactionId', uncategorizedTransactionIds) + .delete(); + }, + trx + ); + } +} diff --git a/packages/server/src/services/Banking/RegonizeTranasctions/_types.ts b/packages/server/src/services/Banking/RegonizeTranasctions/_types.ts new file mode 100644 index 000000000..92f394b0a --- /dev/null +++ b/packages/server/src/services/Banking/RegonizeTranasctions/_types.ts @@ -0,0 +1,11 @@ +export interface RevertRecognizedTransactionsCriteria { + batch?: string; + accountId?: number; +} + + +export interface RecognizeTransactionsCriteria { + batch?: string; + accountId?: number; +} + diff --git a/packages/server/src/services/Banking/RegonizeTranasctions/events/TriggerRecognizedTransactions.ts b/packages/server/src/services/Banking/RegonizeTranasctions/events/TriggerRecognizedTransactions.ts index bc5285d1b..0ee073a46 100644 --- a/packages/server/src/services/Banking/RegonizeTranasctions/events/TriggerRecognizedTransactions.ts +++ b/packages/server/src/services/Banking/RegonizeTranasctions/events/TriggerRecognizedTransactions.ts @@ -41,14 +41,10 @@ export class TriggerRecognizedTransactions { */ private async recognizedTransactionsOnRuleCreated({ tenantId, - createRuleDTO, + bankRule, }: IBankRuleEventCreatedPayload) { - const payload = { tenantId }; + const payload = { tenantId, ruleId: bankRule.id }; - // Cannot run recognition if the option is not enabled. - if (createRuleDTO.recognition) { - return; - } await this.agenda.now('recognize-uncategorized-transactions-job', payload); } @@ -59,14 +55,14 @@ export class TriggerRecognizedTransactions { private async recognizedTransactionsOnRuleEdited({ tenantId, editRuleDTO, + ruleId, }: IBankRuleEventEditedPayload) { - const payload = { tenantId }; + const payload = { tenantId, ruleId }; - // Cannot run recognition if the option is not enabled. - if (!editRuleDTO.recognition) { - return; - } - await this.agenda.now('recognize-uncategorized-transactions-job', payload); + await this.agenda.now( + 'rerecognize-uncategorized-transactions-job', + payload + ); } /** @@ -75,9 +71,13 @@ export class TriggerRecognizedTransactions { */ private async recognizedTransactionsOnRuleDeleted({ tenantId, + ruleId, }: IBankRuleEventDeletedPayload) { - const payload = { tenantId }; - await this.agenda.now('recognize-uncategorized-transactions-job', payload); + const payload = { tenantId, ruleId }; + await this.agenda.now( + 'revert-recognized-uncategorized-transactions-job', + payload + ); } /** @@ -91,7 +91,7 @@ export class TriggerRecognizedTransactions { }: IImportFileCommitedEventPayload) { const importFile = await Import.query().findOne({ importId }); const batch = importFile.paramsParsed.batch; - const payload = { tenantId, batch }; + const payload = { tenantId, transactionsCriteria: { batch } }; await this.agenda.now('recognize-uncategorized-transactions-job', payload); } diff --git a/packages/server/src/services/Banking/RegonizeTranasctions/RecognizeTransactionsJob.ts b/packages/server/src/services/Banking/RegonizeTranasctions/jobs/RecognizeTransactionsJob.ts similarity index 67% rename from packages/server/src/services/Banking/RegonizeTranasctions/RecognizeTransactionsJob.ts rename to packages/server/src/services/Banking/RegonizeTranasctions/jobs/RecognizeTransactionsJob.ts index 9a3d625a3..319ec8a10 100644 --- a/packages/server/src/services/Banking/RegonizeTranasctions/RecognizeTransactionsJob.ts +++ b/packages/server/src/services/Banking/RegonizeTranasctions/jobs/RecognizeTransactionsJob.ts @@ -1,5 +1,5 @@ import Container, { Service } from 'typedi'; -import { RecognizeTranasctionsService } from './RecognizeTranasctionsService'; +import { RecognizeTranasctionsService } from '../RecognizeTranasctionsService'; @Service() export class RegonizeTransactionsJob { @@ -18,11 +18,15 @@ export class RegonizeTransactionsJob { * Triggers sending invoice mail. */ private handler = async (job, done: Function) => { - const { tenantId, batch } = job.attrs.data; + const { tenantId, ruleId, transactionsCriteria } = job.attrs.data; const regonizeTransactions = Container.get(RecognizeTranasctionsService); try { - await regonizeTransactions.recognizeTransactions(tenantId, batch); + await regonizeTransactions.recognizeTransactions( + tenantId, + ruleId, + transactionsCriteria + ); done(); } catch (error) { console.log(error); diff --git a/packages/server/src/services/Banking/RegonizeTranasctions/jobs/RerecognizeTransactionsJob.ts b/packages/server/src/services/Banking/RegonizeTranasctions/jobs/RerecognizeTransactionsJob.ts new file mode 100644 index 000000000..1c50a1f81 --- /dev/null +++ b/packages/server/src/services/Banking/RegonizeTranasctions/jobs/RerecognizeTransactionsJob.ts @@ -0,0 +1,45 @@ +import Container, { Service } from 'typedi'; +import { RecognizeTranasctionsService } from '../RecognizeTranasctionsService'; +import { RevertRecognizedTransactions } from '../RevertRecognizedTransactions'; + +@Service() +export class ReregonizeTransactionsJob { + /** + * Constructor method. + */ + constructor(agenda) { + agenda.define( + 'rerecognize-uncategorized-transactions-job', + { priority: 'high', concurrency: 2 }, + this.handler + ); + } + + /** + * Triggers sending invoice mail. + */ + private handler = async (job, done: Function) => { + const { tenantId, ruleId, transactionsCriteria } = job.attrs.data; + const regonizeTransactions = Container.get(RecognizeTranasctionsService); + const revertRegonizedTransactions = Container.get( + RevertRecognizedTransactions + ); + + try { + await revertRegonizedTransactions.revertRecognizedTransactions( + tenantId, + ruleId, + transactionsCriteria + ); + await regonizeTransactions.recognizeTransactions( + tenantId, + ruleId, + transactionsCriteria + ); + done(); + } catch (error) { + console.log(error); + done(error); + } + }; +} diff --git a/packages/server/src/services/Banking/RegonizeTranasctions/jobs/RevertRecognizedTransactionsJob.ts b/packages/server/src/services/Banking/RegonizeTranasctions/jobs/RevertRecognizedTransactionsJob.ts new file mode 100644 index 000000000..a1d378675 --- /dev/null +++ b/packages/server/src/services/Banking/RegonizeTranasctions/jobs/RevertRecognizedTransactionsJob.ts @@ -0,0 +1,38 @@ +import Container, { Service } from 'typedi'; +import { RevertRecognizedTransactions } from '../RevertRecognizedTransactions'; + +@Service() +export class RevertRegonizeTransactionsJob { + /** + * Constructor method. + */ + constructor(agenda) { + agenda.define( + 'revert-recognized-uncategorized-transactions-job', + { priority: 'high', concurrency: 2 }, + this.handler + ); + } + + /** + * Triggers sending invoice mail. + */ + private handler = async (job, done: Function) => { + const { tenantId, ruleId, transactionsCriteria } = job.attrs.data; + const revertRegonizedTransactions = Container.get( + RevertRecognizedTransactions + ); + + try { + await revertRegonizedTransactions.revertRecognizedTransactions( + tenantId, + ruleId, + transactionsCriteria + ); + done(); + } catch (error) { + console.log(error); + done(error); + } + }; +} diff --git a/packages/server/src/services/Banking/Rules/CreateBankRule.ts b/packages/server/src/services/Banking/Rules/CreateBankRule.ts index ccca113e3..1ca3a99e6 100644 --- a/packages/server/src/services/Banking/Rules/CreateBankRule.ts +++ b/packages/server/src/services/Banking/Rules/CreateBankRule.ts @@ -62,6 +62,7 @@ export class CreateBankRuleService { await this.eventPublisher.emitAsync(events.bankRules.onCreated, { tenantId, createRuleDTO, + bankRule, trx, } as IBankRuleEventCreatedPayload); diff --git a/packages/server/src/services/Banking/Rules/UnlinkBankRuleRecognizedTransactions.ts b/packages/server/src/services/Banking/Rules/UnlinkBankRuleRecognizedTransactions.ts deleted file mode 100644 index 25bff99ef..000000000 --- a/packages/server/src/services/Banking/Rules/UnlinkBankRuleRecognizedTransactions.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Inject, Service } from 'typedi'; -import { Knex } from 'knex'; -import HasTenancyService from '@/services/Tenancy/TenancyService'; -import UnitOfWork from '@/services/UnitOfWork'; - -@Service() -export class UnlinkBankRuleRecognizedTransactions { - @Inject() - private tenancy: HasTenancyService; - - @Inject() - private uow: UnitOfWork; - - /** - * Unlinks the given bank rule out of recognized transactions. - * @param {number} tenantId - Tenant id. - * @param {number} bankRuleId - Bank rule id. - * @param {Knex.Transaction} trx - Knex transaction. - * @returns {Promise} - */ - public async unlinkBankRuleOutRecognizedTransactions( - tenantId: number, - bankRuleId: number, - trx?: Knex.Transaction - ): Promise { - const { UncategorizedCashflowTransaction, RecognizedBankTransaction } = - this.tenancy.models(tenantId); - - return this.uow.withTransaction( - tenantId, - async (trx: Knex.Transaction) => { - // Retrieves all the recognized transactions of the banbk rule. - const recognizedTransactions = await RecognizedBankTransaction.query( - trx - ).where('bankRuleId', bankRuleId); - - const uncategorizedTransactionIds = recognizedTransactions.map( - (r) => r.uncategorizedTransactionId - ); - // Unlink the recongized transactions out of uncategorized transactions. - await UncategorizedCashflowTransaction.query(trx) - .whereIn('id', uncategorizedTransactionIds) - .patch({ - recognizedTransactionId: null, - }); - // Delete the recognized bank transactions that assocaited to bank rule. - await RecognizedBankTransaction.query(trx) - .where({ bankRuleId }) - .delete(); - }, - trx - ); - } -} diff --git a/packages/server/src/services/Banking/Rules/events/UnlinkBankRuleOnDeleteBankRule.ts b/packages/server/src/services/Banking/Rules/events/UnlinkBankRuleOnDeleteBankRule.ts index 91ad36fe9..42778e968 100644 --- a/packages/server/src/services/Banking/Rules/events/UnlinkBankRuleOnDeleteBankRule.ts +++ b/packages/server/src/services/Banking/Rules/events/UnlinkBankRuleOnDeleteBankRule.ts @@ -1,12 +1,12 @@ import { Inject, Service } from 'typedi'; import events from '@/subscribers/events'; -import { UnlinkBankRuleRecognizedTransactions } from '../UnlinkBankRuleRecognizedTransactions'; import { IBankRuleEventDeletingPayload } from '../types'; +import { RevertRecognizedTransactions } from '../../RegonizeTranasctions/RevertRecognizedTransactions'; @Service() export class UnlinkBankRuleOnDeleteBankRule { @Inject() - private unlinkBankRule: UnlinkBankRuleRecognizedTransactions; + private revertRecognizedTransactionsService: RevertRecognizedTransactions; /** * Constructor method. @@ -26,7 +26,7 @@ export class UnlinkBankRuleOnDeleteBankRule { tenantId, ruleId, }: IBankRuleEventDeletingPayload) { - await this.unlinkBankRule.unlinkBankRuleOutRecognizedTransactions( + await this.revertRecognizedTransactionsService.revertRecognizedTransactions( tenantId, ruleId ); diff --git a/packages/server/src/services/Banking/Rules/types.ts b/packages/server/src/services/Banking/Rules/types.ts index 66017bf2b..d54cb9388 100644 --- a/packages/server/src/services/Banking/Rules/types.ts +++ b/packages/server/src/services/Banking/Rules/types.ts @@ -30,6 +30,7 @@ export enum BankRuleApplyIfTransactionType { } export interface IBankRule { + id?: number; name: string; order?: number; applyIfAccountId: number; @@ -71,8 +72,6 @@ export interface IBankRuleCommonDTO { assignAccountId: number; assignPayee?: string; assignMemo?: string; - - recognition?: boolean; } export interface ICreateBankRuleDTO extends IBankRuleCommonDTO {} @@ -86,6 +85,7 @@ export interface IBankRuleEventCreatingPayload { export interface IBankRuleEventCreatedPayload { tenantId: number; createRuleDTO: ICreateBankRuleDTO; + bankRule: IBankRule; trx?: Knex.Transaction; }