FINERACT-2354: Re-aging - Accrual and Accrual Activity handling - Equal Amortization

This commit is contained in:
Soma Sörös
2026-01-19 19:29:21 +01:00
committed by Adam Saghy
parent 3a6f805d50
commit eecc54c224
10 changed files with 1758 additions and 145 deletions
@@ -2034,7 +2034,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
&& loan.isInterestBearingAndInterestRecalculationEnabled()) {
final BigDecimal interestOutstanding = currentInstallment.getInterestOutstanding(loan.getCurrency()).getAmount();
final BigDecimal newInterest = emiCalculator.getPeriodInterestTillDate(progressiveTransactionCtx.getModel(),
currentInstallment.getFromDate(), currentInstallment.getDueDate(), transactionDate, true).getAmount();
currentInstallment.getFromDate(), currentInstallment.getDueDate(), transactionDate, true, false).getAmount();
if (interestOutstanding.compareTo(BigDecimal.ZERO) > 0 || newInterest.compareTo(BigDecimal.ZERO) > 0) {
currentInstallment.updateInterestCharged(newInterest);
}
@@ -2146,8 +2146,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
installments.stream().filter(installment -> !installment.getFromDate().isAfter(transactionDate)
&& installment.getDueDate().isAfter(transactionDate)).forEach(installment -> {
final BigDecimal interestOutstanding = installment.getInterestOutstanding(currency).getAmount();
final BigDecimal newInterest = emiCalculator.getPeriodInterestTillDate(progressiveTransactionCtx.getModel(),
installment.getFromDate(), installment.getDueDate(), transactionDate, true).getAmount();
installment.getFromDate(), installment.getDueDate(), transactionDate, true, false).getAmount();
if (MathUtil.isGreaterThanZero(interestOutstanding) || MathUtil.isGreaterThanZero(newInterest)) {
final BigDecimal interestRemoved = MathUtil.subtract(MathUtil.nullToZero(installment.getInterestCharged()),
newInterest);
@@ -3390,7 +3391,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
BigDecimal::add);
final BigDecimal newInterest = emiCalculator.getPeriodInterestTillDate(transactionCtx.getModel(), lastPeriod.getFromDate(),
lastPeriod.getDueDate(), transactionDate, false).getAmount();
lastPeriod.getDueDate(), transactionDate, false, false).getAmount();
lastPeriod.setEmi(lastPeriod.getDuePrincipal().add(totalPrincipal).add(newInterest));
@@ -3434,7 +3435,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
if (transactionCtx instanceof ProgressiveTransactionCtx progressiveTransactionCtx
&& loan.isInterestBearingAndInterestRecalculationEnabled()) {
interest = emiCalculator.getPeriodInterestTillDate(progressiveTransactionCtx.getModel(), installment.getFromDate(),
installment.getDueDate(), chargeOffDate, true).getAmount();
installment.getDueDate(), chargeOffDate, true, false).getAmount();
} else {
final BigDecimal totalInterest = installment.getInterestOutstanding(currency).getAmount();
if (LoanChargeOffBehaviour.ZERO_INTEREST.equals(loan.getLoanProductRelatedDetail().getChargeOffBehaviour())
@@ -42,7 +42,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeInterestHandlingType;
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
@@ -249,16 +248,15 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
final LoanTransaction reAgeTransaction = loan.findReAgeTransaction();
final LoanReAgeParameter loanReAgeParameter = reAgeTransaction != null ? reAgeTransaction.getLoanReAgeParameter() : null;
if (!(transactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor processor)) {
if (!(transactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor)) {
throw new IllegalStateException("Expected an AdvancedPaymentScheduleTransactionProcessor");
}
if (installment.isAdditional() || installment.isDownPayment() || (installment.isReAged() && loanReAgeParameter != null
&& !LoanReAgeInterestHandlingType.DEFAULT.equals(loanReAgeParameter.getInterestHandlingType()))) {
if (installment.isAdditional() || installment.isDownPayment()) {
return Money.zero(loan.getCurrency());
}
Optional<ProgressiveLoanInterestScheduleModel> savedModel = interestScheduleModelRepositoryWrapper.getSavedModel(loan, targetDate);
ProgressiveLoanInterestScheduleModel model = savedModel.orElseThrow();
return emiCalculator.getPeriodInterestTillDate(model, installment.getFromDate(), installment.getDueDate(), targetDate, false);
return emiCalculator.getPeriodInterestTillDate(model, installment.getFromDate(), installment.getDueDate(), targetDate, false, true);
}
// Private, internal methods
@@ -124,11 +124,17 @@ public interface EMICalculator {
@NotNull LocalDate periodDueDate, @NotNull LocalDate targetDate);
/**
* Gives back the sum of the interest from the whole model on the given date.
* Gives back the sum of the interest from the whole model on the given date. Fixed interest till date calculation
* flag indicates that the fixed interest should be calculated only till date or not. This flag should be true for
* accrual calculation. It should be false for repayment or repayment schedule related calculation.
*
* @param fixedInterestTillDate
* indicates that fixed interest should be calculated till the date.
*/
@NotNull
Money getPeriodInterestTillDate(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodFromDate,
@NotNull LocalDate periodDueDate, @NotNull LocalDate targetDate, boolean includeChargebackInterest);
@NotNull LocalDate periodDueDate, @NotNull LocalDate targetDate, boolean includeChargebackInterest,
boolean fixedInterestTillDate);
Money getOutstandingLoanBalanceOfPeriod(ProgressiveLoanInterestScheduleModel interestScheduleModel, LocalDate targetDate);
@@ -519,15 +519,21 @@ public final class ProgressiveEMICalculator implements EMICalculator {
@Override
@NotNull
public Money getPeriodInterestTillDate(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodFromDate,
@NotNull LocalDate periodDueDate, @NotNull LocalDate targetDate, boolean includeCreditedInterest) {
ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate = recalculateScheduleModelTillDate(scheduleModel,
public Money getPeriodInterestTillDate(@NotNull final ProgressiveLoanInterestScheduleModel scheduleModel,
@NotNull final LocalDate periodFromDate, @NotNull final LocalDate periodDueDate, @NotNull final LocalDate targetDate,
final boolean includeCreditedInterest, final boolean fixedInterestTillDate) {
final ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate = recalculateScheduleModelTillDate(scheduleModel,
targetDate);
RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate
final MathContext mc = recalculatedScheduleModelTillDate.mc();
final RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate
.findRepaymentPeriodByFromAndDueDate(periodFromDate, periodDueDate).orElseThrow();
return includeCreditedInterest ? repaymentPeriod.getCalculatedDueInterest()
: repaymentPeriod.getCalculatedDueInterest().minus(repaymentPeriod.getCreditedInterest(),
recalculatedScheduleModelTillDate.mc());
Money calculatedDueInterest = repaymentPeriod.getCalculatedDueInterest();
if (fixedInterestTillDate) {
calculatedDueInterest = MathUtil.negativeToZero(
calculatedDueInterest.minus(repaymentPeriod.getFixedInterest()).add(repaymentPeriod.calculateFixedInterestTillDate()),
mc);
}
return includeCreditedInterest ? calculatedDueInterest : calculatedDueInterest.minus(repaymentPeriod.getCreditedInterest(), mc);
}
@Override
@@ -853,7 +859,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
calculateRateFactorForPeriods(futurePeriods, model);
for (final RepaymentPeriod futurePeriod : futurePeriods) {
futurePeriod.setReAgedInterest(MathUtil.plus(futurePeriod.getReAgedInterest(), interestPortionPerPeriod, mc));
futurePeriod.setFixedInterest(MathUtil.plus(futurePeriod.getFixedInterest(), interestPortionPerPeriod, mc));
}
calculateEMIOnActualModel(futurePeriods, model);
@@ -1908,7 +1914,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
EqualAmortizationValues emiAEV = principalEAV.add(interestEAV);
for (int i = 0; i < repaymentPeriods.size(); i++) {
RepaymentPeriod rp = repaymentPeriods.get(i);
rp.setReAgedInterest(interestEAV.calculateValue(i));
rp.setFixedInterest(interestEAV.calculateValue(i));
Money emi = emiAEV.calculateValue(i);
rp.setEmi(emi);
rp.setOriginalEmi(emi);
@@ -1953,6 +1959,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
.addCreditedInterestAmount(MathUtil.min(rp.getOutstandingInterest(), rp.getCreditedInterest(), false).negated());
rp.setEmi(rp.getTotalPaidAmount());
rp.moveOutstandingDueToReAging();
rp.setNoUnrecognisedInterest(true);
});
// stop calculate unrecognised interest at this point because all
@@ -32,6 +32,7 @@ import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.apache.fineract.infrastructure.core.serialization.gson.JsonExclude;
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;
@@ -105,14 +106,13 @@ public class RepaymentPeriod {
@Setter
@Getter
private boolean reAgedEarlyRepaymentHolder;
@Getter
@Setter
private Money reAgedInterest;
private Money fixedInterest;
protected RepaymentPeriod(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, List<InterestPeriod> interestPeriods,
Money emi, Money originalEmi, Money paidPrincipal, Money paidInterest, Money futureUnrecognizedInterest, MathContext mc,
ILoanConfigurationDetails loanProductRelatedDetail, boolean noUnrecognisedInterest, boolean reAged,
boolean reAgedEarlyRepaymentHolder, Money reAgedInterest) {
boolean reAgedEarlyRepaymentHolder, Money fixedInterest) {
this.previous = previous;
this.fromDate = fromDate;
this.dueDate = dueDate;
@@ -127,7 +127,7 @@ public class RepaymentPeriod {
this.noUnrecognisedInterest = noUnrecognisedInterest;
this.reAged = reAged;
this.reAgedEarlyRepaymentHolder = reAgedEarlyRepaymentHolder;
this.reAgedInterest = reAgedInterest;
this.fixedInterest = fixedInterest;
this.creditedInterestMovedDueReAge = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc);
this.creditedInterestMovedDueReAge = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc);
}
@@ -152,7 +152,7 @@ public class RepaymentPeriod {
repaymentPeriod.getDueDate(), new ArrayList<>(), repaymentPeriod.getEmi(), repaymentPeriod.getOriginalEmi(),
repaymentPeriod.getPaidPrincipal(), repaymentPeriod.getPaidInterest(), repaymentPeriod.getFutureUnrecognizedInterest(), mc,
repaymentPeriod.getLoanProductRelatedDetail(), repaymentPeriod.isNoUnrecognisedInterest(), repaymentPeriod.isReAged(),
repaymentPeriod.isReAgedEarlyRepaymentHolder(), repaymentPeriod.getReAgedInterest());
repaymentPeriod.isReAgedEarlyRepaymentHolder(), repaymentPeriod.getFixedInterest());
newRepaymentPeriod.setCreditedPrincipalMovedDueReAge(repaymentPeriod.getCreditedPrincipalMovedDueReAge());
newRepaymentPeriod.setCreditedInterestMovedDueReAge(repaymentPeriod.getCreditedInterestMovedDueReAge());
newRepaymentPeriod.setTotalDisbursedAmount(repaymentPeriod.getTotalDisbursedAmount());
@@ -171,7 +171,7 @@ public class RepaymentPeriod {
final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, repaymentPeriod.getFromDate(),
repaymentPeriod.getDueDate(), new ArrayList<>(), repaymentPeriod.getEmi(), repaymentPeriod.getOriginalEmi(), zero, zero,
zero, mc, repaymentPeriod.getLoanProductRelatedDetail(), repaymentPeriod.isNoUnrecognisedInterest(),
repaymentPeriod.isReAged(), repaymentPeriod.isReAgedEarlyRepaymentHolder(), repaymentPeriod.getReAgedInterest());
repaymentPeriod.isReAged(), repaymentPeriod.isReAgedEarlyRepaymentHolder(), repaymentPeriod.getFixedInterest());
newRepaymentPeriod.setCreditedPrincipalMovedDueReAge(repaymentPeriod.getCreditedPrincipalMovedDueReAge());
newRepaymentPeriod.setCreditedInterestMovedDueReAge(repaymentPeriod.getCreditedInterestMovedDueReAge());
newRepaymentPeriod.setTotalDisbursedAmount(repaymentPeriod.getTotalDisbursedAmount());
@@ -218,11 +218,28 @@ public class RepaymentPeriod {
public Money getCalculatedDueInterest() {
if (calculatedDueInterestCalculation == null) {
calculatedDueInterestCalculation = Memo.of(this::calculateCalculatedDueInterest, () -> new Object[] { previous, interestPeriods,
futureUnrecognizedInterest, isInterestMoved, totalDisbursedAmount, reAgedInterest, reAged });
futureUnrecognizedInterest, isInterestMoved, totalDisbursedAmount, fixedInterest, reAged });
}
return calculatedDueInterestCalculation.get();
}
public Money calculateFixedInterestTillDate() {
Money calculatedFixedInterest = getZero();
if (!getFixedInterest().isZero()) {
long length = DateUtils.getDifferenceInDays(getFromDate(), getDueDate());
if (length == 0 || getInterestPeriods() == null || getInterestPeriods().isEmpty()) {
// if the repayment period length is zero. return reAgedInterest.
calculatedFixedInterest = getFixedInterest();
} else {
long interestCalculationLength = DateUtils.getDifferenceInDays(getInterestPeriods().getFirst().getFromDate(),
getInterestPeriods().getLast().getDueDate());
calculatedFixedInterest = Money.of(getZero().getCurrencyData(), BigDecimal.valueOf(interestCalculationLength)
.divide(BigDecimal.valueOf(length), getMc()).multiply(getFixedInterest().getAmount(), getMc()));
}
}
return calculatedFixedInterest;
}
public Money calculateCalculatedDueInterest() {
Money calculatedDueInterest = getZero();
if (!isInterestMoved()) {
@@ -230,7 +247,7 @@ public class RepaymentPeriod {
getInterestPeriods().stream().map(InterestPeriod::getCalculatedDueInterest).reduce(BigDecimal.ZERO, BigDecimal::add),
mc);
}
calculatedDueInterest = calculatedDueInterest.add(reAgedInterest);
calculatedDueInterest = calculatedDueInterest.add(getFixedInterest());
calculatedDueInterest = calculatedDueInterest.add(getFutureUnrecognizedInterest(), getMc());
if (getPrevious().isPresent()) {
calculatedDueInterest = calculatedDueInterest.add(getPrevious().get().getUnrecognizedInterest(), getMc());
@@ -251,7 +268,7 @@ public class RepaymentPeriod {
: MathUtil.min(getCalculatedDueInterest(), getEmiPlusCreditedAmountsPlusFutureUnrecognizedInterest(), false),
getPaidInterest(), false),
() -> new Object[] { paidPrincipal, paidInterest, interestPeriods, futureUnrecognizedInterest, totalDisbursedAmount,
reAgedInterest, reAged, emi });
fixedInterest, reAged, emi });
}
return dueInterestCalculation.get();
}
@@ -497,6 +514,10 @@ public class RepaymentPeriod {
return MathUtil.nullToZero(totalCapitalizedIncomeAmount, getCurrency(), getMc());
}
public Money getFixedInterest() {
return MathUtil.nullToZero(fixedInterest, getCurrency(), getMc());
}
public void moveOutstandingDueToReAging() {
setCreditedPrincipalMovedDueReAge(getCreditedPrincipal());
setCreditedInterestMovedDueReAge(getCreditedInterest());