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.
This commit is contained in:
2026-10-04 11:54:28 +05:30
parent 16c67f6701
commit aa264097e7
5 changed files with 58 additions and 19 deletions
@@ -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);
@@ -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.
*
@@ -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<PostingPeriod> postingPeriods = calculateInterestPayable(mc, preMatureDate, transactions, isPreMatureClosure,
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
this.resetAccountTransactionsEndOfDayBalances(transactions, preMatureDate);
try {
final List<PostingPeriod> 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,
@@ -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<PostingPeriod> postingPeriods = calculateInterestPayable(mc, preMatureDate, transactions, isPreMatureClosure,
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
this.resetAccountTransactionsEndOfDayBalances(transactions, preMatureDate);
try {
final List<PostingPeriod> 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
@@ -437,14 +437,38 @@ public class SavingsAccountInterestPostingServiceImpl implements SavingsAccountI
return false;
}
protected Collection<Long> 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<SavingsAccountTransactionData> excludeInterestTransferWithdrawals(
final List<SavingsAccountTransactionData> transactions, final SavingsAccountData savingsAccountData) {
final Collection<Long> interestTransferWithdrawalIds = interestTransferWithdrawalTransactionIds(savingsAccountData);
if (interestTransferWithdrawalIds.isEmpty()) {
return transactions;
}
return transactions.stream().filter(transaction -> !interestTransferWithdrawalIds.contains(transaction.getId()))
.collect(Collectors.toList());
}
private List<SavingsAccountTransactionData> retreiveOrderedNonInterestPostingTransactions(final SavingsAccountData savingsAccountData) {
final List<SavingsAccountTransactionData> listOfTransactionsSorted = retrieveListOfTransactions(savingsAccountData);
final Collection<Long> interestTransferWithdrawalIds = interestTransferWithdrawalTransactionIds(savingsAccountData);
final List<SavingsAccountTransactionData> 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<SavingsAccountTransactionData> accountTransactionsSorted,
final LocalDate interestPostingUpToDate, final SavingsAccountData savingsAccountData) {
final List<SavingsAccountTransactionData> 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()),