Merge pull request #569 from bigcapitalhq/fix-edit-bank-rule-recognized

fix: Recognize transactions on editing bank rule
This commit is contained in:
Ahmed Bouhuolia
2024-08-08 00:26:18 +02:00
committed by GitHub
14 changed files with 227 additions and 87 deletions

View File

@@ -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 }),
];
}

View File

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

View File

@@ -35,7 +35,8 @@ export class RecognizeSyncedBankTranasctions extends EventSubscriber {
runAfterTransaction(trx, async () => {
await this.recognizeTranasctionsService.recognizeTransactions(
tenantId,
batch
null,
{ batch }
);
});
};

View File

@@ -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<number>} ruleId - The target rule id/ids.
* @param {RecognizeTransactionsCriteria}
* @param {Knex.Transaction} trx -
*/
public async recognizeTransactions(
tenantId: number,
batch: string = '',
ruleId?: number | Array<number>,
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'

View File

@@ -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<number>} bankRuleId - Bank rule id.
* @param {RevertRecognizedTransactionsCriteria} transactionsCriteria -
* @param {Knex.Transaction} trx - Knex transaction.
* @returns {Promise<void>}
*/
public async revertRecognizedTransactions(
tenantId: number,
ruleId?: number | Array<number>,
transactionsCriteria?: RevertRecognizedTransactionsCriteria,
trx?: Knex.Transaction
): Promise<void> {
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
);
}
}

View File

@@ -0,0 +1,11 @@
export interface RevertRecognizedTransactionsCriteria {
batch?: string;
accountId?: number;
}
export interface RecognizeTransactionsCriteria {
batch?: string;
accountId?: number;
}

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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);
}
};
}

View File

@@ -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);
}
};
}

View File

@@ -62,6 +62,7 @@ export class CreateBankRuleService {
await this.eventPublisher.emitAsync(events.bankRules.onCreated, {
tenantId,
createRuleDTO,
bankRule,
trx,
} as IBankRuleEventCreatedPayload);

View File

@@ -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<void>}
*/
public async unlinkBankRuleOutRecognizedTransactions(
tenantId: number,
bankRuleId: number,
trx?: Knex.Transaction
): Promise<void> {
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
);
}
}

View File

@@ -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
);

View File

@@ -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;
}