feat: bulk categorizing bank transactions

This commit is contained in:
Ahmed Bouhuolia
2024-07-18 17:00:23 +02:00
parent 51471ed000
commit 449390143d
18 changed files with 335 additions and 126 deletions

View File

@@ -164,12 +164,12 @@ export class CashflowApplication {
*/
public categorizeTransaction(
tenantId: number,
cashflowTransactionId: number,
uncategorizeTransactionIds: Array<number>,
categorizeDTO: ICategorizeCashflowTransactioDTO
) {
return this.categorizeTransactionService.categorize(
tenantId,
cashflowTransactionId,
uncategorizeTransactionIds,
categorizeDTO
);
}

View File

@@ -1,4 +1,6 @@
import { Inject, Service } from 'typedi';
import { castArray } from 'lodash';
import { Knex } from 'knex';
import HasTenancyService from '../Tenancy/TenancyService';
import events from '@/subscribers/events';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
@@ -8,12 +10,12 @@ import {
ICashflowTransactionUncategorizingPayload,
ICategorizeCashflowTransactioDTO,
} from '@/interfaces';
import { Knex } from 'knex';
import { transformCategorizeTransToCashflow } from './utils';
import {
transformCategorizeTransToCashflow,
validateUncategorizedTransactionsNotExcluded,
} from './utils';
import { CommandCashflowValidator } from './CommandCasflowValidator';
import NewCashflowTransactionService from './NewCashflowTransactionService';
import { ServiceError } from '@/exceptions';
import { ERRORS } from './constants';
@Service()
export class CategorizeCashflowTransaction {
@@ -39,29 +41,31 @@ export class CategorizeCashflowTransaction {
*/
public async categorize(
tenantId: number,
uncategorizedTransactionId: number,
uncategorizedTransactionId: number | Array<number>,
categorizeDTO: ICategorizeCashflowTransactioDTO
) {
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
const uncategorizedTransactionIds = castArray(uncategorizedTransactionId);
// Retrieves the uncategorized transaction or throw an error.
const transaction = await UncategorizedCashflowTransaction.query()
.findById(uncategorizedTransactionId)
.throwIfNotFound();
const oldUncategorizedTransactions =
await UncategorizedCashflowTransaction.query()
.whereIn('id', uncategorizedTransactionIds)
.throwIfNotFound();
// Validate cannot categorize excluded transaction.
if (transaction.excluded) {
throw new ServiceError(ERRORS.CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION);
}
// Validates the transaction shouldn't be categorized before.
this.commandValidators.validateTransactionShouldNotCategorized(transaction);
validateUncategorizedTransactionsNotExcluded(oldUncategorizedTransactions);
// Validates the transaction shouldn't be categorized before.
this.commandValidators.validateTransactionsShouldNotCategorized(
oldIncategorizedTransactions
);
// Validate the uncateogirzed transaction if it's deposit the transaction direction
// should `IN` and the same thing if it's withdrawal the direction should be OUT.
this.commandValidators.validateUncategorizeTransactionType(
transaction,
categorizeDTO.transactionType
);
// this.commandValidators.validateUncategorizeTransactionType(
// uncategorizedTransactions,
// categorizeDTO.transactionType
// );
// Edits the cashflow transaction under UOW env.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onTransactionCategorizing` event.
@@ -69,12 +73,13 @@ export class CategorizeCashflowTransaction {
events.cashflow.onTransactionCategorizing,
{
tenantId,
oldUncategorizedTransactions,
trx,
} as ICashflowTransactionUncategorizingPayload
);
// Transformes the categorize DTO to the cashflow transaction.
const cashflowTransactionDTO = transformCategorizeTransToCashflow(
transaction,
oldUncategorizedTransactions,
categorizeDTO
);
// Creates a new cashflow transaction.
@@ -84,22 +89,28 @@ export class CategorizeCashflowTransaction {
cashflowTransactionDTO
);
// Updates the uncategorized transaction as categorized.
const uncategorizedTransaction =
await UncategorizedCashflowTransaction.query(trx).patchAndFetchById(
uncategorizedTransactionId,
{
categorized: true,
categorizeRefType: 'CashflowTransaction',
categorizeRefId: cashflowTransaction.id,
}
await UncategorizedCashflowTransaction.query(trx)
.whereIn('id', uncategorizedTransactionIds)
.patch({
categorized: true,
categorizeRefType: 'CashflowTransaction',
categorizeRefId: cashflowTransaction.id,
});
// Fetch the new updated uncategorized transactions.
const uncategorizedTransactions =
await UncategorizedCashflowTransaction.query(trx).whereIn(
'id',
uncategorizedTransactionIds
);
// Triggers `onCashflowTransactionCategorized` event.
await this.eventPublisher.emitAsync(
events.cashflow.onTransactionCategorized,
{
tenantId,
cashflowTransaction,
uncategorizedTransaction,
uncategorizedTransactions,
oldUncategorizedTransactions,
categorizeDTO,
trx,
} as ICashflowTransactionCategorizedPayload

View File

@@ -68,10 +68,12 @@ export class CommandCashflowValidator {
* Validate the given transcation shouldn't be categorized.
* @param {CashflowTransaction} cashflowTransaction
*/
public validateTransactionShouldNotCategorized(
cashflowTransaction: CashflowTransaction
public validateTransactionsShouldNotCategorized(
cashflowTransactions: Array<IUncategorizedCashflowTransaction>
) {
if (cashflowTransaction.uncategorize) {
const categorized = cashflowTransactions.filter((t) => t.categorized);
if (categorized?.length > 0) {
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_CATEGORIZED);
}
}
@@ -87,7 +89,7 @@ export class CommandCashflowValidator {
transactionType: string
) {
const type = getCashflowTransactionType(
upperFirst(camelCase(transactionType)) as CASHFLOW_TRANSACTION_TYPE
transactionType as CASHFLOW_TRANSACTION_TYPE
);
if (
(type.direction === CASHFLOW_DIRECTION.IN &&

View File

@@ -1,7 +1,9 @@
import { upperFirst, camelCase } from 'lodash';
import { upperFirst, camelCase, first, sum, sumBy } from 'lodash';
import {
CASHFLOW_DIRECTION,
CASHFLOW_TRANSACTION_TYPE,
CASHFLOW_TRANSACTION_TYPE_META,
ERRORS,
ICashflowTransactionTypeMeta,
} from './constants';
import {
@@ -9,6 +11,8 @@ import {
ICategorizeCashflowTransactioDTO,
IUncategorizedCashflowTransaction,
} from '@/interfaces';
import { UncategorizeCashflowTransaction } from './UncategorizeCashflowTransaction';
import { ServiceError } from '@/exceptions';
/**
* Ensures the given transaction type to transformed to appropriate format.
@@ -27,7 +31,9 @@ export const transformCashflowTransactionType = (type) => {
export function getCashflowTransactionType(
transactionType: CASHFLOW_TRANSACTION_TYPE
): ICashflowTransactionTypeMeta {
return CASHFLOW_TRANSACTION_TYPE_META[transactionType];
const _transactionType = transformCashflowTransactionType(transactionType);
return CASHFLOW_TRANSACTION_TYPE_META[_transactionType];
}
/**
@@ -46,22 +52,35 @@ export const getCashflowAccountTransactionsTypes = () => {
* @returns {ICashflowNewCommandDTO}
*/
export const transformCategorizeTransToCashflow = (
uncategorizeModel: IUncategorizedCashflowTransaction,
uncategorizeTransactions: Array<IUncategorizedCashflowTransaction>,
categorizeDTO: ICategorizeCashflowTransactioDTO
): ICashflowNewCommandDTO => {
const uncategorizeTransaction = first(uncategorizeTransactions);
const amount = sumBy(uncategorizeTransactions, 'amount');
const amountAbs = Math.abs(amount);
return {
date: uncategorizeModel.date,
referenceNo: categorizeDTO.referenceNo || uncategorizeModel.referenceNo,
description: categorizeDTO.description || uncategorizeModel.description,
cashflowAccountId: uncategorizeModel.accountId,
date: categorizeDTO.date,
referenceNo: categorizeDTO.referenceNo,
description: categorizeDTO.description,
cashflowAccountId: uncategorizeTransaction.accountId,
creditAccountId: categorizeDTO.creditAccountId,
exchangeRate: categorizeDTO.exchangeRate || 1,
currencyCode: uncategorizeModel.currencyCode,
amount: uncategorizeModel.amount,
currencyCode: categorizeDTO.currencyCode,
amount: amountAbs,
transactionNumber: categorizeDTO.transactionNumber,
transactionType: categorizeDTO.transactionType,
uncategorizedTransactionId: uncategorizeModel.id,
branchId: categorizeDTO?.branchId,
publish: true,
};
};
export const validateUncategorizedTransactionsNotExcluded = (
transactions: Array<UncategorizeCashflowTransaction>
) => {
const excluded = transactions.filter((tran) => tran.excluded);
if (excluded?.length > 0) {
throw new ServiceError(ERRORS.CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION);
}
};