From 37f0f4e227d5d5d8eba0079826329dccaf1eee01 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 22 Dec 2025 23:02:08 +0200 Subject: [PATCH] fix: match uncategorized bank transactions --- .../src/modules/BankingMatching/_utils.ts | 18 ++++++---- .../commands/MatchTransactions.ts | 7 ++-- .../commands/MatchTransactionsTypes.ts | 35 +++++++++++-------- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/packages/server/src/modules/BankingMatching/_utils.ts b/packages/server/src/modules/BankingMatching/_utils.ts index 6d269911e..914a725dc 100644 --- a/packages/server/src/modules/BankingMatching/_utils.ts +++ b/packages/server/src/modules/BankingMatching/_utils.ts @@ -1,6 +1,6 @@ import * as moment from 'moment'; import * as R from 'ramda'; -import { isEmpty, sumBy } from 'lodash'; +import { isEmpty, round, sumBy } from 'lodash'; import { ERRORS, MatchedTransactionPOJO } from './types'; import { ServiceError } from '../Items/ServiceError'; @@ -22,18 +22,24 @@ export const sortClosestMatchTransactions = ( }; export const sumMatchTranasctions = (transactions: Array) => { - return transactions.reduce( - (total, item) => - total + - (item.transactionNormal === 'debit' ? 1 : -1) * parseFloat(item.amount), + const total = transactions.reduce( + (sum, item) => { + const amount = parseFloat(item.amount) || 0; + const multiplier = item.transactionNormal === 'debit' ? 1 : -1; + return sum + multiplier * amount; + }, 0 ); + // Round to 2 decimal places to avoid floating-point precision issues + return round(total, 2); }; export const sumUncategorizedTransactions = ( uncategorizedTransactions: Array ) => { - return sumBy(uncategorizedTransactions, 'amount'); + const total = sumBy(uncategorizedTransactions, 'amount'); + // Round to 2 decimal places to avoid floating-point precision issues + return round(total, 2); }; export const validateUncategorizedTransactionsNotMatched = ( diff --git a/packages/server/src/modules/BankingMatching/commands/MatchTransactions.ts b/packages/server/src/modules/BankingMatching/commands/MatchTransactions.ts index 384cfacf8..7c4e9664a 100644 --- a/packages/server/src/modules/BankingMatching/commands/MatchTransactions.ts +++ b/packages/server/src/modules/BankingMatching/commands/MatchTransactions.ts @@ -34,7 +34,7 @@ export class MatchBankTransactions { private readonly uncategorizedBankTransactionModel: TenantModelProxy< typeof UncategorizedBankTransaction >, - ) {} + ) { } /** * Validates the match bank transactions DTO. @@ -100,7 +100,10 @@ export class MatchBankTransactions { ); // Validates the total given matching transcations whether is not equal // uncategorized transaction amount. - if (totalUncategorizedTransactions !== totalMatchedTranasctions) { + // Use tolerance-based comparison to handle floating-point precision issues + const tolerance = 0.01; // Allow 0.01 difference for floating-point precision + const difference = Math.abs(totalUncategorizedTransactions - totalMatchedTranasctions); + if (difference > tolerance) { throw new ServiceError(ERRORS.TOTAL_MATCHING_TRANSACTIONS_INVALID); } } diff --git a/packages/server/src/modules/BankingMatching/commands/MatchTransactionsTypes.ts b/packages/server/src/modules/BankingMatching/commands/MatchTransactionsTypes.ts index dd81e046b..f800bf9fe 100644 --- a/packages/server/src/modules/BankingMatching/commands/MatchTransactionsTypes.ts +++ b/packages/server/src/modules/BankingMatching/commands/MatchTransactionsTypes.ts @@ -12,24 +12,30 @@ export class MatchTransactionsTypes { private static registry: MatchTransactionsTypesRegistry; /** - * Consttuctor method. + * Constructor method. */ - constructor() { + constructor( + private readonly getMatchedInvoicesService: GetMatchedTransactionsByInvoices, + private readonly getMatchedBillsService: GetMatchedTransactionsByBills, + private readonly getMatchedExpensesService: GetMatchedTransactionsByExpenses, + private readonly getMatchedManualJournalsService: GetMatchedTransactionsByManualJournals, + private readonly getMatchedCashflowService: GetMatchedTransactionsByCashflow, + ) { this.boot(); } get registered() { return [ - { type: 'SaleInvoice', service: GetMatchedTransactionsByInvoices }, - { type: 'Bill', service: GetMatchedTransactionsByBills }, - { type: 'Expense', service: GetMatchedTransactionsByExpenses }, + { type: 'SaleInvoice', service: this.getMatchedInvoicesService }, + { type: 'Bill', service: this.getMatchedBillsService }, + { type: 'Expense', service: this.getMatchedExpensesService }, { type: 'ManualJournal', - service: GetMatchedTransactionsByManualJournals, + service: this.getMatchedManualJournalsService, }, { type: 'CashflowTransaction', - service: GetMatchedTransactionsByCashflow, + service: this.getMatchedCashflowService, }, ]; } @@ -50,14 +56,13 @@ export class MatchTransactionsTypes { * Boots all the registered importables. */ public boot() { - if (!MatchTransactionsTypes.registry) { - const instance = MatchTransactionsTypesRegistry.getInstance(); + const instance = MatchTransactionsTypesRegistry.getInstance(); - this.registered.forEach((registered) => { - // const serviceInstanace = Container.get(registered.service); - // instance.register(registered.type, serviceInstanace); - }); - MatchTransactionsTypes.registry = instance; - } + // Always register services to ensure they're available + this.registered.forEach((registered) => { + instance.register(registered.type, registered.service); + }); + + MatchTransactionsTypes.registry = instance; } }