FINERACT-2312: Post interest with adjustments for savings accounts

This commit is contained in:
Juan-Pablo-Alvarez
2025-09-04 10:24:42 +02:00
committed by Adam Saghy
parent ef1e2e1095
commit a6fa1adc99
11 changed files with 725 additions and 60 deletions
@@ -32,6 +32,7 @@ import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.domain.LocalDateInterval;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.savings.DepositAccountType;
@@ -81,11 +82,17 @@ public class SavingsAccountInterestPostingServiceImpl implements SavingsAccountI
for (final PostingPeriod interestPostingPeriod : postingPeriods) {
final LocalDate interestPostingTransactionDate = interestPostingPeriod.dateOfPostingTransaction();
final Money interestEarnedToBePostedForPeriod = interestPostingPeriod.getInterestEarned();
final Boolean isOverdraft = interestPostingPeriod.isOverdraftInterest();
if (!DateUtils.isAfter(interestPostingTransactionDate, interestPostingUpToDate)) {
interestPostedToDate = interestPostedToDate.plus(interestEarnedToBePostedForPeriod);
final SavingsAccountTransactionData postingTransaction = findInterestPostingTransactionFor(interestPostingTransactionDate,
savingsAccountData);
SavingsAccountTransactionData postingTransaction = null;
if (this.depositAccountType(savingsAccountData).isSavingsDeposit() && savingsAccountData.isAllowOverdraft()) {
postingTransaction = findInterestPostingTransactionForInterest(interestPostingTransactionDate, savingsAccountData,
isOverdraft);
} else {
postingTransaction = findInterestPostingTransactionFor(interestPostingTransactionDate, savingsAccountData);
}
if (postingTransaction == null) {
SavingsAccountTransactionData newPostingTransaction;
@@ -95,7 +102,7 @@ public class SavingsAccountInterestPostingServiceImpl implements SavingsAccountI
} else {
newPostingTransaction = SavingsAccountTransactionData.overdraftInterest(savingsAccountData,
interestPostingTransactionDate, interestEarnedToBePostedForPeriod.negated(),
interestPostingPeriod.isUserPosting());
interestPostingPeriod.isUserPosting(), isOverdraft);
}
savingsAccountData.updateTransactions(newPostingTransaction);
@@ -131,7 +138,7 @@ public class SavingsAccountInterestPostingServiceImpl implements SavingsAccountI
} else {
newPostingTransaction = SavingsAccountTransactionData.overdraftInterest(savingsAccountData,
interestPostingTransactionDate, interestEarnedToBePostedForPeriod.negated(),
interestPostingPeriod.isUserPosting());
interestPostingPeriod.isUserPosting(), isOverdraft);
}
savingsAccountData.updateTransactions(newPostingTransaction);
@@ -190,6 +197,36 @@ public class SavingsAccountInterestPostingServiceImpl implements SavingsAccountI
return transaction;
}
private Money appendPostingPeriodIfAny(final LocalDateInterval periodInterval, Money periodStartingBalance,
final List<SavingsAccountTransactionData> txs, final MonetaryCurrency monetaryCurrency,
final SavingsCompoundingInterestPeriodType compoundingPeriodType, final SavingsInterestCalculationType interestCalculationType,
final BigDecimal interestRateAsFraction, final int daysInYear, final LocalDate upToInterestCalculationDate,
final Collection<Long> interestPostTransactions, final boolean isInterestTransfer, final Money minBalanceForInterestCalculation,
final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final BigDecimal overdraftInterestRateAsFraction,
final Money minOverdraftForInterestCalculation, final boolean isUserPosting, final Integer financialYearBeginningMonth,
final boolean allowOverdraft, final List<PostingPeriod> allPostingPeriods, Boolean isOverdraftTransacction) {
if (txs == null || txs.isEmpty()) {
return periodStartingBalance;
}
final PostingPeriod postingPeriod = PostingPeriod.createFromDTO(periodInterval, periodStartingBalance, txs, monetaryCurrency,
compoundingPeriodType, interestCalculationType, interestRateAsFraction, daysInYear, upToInterestCalculationDate,
interestPostTransactions, isInterestTransfer, minBalanceForInterestCalculation, isSavingsInterestPostingAtCurrentPeriodEnd,
overdraftInterestRateAsFraction, minOverdraftForInterestCalculation, isUserPosting, financialYearBeginningMonth,
allowOverdraft);
periodStartingBalance = postingPeriod.closingBalance();
postingPeriod.setOverdraftInterest(isOverdraftTransacction);
if (!(MathUtil.isZero(postingPeriod.getOpeningBalance().getAmount())
&& MathUtil.isZero(postingPeriod.closingBalance().getAmount()))) {
allPostingPeriods.add(postingPeriod);
}
return periodStartingBalance;
}
public List<PostingPeriod> calculateInterestUsing(final MathContext mc, final LocalDate upToInterestCalculationDate,
boolean isInterestTransfer, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth,
final LocalDate postInterestOnDate, final boolean backdatedTxnsAllowedTill, final SavingsAccountData savingsAccountData) {
@@ -268,16 +305,40 @@ public class SavingsAccountInterestPostingServiceImpl implements SavingsAccountI
if (postedAsOnDates.contains(periodInterval.endDate().plusDays(1))) {
isUserPosting = true;
}
final PostingPeriod postingPeriod = PostingPeriod.createFromDTO(periodInterval, periodStartingBalance,
retreiveOrderedNonInterestPostingTransactions(savingsAccountData), monetaryCurrency, compoundingPeriodType,
interestCalculationType, interestRateAsFraction, daysInYearType.getValue(), upToInterestCalculationDate,
interestPostTransactions, isInterestTransfer, minBalanceForInterestCalculation,
isSavingsInterestPostingAtCurrentPeriodEnd, overdraftInterestRateAsFraction, minOverdraftForInterestCalculation,
isUserPosting, financialYearBeginningMonth, savingsAccountData.isAllowOverdraft());
if (savingsAccountData.isAllowOverdraft() && !MathUtil.isZero(savingsAccountData.getGlAccountIdForInterestReceivable())) {
periodStartingBalance = postingPeriod.closingBalance();
List<SavingsAccountTransactionData> overdraftTxs = listForOverdraft(savingsAccountData, periodInterval);
List<SavingsAccountTransactionData> interestPostingTxs = listForInterestPosting(savingsAccountData, periodInterval,
monetaryCurrency);
allPostingPeriods.add(postingPeriod);
boolean isOverdraftAccountType = isOverdraftAccount(savingsAccountData, periodInterval, monetaryCurrency);
List<SavingsAccountTransactionData> primaryInterestPublication = isOverdraftAccountType ? overdraftTxs : interestPostingTxs;
List<SavingsAccountTransactionData> secondaryInterestPublication = isOverdraftAccountType ? interestPostingTxs
: overdraftTxs;
periodStartingBalance = appendPostingPeriodIfAny(periodInterval, periodStartingBalance, primaryInterestPublication,
monetaryCurrency, compoundingPeriodType, interestCalculationType, interestRateAsFraction, daysInYearType.getValue(),
upToInterestCalculationDate, interestPostTransactions, isInterestTransfer, minBalanceForInterestCalculation,
isSavingsInterestPostingAtCurrentPeriodEnd, overdraftInterestRateAsFraction, minOverdraftForInterestCalculation,
isUserPosting, financialYearBeginningMonth, savingsAccountData.isAllowOverdraft(), allPostingPeriods,
isOverdraftAccountType ? true : false);
periodStartingBalance = appendPostingPeriodIfAny(periodInterval, periodStartingBalance, secondaryInterestPublication,
monetaryCurrency, compoundingPeriodType, interestCalculationType, interestRateAsFraction, daysInYearType.getValue(),
upToInterestCalculationDate, interestPostTransactions, isInterestTransfer, minBalanceForInterestCalculation,
isSavingsInterestPostingAtCurrentPeriodEnd, overdraftInterestRateAsFraction, minOverdraftForInterestCalculation,
isUserPosting, financialYearBeginningMonth, savingsAccountData.isAllowOverdraft(), allPostingPeriods,
isOverdraftAccountType ? false : true);
} else {
periodStartingBalance = appendPostingPeriodIfAny(periodInterval, periodStartingBalance,
retreiveOrderedNonInterestPostingTransactions(savingsAccountData), monetaryCurrency, compoundingPeriodType,
interestCalculationType, interestRateAsFraction, daysInYearType.getValue(), upToInterestCalculationDate,
interestPostTransactions, isInterestTransfer, minBalanceForInterestCalculation,
isSavingsInterestPostingAtCurrentPeriodEnd, overdraftInterestRateAsFraction, minOverdraftForInterestCalculation,
isUserPosting, financialYearBeginningMonth, savingsAccountData.isAllowOverdraft(), allPostingPeriods, false);
}
}
this.savingsHelper.calculateInterestForAllPostingPeriods(monetaryCurrency, allPostingPeriods,
@@ -297,6 +358,52 @@ public class SavingsAccountInterestPostingServiceImpl implements SavingsAccountI
return allPostingPeriods;
}
private List<SavingsAccountTransactionData> listForOverdraft(final SavingsAccountData savingsAccountData,
final LocalDateInterval periodInterval) {
List<SavingsAccountTransactionData> overdraftTransactionsInPeriod = new ArrayList<>();
for (SavingsAccountTransactionData lists : retreiveOrderedNonInterestPostingTransactions(savingsAccountData)) {
if (MathUtil.isLessThanZero(lists.getRunningBalance()) && periodInterval.startDate().getMonth() == lists.getDate().getMonth()) {
overdraftTransactionsInPeriod.add(lists);
}
}
return overdraftTransactionsInPeriod;
}
private List<SavingsAccountTransactionData> listForInterestPosting(final SavingsAccountData savingsAccountData,
final LocalDateInterval periodInterval, final MonetaryCurrency currency) {
final List<SavingsAccountTransactionData> nonOverdraftTransactions = new ArrayList<>();
for (final SavingsAccountTransactionData tx : retreiveOrderedNonInterestPostingTransactions(savingsAccountData)) {
if (periodInterval.startDate().getMonth() == tx.getDate().getMonth()) {
final Money runningBalance = Money.of(currency, tx.getRunningBalance());
if (runningBalance.isGreaterThanZero() && !runningBalance.isZero()) {
nonOverdraftTransactions.add(tx);
}
}
}
return nonOverdraftTransactions;
}
private Boolean isOverdraftAccount(final SavingsAccountData savingsAccountData, final LocalDateInterval periodInterval,
final MonetaryCurrency currency) {
for (SavingsAccountTransactionData tx : retreiveOrderedNonInterestPostingTransactions(savingsAccountData)) {
if (MathUtil.isLessThanZero(tx.getRunningBalance()) && periodInterval.startDate().getMonth() == tx.getDate().getMonth()) {
return true;
} else if (periodInterval.startDate().getMonth() == tx.getDate().getMonth()) {
final Money runningBalance = Money.of(currency, tx.getRunningBalance());
if (!runningBalance.isZero()) {
return false;
}
}
}
return false;
}
private List<SavingsAccountTransactionData> retreiveOrderedNonInterestPostingTransactions(final SavingsAccountData savingsAccountData) {
final List<SavingsAccountTransactionData> listOfTransactionsSorted = retrieveListOfTransactions(savingsAccountData);
@@ -509,6 +616,18 @@ public class SavingsAccountInterestPostingServiceImpl implements SavingsAccountI
return postingTransation;
}
protected SavingsAccountTransactionData findInterestPostingTransactionForInterest(final LocalDate postingDate,
final SavingsAccountData savingsAccountData, boolean isOverdraft) {
SavingsAccountTransactionData postingTransation = null;
List<SavingsAccountTransactionData> transactions = savingsAccountData.getSavingsAccountTransactionData();
postingTransation = transactions.stream().filter(t -> {
Boolean interestSearch = isOverdraft ? t.isOverdraftInterestAndNotReversed() : t.isInterestPostingAndNotReversed();
return interestSearch && t.occursOn(postingDate) && !t.isReversalTransaction();
}).findFirst().orElse(null);
return postingTransation;
}
protected void resetAccountTransactionsEndOfDayBalances(final List<SavingsAccountTransactionData> accountTransactionsSorted,
final LocalDate interestPostingUpToDate, final SavingsAccountData savingsAccountData) {
// loop over transactions in reverse
@@ -337,6 +337,9 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
"msac.id as chargeId, msac.amount as chargeAmount, msac.charge_time_enum as chargeTimeType, msac.is_penalty as isPenaltyCharge, ");
sqlBuilder.append("txd.id as taxDetailsId, txd.amount as taxAmount, ");
sqlBuilder.append("apm.gl_account_id as glAccountIdForInterestOnSavings, apm1.gl_account_id as glAccountIdForSavingsControl, ");
sqlBuilder.append(
"apm2.gl_account_id as glAccountIdForInterestReceivable,apm3.gl_account_id as glAccountIdForOverdraftPorfolio, ");
sqlBuilder.append("apm4.gl_account_id as glAccountIdForInterestPayable, ");
sqlBuilder.append(
"mtc.id as taxComponentId, mtc.debit_account_id as debitAccountId, mtc.credit_account_id as creditAccountId, mtc.percentage as taxPercentage ");
sqlBuilder.append("from m_savings_account sa ");
@@ -356,6 +359,9 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
"left join acc_product_mapping apm on apm.product_type = 2 and apm.product_id = sp.id and apm.financial_account_type=3 ");
sqlBuilder.append(
"left join acc_product_mapping apm1 on apm1.product_type = 2 and apm1.product_id = sp.id and apm1.financial_account_type=2 ");
sqlBuilder.append("left join acc_product_mapping apm2 on apm2.product_id = sp.id and apm2.financial_account_type=18 ");
sqlBuilder.append("left join acc_product_mapping apm3 on apm3.product_id = sp.id and apm3.financial_account_type = 11 ");
sqlBuilder.append("left join acc_product_mapping apm4 on apm4.product_id = sp.id and apm4.financial_account_type = 17 ");
this.schemaSql = sqlBuilder.toString();
}
@@ -409,6 +415,11 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
final Long glAccountIdForInterestOnSavings = rs.getLong("glAccountIdForInterestOnSavings");
final Long glAccountIdForSavingsControl = rs.getLong("glAccountIdForSavingsControl");
final Long glAccountIdForOverdraftPorfolio = rs.getLong("glAccountIdForOverdraftPorfolio");
final Long glAccountIdForInterestReceivable = rs.getLong("glAccountIdForInterestReceivable");
final Long glAccountIdForInterestPayable = rs.getLong("glAccountIdForInterestPayable");
final Long productId = rs.getLong("productId");
final Integer accountType = rs.getInt("accountingType");
final AccountingRuleType accountingRuleType = AccountingRuleType.fromInt(accountType);
@@ -565,6 +576,12 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
savingsAccountData.setClientData(clientData);
savingsAccountData.setGroupGeneralData(groupGeneralData);
savingsAccountData.setSavingsProduct(savingsProductData);
savingsAccountData.setGlAccountIdForInterestReceivable(glAccountIdForInterestReceivable);
savingsAccountData.setGlAccountIdForOverdraftPorfolio(glAccountIdForOverdraftPorfolio);
savingsAccountData.setGlAccountIdForInterestPayable(glAccountIdForInterestPayable);
savingsAccountData.setGlAccountIdForInterestOnSavings(glAccountIdForInterestOnSavings);
savingsAccountData.setGlAccountIdForSavingsControl(glAccountIdForSavingsControl);
}
@@ -595,6 +595,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
for (SavingsAccountTransactionData accountTransaction : transactions) {
if (accountTransaction.getId() == null) {
savingsAccountData.setNewSavingsAccountTransactionData(accountTransaction);
selectAccountId(accountTransaction, savingsAccountData);
}
}
}
@@ -604,6 +605,23 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
return savingsAccountData;
}
public void selectAccountId(SavingsAccountTransactionData accountTransaction, SavingsAccountData savingsAccountData) {
SavingsAccountTransactionType transactionType = SavingsAccountTransactionType
.fromInt(accountTransaction.getTransactionType().getId().intValue());
if (transactionType.isOverDraftInterestPosting()) {
if (MathUtil.isGreaterThanZero(accountTransaction.getRunningBalance())) {
accountTransaction.setAccountDebit(savingsAccountData.getGlAccountIdForSavingsControl());
accountTransaction.setAccountCredit(savingsAccountData.getGlAccountIdForInterestReceivable());
} else {
accountTransaction.setAccountDebit(savingsAccountData.getGlAccountIdForOverdraftPorfolio());
accountTransaction.setAccountCredit(savingsAccountData.getGlAccountIdForInterestReceivable());
}
} else {
accountTransaction.setAccountDebit(savingsAccountData.getGlAccountIdForInterestPayable());
accountTransaction.setAccountCredit(savingsAccountData.getGlAccountIdForSavingsControl());
}
}
@Override
public CommandProcessingResult reverseTransaction(final Long savingsId, final Long transactionId,
final boolean allowAccountTransferModification, final JsonCommand command) {