refactor(nestjs): banking module

This commit is contained in:
Ahmed Bouhuolia
2025-06-02 21:32:53 +02:00
parent 7247b52fe5
commit 5595478e19
18 changed files with 216 additions and 103 deletions

View File

@@ -1,5 +1,4 @@
import { Controller, Get, Param, Query } from '@nestjs/common';
import { BankingTransactionsApplication } from '../BankingTransactions/BankingTransactionsApplication.service';
import { ApiTags } from '@nestjs/swagger';
import { RecognizedTransactionsApplication } from './RecognizedTransactions.application';

View File

@@ -1,7 +1,6 @@
import { forwardRef, Module } from '@nestjs/common';
import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module';
import { RecognizedBankTransaction } from './models/RecognizedBankTransaction';
import { GetAutofillCategorizeTransactionService } from './queries/GetAutofillCategorizeTransaction.service';
import { RevertRecognizedTransactionsService } from './commands/RevertRecognizedTransactions.service';
import { RecognizeTranasctionsService } from './commands/RecognizeTranasctions.service';
import { TriggerRecognizedTransactionsSubscriber } from './events/TriggerRecognizedTransactions';
@@ -11,27 +10,34 @@ import { BankingRecognizedTransactionsController } from './BankingRecognizedTran
import { RecognizedTransactionsApplication } from './RecognizedTransactions.application';
import { GetRecognizedTransactionsService } from './GetRecongizedTransactions';
import { GetRecognizedTransactionService } from './queries/GetRecognizedTransaction.service';
import { BullModule } from '@nestjs/bullmq';
import { RecognizeUncategorizedTransactionsQueue } from './_types';
import { RegonizeTransactionsPrcessor } from './jobs/RecognizeTransactionsJob';
import { TenancyModule } from '../Tenancy/Tenancy.module';
const models = [RegisterTenancyModel(RecognizedBankTransaction)];
@Module({
imports: [
BankingTransactionsModule,
TenancyModule,
forwardRef(() => BankRulesModule),
BullModule.registerQueue({
name: RecognizeUncategorizedTransactionsQueue,
}),
...models,
],
providers: [
RecognizedTransactionsApplication,
GetRecognizedTransactionsService,
GetAutofillCategorizeTransactionService,
RevertRecognizedTransactionsService,
RecognizeTranasctionsService,
TriggerRecognizedTransactionsSubscriber,
GetRecognizedTransactionService,
RegonizeTransactionsPrcessor,
],
exports: [
...models,
GetAutofillCategorizeTransactionService,
RevertRecognizedTransactionsService,
RecognizeTranasctionsService,
],

View File

@@ -3,7 +3,6 @@ import { Injectable } from '@nestjs/common';
import { GetRecognizedTransactionsService } from './GetRecongizedTransactions';
import { GetRecognizedTransactionService } from './queries/GetRecognizedTransaction.service';
import { RevertRecognizedTransactionsService } from './commands/RevertRecognizedTransactions.service';
import { GetAutofillCategorizeTransactionService } from './queries/GetAutofillCategorizeTransaction.service';
import { IGetRecognizedTransactionsQuery } from '../BankingTransactions/types/BankingTransactions.types';
import { RevertRecognizedTransactionsCriteria } from './_types';
@@ -13,7 +12,6 @@ export class RecognizedTransactionsApplication {
private readonly getRecognizedTransactionsService: GetRecognizedTransactionsService,
private readonly getRecognizedTransactionService: GetRecognizedTransactionService,
private readonly revertRecognizedTransactionsService: RevertRecognizedTransactionsService,
private readonly getAutofillCategorizeTransactionService: GetAutofillCategorizeTransactionService,
) {}
/**
@@ -56,15 +54,4 @@ export class RecognizedTransactionsApplication {
trx,
);
}
/**
* Gets autofill categorize suggestions for a transaction.
* @param {number} uncategorizedTransactionId - The ID of the uncategorized transaction.
* @returns {Promise<any>}
*/
public getAutofillCategorizeTransaction(uncategorizedTransactionId: number) {
return this.getAutofillCategorizeTransactionService.getAutofillCategorizeTransaction(
uncategorizedTransactionId,
);
}
}

View File

@@ -1,3 +1,5 @@
import { TenantJobPayload } from "@/interfaces/Tenant";
export interface RevertRecognizedTransactionsCriteria {
batch?: string;
accountId?: number;
@@ -7,3 +9,14 @@ export interface RecognizeTransactionsCriteria {
batch?: string;
accountId?: number;
}
export const RecognizeUncategorizedTransactionsJob =
'recognize-uncategorized-transactions-job';
export const RecognizeUncategorizedTransactionsQueue =
'recognize-uncategorized-transactions-queue';
export interface RecognizeUncategorizedTransactionsJobPayload extends TenantJobPayload {
ruleId: number,
transactionsCriteria: any;
}

View File

@@ -2,21 +2,47 @@ import { isEqual, omit } from 'lodash';
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import { IBankRuleEventCreatedPayload, IBankRuleEventDeletedPayload, IBankRuleEventEditedPayload } from '@/modules/BankRules/types';
import {
IBankRuleEventCreatedPayload,
IBankRuleEventDeletedPayload,
IBankRuleEventEditedPayload,
} from '@/modules/BankRules/types';
import { Queue } from 'bullmq';
import { InjectQueue } from '@nestjs/bullmq';
import {
RecognizeUncategorizedTransactionsJob,
RecognizeUncategorizedTransactionsJobPayload,
RecognizeUncategorizedTransactionsQueue,
} from '../_types';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Injectable()
export class TriggerRecognizedTransactionsSubscriber {
constructor(
private readonly tenancyContect: TenancyContext,
@InjectQueue(RecognizeUncategorizedTransactionsQueue)
private readonly recognizeTransactionsQueue: Queue,
) {}
/**
* Triggers the recognize uncategorized transactions job on rule created.
* @param {IBankRuleEventCreatedPayload} payload -
*/
@OnEvent(events.bankRules.onCreated)
private async recognizedTransactionsOnRuleCreated({
async recognizedTransactionsOnRuleCreated({
bankRule,
}: IBankRuleEventCreatedPayload) {
const payload = { ruleId: bankRule.id };
const tenantPayload = await this.tenancyContect.getTenantJobPayload();
const payload = {
ruleId: bankRule.id,
...tenantPayload,
} as RecognizeUncategorizedTransactionsJobPayload;
// await this.agenda.now('recognize-uncategorized-transactions-job', payload);
await this.recognizeTransactionsQueue.add(
RecognizeUncategorizedTransactionsJob,
payload,
);
}
/**
@@ -29,22 +55,28 @@ export class TriggerRecognizedTransactionsSubscriber {
oldBankRule,
bankRule,
}: IBankRuleEventEditedPayload) {
const payload = { ruleId: bankRule.id };
// Cannot continue if the new and old bank rule values are the same,
// after excluding `createdAt` and `updatedAt` dates.
if (
isEqual(
omit(bankRule, ['createdAt', 'updatedAt']),
omit(oldBankRule, ['createdAt', 'updatedAt'])
omit(oldBankRule, ['createdAt', 'updatedAt']),
)
) {
return;
}
// await this.agenda.now(
// 'rerecognize-uncategorized-transactions-job',
// payload
// );
const tenantPayload = await this.tenancyContect.getTenantJobPayload();
const payload = {
ruleId: bankRule.id,
...tenantPayload,
} as RecognizeUncategorizedTransactionsJobPayload;
// Re-recognize the transactions based on the new rules.
await this.recognizeTransactionsQueue.add(
RecognizeUncategorizedTransactionsJob,
payload,
);
}
/**
@@ -52,15 +84,20 @@ export class TriggerRecognizedTransactionsSubscriber {
* @param {IBankRuleEventDeletedPayload} payload -
*/
@OnEvent(events.bankRules.onDeleted)
private async recognizedTransactionsOnRuleDeleted({
async recognizedTransactionsOnRuleDeleted({
ruleId,
}: IBankRuleEventDeletedPayload) {
const payload = { ruleId };
const tenantPayload = await this.tenancyContect.getTenantJobPayload();
const payload = {
ruleId,
...tenantPayload,
} as RecognizeUncategorizedTransactionsJobPayload;
// await this.agenda.now(
// 'revert-recognized-uncategorized-transactions-job',
// payload
// );
// Re-recognize the transactions based on the new rules.
await this.recognizeTransactionsQueue.add(
RecognizeUncategorizedTransactionsJob,
payload,
);
}
/**
@@ -68,7 +105,7 @@ export class TriggerRecognizedTransactionsSubscriber {
* @param {IImportFileCommitedEventPayload} payload -
*/
@OnEvent(events.import.onImportCommitted)
private async triggerRecognizeTransactionsOnImportCommitted({
async triggerRecognizeTransactionsOnImportCommitted({
importId,
// @ts-ignore
@@ -76,10 +113,8 @@ export class TriggerRecognizedTransactionsSubscriber {
// const importFile = await Import.query().findOne({ importId });
// const batch = importFile.paramsParsed.batch;
// const payload = { transactionsCriteria: { batch } };
// // Cannot continue if the imported resource is not bank account transactions.
// if (importFile.resource !== 'UncategorizedCashflowTransaction') return;
// await this.agenda.now('recognize-uncategorized-transactions-job', payload);
}
}

View File

@@ -1,36 +1,48 @@
// import Container, { Service } from 'typedi';
// import { RecognizeTranasctionsService } from '../commands/RecognizeTranasctions.service';
import { Job } from 'bullmq';
import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Scope } from '@nestjs/common';
import { ClsService, UseCls } from 'nestjs-cls';
import { RecognizeTranasctionsService } from '../commands/RecognizeTranasctions.service';
import {
RecognizeUncategorizedTransactionsJobPayload,
RecognizeUncategorizedTransactionsQueue,
} from '../_types';
import { Process } from '@nestjs/bull';
// @Service()
// export class RegonizeTransactionsJob {
// /**
// * Constructor method.
// */
// constructor(agenda) {
// agenda.define(
// 'recognize-uncategorized-transactions-job',
// { priority: 'high', concurrency: 2 },
// this.handler
// );
// }
@Processor({
name: RecognizeUncategorizedTransactionsQueue,
scope: Scope.REQUEST,
})
export class RegonizeTransactionsPrcessor extends WorkerHost {
/**
* @param {RecognizeTranasctionsService} recognizeTranasctionsService -
* @param {ClsService} clsService -
*/
constructor(
private readonly recognizeTranasctionsService: RecognizeTranasctionsService,
private readonly clsService: ClsService,
) {
super();
}
// /**
// * Triggers sending invoice mail.
// */
// private handler = async (job, done: Function) => {
// const { tenantId, ruleId, transactionsCriteria } = job.attrs.data;
// const regonizeTransactions = Container.get(RecognizeTranasctionsService);
/**
* Triggers sending invoice mail.
*/
@Process(RecognizeUncategorizedTransactionsQueue)
@UseCls()
async process(job: Job<RecognizeUncategorizedTransactionsJobPayload>) {
const { ruleId, transactionsCriteria } = job.data;
// try {
// await regonizeTransactions.recognizeTransactions(
// tenantId,
// ruleId,
// transactionsCriteria
// );
// done();
// } catch (error) {
// console.log(error);
// done(error);
// }
// };
// }
this.clsService.set('organizationId', job.data.organizationId);
this.clsService.set('userId', job.data.userId);
try {
await this.recognizeTranasctionsService.recognizeTransactions(
ruleId,
transactionsCriteria,
);
} catch (error) {
console.log(error);
}
}
}

View File

@@ -1,46 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { castArray, first, uniq } from 'lodash';
import { GetAutofillCategorizeTransctionTransformer } from './GetAutofillCategorizeTransactionTransformer';
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class GetAutofillCategorizeTransactionService {
constructor(
private readonly transformer: TransformerInjectable,
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransactionModel: TenantModelProxy<
typeof UncategorizedBankTransaction
>,
) {}
/**
* Retrieves the autofill values of categorize transactions form.
* @param {Array<number> | number} uncategorizeTransactionsId - Uncategorized transactions ids.
*/
public async getAutofillCategorizeTransaction(
uncategorizeTransactionsId: Array<number> | number,
) {
const uncategorizeTransactionsIds = uniq(
castArray(uncategorizeTransactionsId),
);
const uncategorizedTransactions =
await this.uncategorizedBankTransactionModel()
.query()
.whereIn('id', uncategorizeTransactionsIds)
.withGraphFetched('recognizedTransaction.assignAccount')
.withGraphFetched('recognizedTransaction.bankRule')
.throwIfNotFound();
return this.transformer.transform(
{},
new GetAutofillCategorizeTransctionTransformer(),
{
uncategorizedTransactions,
firstUncategorizedTransaction: first(uncategorizedTransactions),
},
);
}
}

View File

@@ -1,176 +0,0 @@
import { sumBy } from 'lodash';
import { Transformer } from '@/modules/Transformer/Transformer';
export class GetAutofillCategorizeTransctionTransformer extends Transformer {
/**
* Included attributes to the object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'amount',
'formattedAmount',
'isRecognized',
'date',
'formattedDate',
'creditAccountId',
'debitAccountId',
'referenceNo',
'transactionType',
'recognizedByRuleId',
'recognizedByRuleName',
'isWithdrawalTransaction',
'isDepositTransaction',
];
};
/**
* Detarmines whether the transaction is recognized.
* @returns {boolean}
*/
public isRecognized() {
return !!this.options.firstUncategorizedTransaction?.recognizedTransaction;
}
/**
* Retrieves the total amount of uncategorized transactions.
* @returns {number}
*/
public amount() {
return sumBy(this.options.uncategorizedTransactions, 'amount');
}
/**
* Retrieves the formatted total amount of uncategorized transactions.
* @returns {string}
*/
public formattedAmount() {
return this.formatNumber(this.amount(), {
currencyCode: 'USD',
money: true,
});
}
/**
* Detarmines whether the transaction is deposit.
* @returns {boolean}
*/
public isDepositTransaction() {
const amount = this.amount();
return amount > 0;
}
/**
* Detarmines whether the transaction is withdrawal.
* @returns {boolean}
*/
public isWithdrawalTransaction() {
const amount = this.amount();
return amount < 0;
}
/**
*
* @param {string}
*/
public date() {
return this.options.firstUncategorizedTransaction?.date || null;
}
/**
* Retrieves the formatted date of uncategorized transaction.
* @returns {string}
*/
public formattedDate() {
return this.formatDate(this.date());
}
/**
*
* @param {string}
*/
public referenceNo() {
return this.options.firstUncategorizedTransaction?.referenceNo || null;
}
/**
*
* @returns {number}
*/
public creditAccountId() {
return (
this.options.firstUncategorizedTransaction?.recognizedTransaction
?.assignedAccountId || null
);
}
/**
*
* @returns {number}
*/
public debitAccountId() {
return this.options.firstUncategorizedTransaction?.accountId || null;
}
/**
* Retrieves the assigned category of recognized transaction, if is not recognized
* returns the default transaction type depends on the transaction normal.
* @returns {string}
*/
public transactionType() {
const assignedCategory =
this.options.firstUncategorizedTransaction?.recognizedTransaction
?.assignedCategory;
return (
assignedCategory ||
(this.isDepositTransaction() ? 'other_income' : 'other_expense')
);
}
/**
*
* @returns {string}
*/
public payee() {
return (
this.options.firstUncategorizedTransaction?.recognizedTransaction
?.assignedPayee || null
);
}
/**
*
* @returns {string}
*/
public memo() {
return (
this.options.firstUncategorizedTransaction?.recognizedTransaction
?.assignedMemo || null
);
}
/**
* Retrieves the rule id the transaction recongized by.
* @returns {string}
*/
public recognizedByRuleId() {
return (
this.options.firstUncategorizedTransaction?.recognizedTransaction
?.bankRuleId || null
);
}
/**
* Retrieves the rule name the transaction recongized by.
* @returns {string}
*/
public recognizedByRuleName() {
return (
this.options.firstUncategorizedTransaction?.recognizedTransaction
?.bankRule?.name || null
);
}
}