diff --git a/packages/server/src/api/controllers/Cashflow/GetCashflowTransaction.ts b/packages/server/src/api/controllers/Cashflow/GetCashflowTransaction.ts index 6b0be76e2..393cdfcf9 100644 --- a/packages/server/src/api/controllers/Cashflow/GetCashflowTransaction.ts +++ b/packages/server/src/api/controllers/Cashflow/GetCashflowTransaction.ts @@ -1,6 +1,6 @@ import { Service, Inject } from 'typedi'; import { Router, Request, Response, NextFunction } from 'express'; -import { param } from 'express-validator'; +import { param, query } from 'express-validator'; import BaseController from '../BaseController'; import { ServiceError } from '@/exceptions'; import CheckPolicies from '@/api/middleware/CheckPolicies'; @@ -24,7 +24,12 @@ export default class GetCashflowAccounts extends BaseController { const router = Router(); router.get( - '/transactions/:transactionId/matches', + '/transactions/matches', + [ + query('uncategorizeTransactionsIds').exists().isArray({ min: 1 }), + query('uncategorizeTransactionsIds.*').exists().isNumeric().toInt(), + ], + this.validationResult, this.getMatchedTransactions.bind(this) ); router.get( @@ -76,14 +81,15 @@ export default class GetCashflowAccounts extends BaseController { next: NextFunction ) { const { tenantId } = req; - const { transactionId } = req.params; + const uncategorizeTransactionsIds: Array = + req.query.uncategorizeTransactionsIds; const filter = this.matchedQueryData(req) as GetMatchedTransactionsFilter; try { const data = await this.bankTransactionsMatchingApp.getMatchedTransactions( tenantId, - transactionId, + uncategorizeTransactionsIds, filter ); return res.status(200).send(data); diff --git a/packages/server/src/services/Banking/Matching/GetMatchedTransactions.ts b/packages/server/src/services/Banking/Matching/GetMatchedTransactions.ts index f0cfdcaed..3b5f1339a 100644 --- a/packages/server/src/services/Banking/Matching/GetMatchedTransactions.ts +++ b/packages/server/src/services/Banking/Matching/GetMatchedTransactions.ts @@ -1,6 +1,7 @@ import { Inject, Service } from 'typedi'; import * as R from 'ramda'; import moment from 'moment'; +import { first, sumBy } from 'lodash'; import { PromisePool } from '@supercharge/promise-pool'; import { GetMatchedTransactionsFilter, MatchedTransactionsPOJO } from './types'; import { GetMatchedTransactionsByExpenses } from './GetMatchedTransactionsByExpenses'; @@ -47,19 +48,20 @@ export class GetMatchedTransactions { /** * Retrieves the matched transactions. * @param {number} tenantId - + * @param {Array} uncategorizedTransactionIds - Uncategorized transactions ids. * @param {GetMatchedTransactionsFilter} filter - * @returns {Promise} */ public async getMatchedTransactions( tenantId: number, - uncategorizedTransactionId: number, + uncategorizedTransactionIds: Array, filter: GetMatchedTransactionsFilter ): Promise { const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId); - const uncategorizedTransaction = + const uncategorizedTransactions = await UncategorizedCashflowTransaction.query() - .findById(uncategorizedTransactionId) + .whereIn('id', uncategorizedTransactionIds) .throwIfNotFound(); const filtered = filter.transactionType @@ -71,9 +73,8 @@ export class GetMatchedTransactions { .process(async ({ type, service }) => { return service.getMatchedTransactions(tenantId, filter); }); - const { perfectMatches, possibleMatches } = this.groupMatchedResults( - uncategorizedTransaction, + uncategorizedTransactions, matchedTransactions ); return { @@ -90,20 +91,20 @@ export class GetMatchedTransactions { * @returns {MatchedTransactionsPOJO} */ private groupMatchedResults( - uncategorizedTransaction, + uncategorizedTransactions: Array, matchedTransactions ): MatchedTransactionsPOJO { const results = R.compose(R.flatten)(matchedTransactions?.results); + const firstUncategorized = first(uncategorizedTransactions); + const amount = sumBy(uncategorizedTransactions, 'amount'); + const date = firstUncategorized.date; + // Sort the results based on amount, date, and transaction type - const closestResullts = sortClosestMatchTransactions( - uncategorizedTransaction, - results - ); + const closestResullts = sortClosestMatchTransactions(amount, date, results); const perfectMatches = R.filter( (match) => - match.amount === uncategorizedTransaction.amount && - moment(match.date).isSame(uncategorizedTransaction.date, 'day'), + match.amount === amount && moment(match.date).isSame(date, 'day'), closestResullts ); const possibleMatches = R.difference(closestResullts, perfectMatches); diff --git a/packages/server/src/services/Banking/Matching/MatchBankTransactionsApplication.ts b/packages/server/src/services/Banking/Matching/MatchBankTransactionsApplication.ts index 39702c4bb..dd60c166c 100644 --- a/packages/server/src/services/Banking/Matching/MatchBankTransactionsApplication.ts +++ b/packages/server/src/services/Banking/Matching/MatchBankTransactionsApplication.ts @@ -23,12 +23,12 @@ export class MatchBankTransactionsApplication { */ public getMatchedTransactions( tenantId: number, - uncategorizedTransactionId: number, + uncategorizedTransactionsIds: Array, filter: GetMatchedTransactionsFilter ) { return this.getMatchedTransactionsService.getMatchedTransactions( tenantId, - uncategorizedTransactionId, + uncategorizedTransactionsIds, filter ); } diff --git a/packages/server/src/services/Banking/Matching/MatchTransactions.ts b/packages/server/src/services/Banking/Matching/MatchTransactions.ts index 9ec128831..af0d41f60 100644 --- a/packages/server/src/services/Banking/Matching/MatchTransactions.ts +++ b/packages/server/src/services/Banking/Matching/MatchTransactions.ts @@ -16,6 +16,7 @@ import { MatchTransactionsTypes } from './MatchTransactionsTypes'; import { ServiceError } from '@/exceptions'; import { sumMatchTranasctions, + sumUncategorizedTransactions, validateUncategorizedTransactionsExcluded, validateUncategorizedTransactionsNotMatched, } from './_utils'; @@ -95,11 +96,14 @@ export class MatchBankTransactions { const totalMatchedTranasctions = sumMatchTranasctions( validatationResult.results ); + const totalUncategorizedTransactions = sumUncategorizedTransactions( + uncategorizedTransactions + ); // Validates the total given matching transcations whether is not equal // uncategorized transaction amount. - // if (totalMatchedTranasctions !== uncategorizedTransaction.amount) { - // throw new ServiceError(ERRORS.TOTAL_MATCHING_TRANSACTIONS_INVALID); - // } + if (totalUncategorizedTransactions === totalMatchedTranasctions) { + throw new ServiceError(ERRORS.TOTAL_MATCHING_TRANSACTIONS_INVALID); + } } /** diff --git a/packages/server/src/services/Banking/Matching/_utils.ts b/packages/server/src/services/Banking/Matching/_utils.ts index b78045904..46a31729c 100644 --- a/packages/server/src/services/Banking/Matching/_utils.ts +++ b/packages/server/src/services/Banking/Matching/_utils.ts @@ -2,23 +2,22 @@ import moment from 'moment'; import * as R from 'ramda'; import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTransaction'; import { ERRORS, MatchedTransactionPOJO } from './types'; -import { isEmpty } from 'lodash'; +import { isEmpty, sumBy } from 'lodash'; import { ServiceError } from '@/exceptions'; export const sortClosestMatchTransactions = ( - uncategorizedTransaction: UncategorizedCashflowTransaction, + amount: number, + date: Date, matches: MatchedTransactionPOJO[] ) => { return R.sortWith([ // Sort by amount difference (closest to uncategorized transaction amount first) R.ascend((match: MatchedTransactionPOJO) => - Math.abs(match.amount - uncategorizedTransaction.amount) + Math.abs(match.amount - amount) ), // Sort by date difference (closest to uncategorized transaction date first) R.ascend((match: MatchedTransactionPOJO) => - Math.abs( - moment(match.date).diff(moment(uncategorizedTransaction.date), 'days') - ) + Math.abs(moment(match.date).diff(moment(date), 'days')) ), ])(matches); }; @@ -32,15 +31,23 @@ export const sumMatchTranasctions = (transactions: Array) => { ); }; +export const sumUncategorizedTransactions = ( + uncategorizedTransactions: Array +) => { + return sumBy(uncategorizedTransactions, 'amount'); +}; + export const validateUncategorizedTransactionsNotMatched = ( uncategorizedTransactions: any ) => { - const isMatchedTransactions = uncategorizedTransactions.filter( + const matchedTransactions = uncategorizedTransactions.filter( (trans) => !isEmpty(trans.matchedBankTransactions) ); // - if (isMatchedTransactions.length > 0) { - throw new ServiceError(ERRORS.TRANSACTION_ALREADY_MATCHED); + if (matchedTransactions.length > 0) { + throw new ServiceError(ERRORS.TRANSACTION_ALREADY_MATCHED, '', { + matchedTransactionsIds: matchedTransactions?.map((m) => m.id), + }); } }; @@ -51,6 +58,8 @@ export const validateUncategorizedTransactionsExcluded = ( (trans) => trans.excluded ); if (excludedTransactions.length > 0) { - throw new ServiceError(ERRORS.CANNOT_MATCH_EXCLUDED_TRANSACTION); + throw new ServiceError(ERRORS.CANNOT_MATCH_EXCLUDED_TRANSACTION, '', { + excludedTransactionsIds: excludedTransactions.map((e) => e.id), + }); } };