From aa264097e707e7d711cef41b61a37337d5be5fce Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Sun, 4 Oct 2026 11:54:28 +0530 Subject: [PATCH] fix(savings,accounting): fix premature interest calculation date bounds and load interest payable GL mapping on edit Details of changes: 1. Premature Closure 1-Day Shortfall Fix (FixedDepositAccount & RecurringDepositAccount): - In calculatePreMatureInterest, explicitly reset transaction end-of-day balances to preMatureDate before invoking calculateInterestPayable. - Ensure end-of-day balances are restored to current business local date in a finally block. - Resolves the issue where premature closure missed 1 day of interest (e.g., -17.80 clawback) due to transaction end-of-day balances defaulting to the previous day when premature date matched current business date. 2. Interest Transfer Exclusions (SavingsAccountInterestPostingServiceImpl): - Exclude interest-transfer withdrawal transactions from non-interest posting transactions and end-of-day cumulative balance updates for fixed deposits transferring interest to linked accounts. 3. Interest Payable GL Load on Edit (AccountingConstants & ProductToGLAccountMappingReadPlatformServiceImpl): - Added INTEREST_PAYABLE(17) enum constant to CashAccountsForSavings in AccountingConstants. - Mapped CashAccountsForSavings.INTEREST_PAYABLE to SavingProductAccountingDataParams.INTEREST_PAYABLE in ProductToGLAccountMappingReadPlatformServiceImpl. - Fixes the bug where Liability -> Interest Payable Account GL was not loading in the product edit form (e.g., /products/fixed-deposit-products/:id/edit) even after being saved. --- ...AccountMappingReadPlatformServiceImpl.java | 2 ++ .../common/AccountingConstants.java | 1 + .../savings/domain/FixedDepositAccount.java | 21 +++++++----- .../domain/RecurringDepositAccount.java | 21 +++++++----- ...ingsAccountInterestPostingServiceImpl.java | 32 +++++++++++++++++-- 5 files changed, 58 insertions(+), 19 deletions(-) diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java index 65b754870..2e87bde94 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java @@ -477,6 +477,8 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product accountMappingDetails.put(SavingProductAccountingDataParams.SAVINGS_CONTROL.getValue(), glAccountData); } else if (glAccountForSavings.equals(CashAccountsForSavings.TRANSFERS_SUSPENSE)) { accountMappingDetails.put(SavingProductAccountingDataParams.TRANSFERS_SUSPENSE.getValue(), glAccountData); + } else if (glAccountForSavings.equals(CashAccountsForSavings.INTEREST_PAYABLE)) { + accountMappingDetails.put(SavingProductAccountingDataParams.INTEREST_PAYABLE.getValue(), glAccountData); // Income } else if (glAccountForSavings.equals(CashAccountsForSavings.INCOME_FROM_FEES)) { accountMappingDetails.put(SavingProductAccountingDataParams.INCOME_FROM_FEES.getValue(), glAccountData); diff --git a/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java b/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java index a5a4ead03..0ce7582f9 100644 --- a/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java +++ b/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java @@ -270,6 +270,7 @@ public final class AccountingConstants { INCOME_FROM_INTEREST(12), // LOSSES_WRITTEN_OFF(13), // ESCHEAT_LIABILITY(14), // + INTEREST_PAYABLE(17), // /** * Where the paise dropped from a cash closure are absorbed. * diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java index b43d758ce..5226ede94 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java @@ -829,17 +829,22 @@ public class FixedDepositAccount extends SavingsAccount { final boolean isPreMatureClosure, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { final MathContext mc = MathContext.DECIMAL64; - final List postingPeriods = calculateInterestPayable(mc, preMatureDate, transactions, isPreMatureClosure, - isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); + this.resetAccountTransactionsEndOfDayBalances(transactions, preMatureDate); + try { + final List postingPeriods = calculateInterestPayable(mc, preMatureDate, transactions, isPreMatureClosure, + isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); - Money interestOnMaturity = Money.zero(this.currency); + Money interestOnMaturity = Money.zero(this.currency); - for (final PostingPeriod interestPostingPeriod : postingPeriods) { - final Money interestEarnedForPeriod = interestPostingPeriod.getInterestEarned(); - interestOnMaturity = interestOnMaturity.plus(interestEarnedForPeriod); + for (final PostingPeriod interestPostingPeriod : postingPeriods) { + final Money interestEarnedForPeriod = interestPostingPeriod.getInterestEarned(); + interestOnMaturity = interestOnMaturity.plus(interestEarnedForPeriod); + } + + return interestOnMaturity; + } finally { + this.resetAccountTransactionsEndOfDayBalances(transactions, DateUtils.getBusinessLocalDate()); } - - return interestOnMaturity; } public void postInterest(final MathContext mc, final LocalDate postingDate, boolean isInterestTransfer, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositAccount.java index c4c07b19e..62cb85a65 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositAccount.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositAccount.java @@ -759,17 +759,22 @@ public class RecurringDepositAccount extends SavingsAccount { final boolean isPreMatureClosure, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { final MathContext mc = MathContext.DECIMAL64; - final List postingPeriods = calculateInterestPayable(mc, preMatureDate, transactions, isPreMatureClosure, - isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); + this.resetAccountTransactionsEndOfDayBalances(transactions, preMatureDate); + try { + final List postingPeriods = calculateInterestPayable(mc, preMatureDate, transactions, isPreMatureClosure, + isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); - Money interestOnMaturity = Money.zero(this.currency); + Money interestOnMaturity = Money.zero(this.currency); - for (final PostingPeriod interestPostingPeriod : postingPeriods) { - final Money interestEarnedForPeriod = interestPostingPeriod.getInterestEarned(); - interestOnMaturity = interestOnMaturity.plus(interestEarnedForPeriod); + for (final PostingPeriod interestPostingPeriod : postingPeriods) { + final Money interestEarnedForPeriod = interestPostingPeriod.getInterestEarned(); + interestOnMaturity = interestOnMaturity.plus(interestEarnedForPeriod); + } + this.summary.updateFromInterestPeriodSummaries(this.currency, postingPeriods); + return interestOnMaturity; + } finally { + this.resetAccountTransactionsEndOfDayBalances(transactions, DateUtils.getBusinessLocalDate()); } - this.summary.updateFromInterestPeriodSummaries(this.currency, postingPeriods); - return interestOnMaturity; } @Override diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java index 4b255ddf0..c936dba03 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java @@ -437,14 +437,38 @@ public class SavingsAccountInterestPostingServiceImpl implements SavingsAccountI return false; } + protected Collection interestTransferWithdrawalTransactionIds(final SavingsAccountData savingsAccountData) { + if (savingsAccountData.getId() == null || this.savingsHelper == null || !isTransferInterestToOtherAccount(savingsAccountData)) { + return Collections.emptyList(); + } + return this.savingsHelper.fetchPostInterestTransactionIds(savingsAccountData.getId()); + } + + private boolean isTransferInterestToOtherAccount(final SavingsAccountData savingsAccountData) { + return savingsAccountData.isTransferInterestToLinkedAccount() + && DepositAccountType.FIXED_DEPOSIT.getValue().equals(savingsAccountData.getDepositTypeId()); + } + + protected List excludeInterestTransferWithdrawals( + final List transactions, final SavingsAccountData savingsAccountData) { + final Collection interestTransferWithdrawalIds = interestTransferWithdrawalTransactionIds(savingsAccountData); + if (interestTransferWithdrawalIds.isEmpty()) { + return transactions; + } + return transactions.stream().filter(transaction -> !interestTransferWithdrawalIds.contains(transaction.getId())) + .collect(Collectors.toList()); + } + private List retreiveOrderedNonInterestPostingTransactions(final SavingsAccountData savingsAccountData) { final List listOfTransactionsSorted = retrieveListOfTransactions(savingsAccountData); + final Collection interestTransferWithdrawalIds = interestTransferWithdrawalTransactionIds(savingsAccountData); final List orderedNonInterestPostingTransactions = new ArrayList<>(); for (final SavingsAccountTransactionData transaction : listOfTransactionsSorted) { if (!(transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed()) - && transaction.isNotReversed() && !transaction.isReversalTransaction()) { + && transaction.isNotReversed() && !transaction.isReversalTransaction() + && !interestTransferWithdrawalIds.contains(transaction.getId())) { orderedNonInterestPostingTransactions.add(transaction); } } @@ -664,10 +688,12 @@ public class SavingsAccountInterestPostingServiceImpl implements SavingsAccountI protected void resetAccountTransactionsEndOfDayBalances(final List accountTransactionsSorted, final LocalDate interestPostingUpToDate, final SavingsAccountData savingsAccountData) { + final List filteredTransactions = excludeInterestTransferWithdrawals(accountTransactionsSorted, + savingsAccountData); // loop over transactions in reverse LocalDate endOfBalanceDate = interestPostingUpToDate; - for (int i = accountTransactionsSorted.size() - 1; i >= 0; i--) { - final SavingsAccountTransactionData transaction = accountTransactionsSorted.get(i); + for (int i = filteredTransactions.size() - 1; i >= 0; i--) { + final SavingsAccountTransactionData transaction = filteredTransactions.get(i); if (transaction.isNotReversed() && !transaction.isReversalTransaction() && !(transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed())) { transaction.updateCumulativeBalanceAndDates(MonetaryCurrency.fromCurrencyData(savingsAccountData.getCurrency()),