FINERACT-2354: [BE] chargeback handling with backdated re-age for last adjustment strategy with equal amortization re-aging behaviour

This commit is contained in:
Soma Sörös
2025-12-10 13:10:33 +01:00
committed by Adam Saghy
parent 1ad45759fd
commit fa5be3f5a5
6 changed files with 293 additions and 94 deletions
@@ -28,6 +28,7 @@ import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
@@ -518,8 +519,9 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
return this.installmentNumber.compareTo(o.installmentNumber);
}
public int compareToByDueDate(LoanRepaymentScheduleInstallment o) {
return this.dueDate.compareTo(o.dueDate);
public int compareToByFromDueDate(LoanRepaymentScheduleInstallment o) {
return Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate)
.thenComparing(LoanRepaymentScheduleInstallment::getFromDate).compare(this, o);
}
public boolean isPrincipalNotCompleted(final MonetaryCurrency currency) {
@@ -3171,7 +3171,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
private void reprocessInstallments(final List<LoanRepaymentScheduleInstallment> installments) {
final AtomicInteger counter = new AtomicInteger(1);
final AtomicReference<LocalDate> previousDueDate = new AtomicReference<>(null);
installments.stream().sorted(LoanRepaymentScheduleInstallment::compareToByDueDate).forEachOrdered(i -> {
installments.stream().sorted(LoanRepaymentScheduleInstallment::compareToByFromDueDate).forEachOrdered(i -> {
i.updateInstallmentNumber(counter.getAndIncrement());
final LocalDate prev = previousDueDate.get();
if (prev != null && (i.isAdditional() || i.isReAged())) {
@@ -3179,6 +3179,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
previousDueDate.set(i.getDueDate());
});
installments.sort(LoanRepaymentScheduleInstallment::compareToByFromDueDate);
}
private LocalDate calculateReAgedInstallmentDueDate(final LoanReAgeParameter reAgeParameter, final LocalDate dueDate) {
@@ -3479,12 +3480,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
BalancesWithPaidInAdvance paidInAdvanceBalances = liftEarlyRepaidBalances(installments, transactionDate, currency,
ctx.getAlreadyProcessedTransactions());
// TODO add as Parameter here: paidInAdvanceBalances.getAggregatedFeeChargesPortion().isGreaterThanZero() ||
// paidInAdvanceBalances.getAggregatedPenaltyChargesPortion().isGreaterThanZero()
emiCalculator.reAgeEqualAmortization(model, transactionDate, loanReAgeParameter,
outstandingBalances.fees.add(outstandingBalances.penalties),
new EqualAmortizationValues(calculatedFees.value().add(calculatedPenalties.value()),
calculatedFees.adjustment().add(calculatedPenalties.adjustment())));
outstandingBalances.fees.add(outstandingBalances.penalties), calculatedFees.add(calculatedPenalties));
installments.removeIf(i -> (i.getInstallmentNumber() != null && !i.isDownPayment() && !i.getDueDate().isBefore(transactionDate)
&& !i.isAdditional()) || (!i.getDueDate().isAfter(model.getMaturityDate()) && i.isAdditional()));
@@ -3494,6 +3491,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
i.setInstallmentNumber(model.repaymentPeriods().size());
});
int reAgedInstallmentIndex = 0;
for (int index = 0; index < model.repaymentPeriods().size(); index++) {
RepaymentPeriod rp = model.repaymentPeriods().get(index);
if (rp.getDueDate().isBefore(transactionDate)) {
@@ -3504,9 +3502,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
installment.setInterestCharged(installment.getInterestPaid());
installment.setPrincipal(installment.getPrincipalCompleted(currency).getAmount());
installment.setInstallmentNumber(index + 1);
installment.setCreditedPrincipal(rp.getCreditedPrincipal().getAmount());
installment.updateObligationsMet(currency, transactionDate);
// TODO add remaining components
} else {
LoanRepaymentScheduleInstallment created = LoanRepaymentScheduleInstallment.newReAgedInstallment(loanTransaction.getLoan(),
index + 1, rp.getFromDate(), rp.getDueDate(), rp.getDuePrincipal().getAmount(), rp.getDueInterest().getAmount(),
@@ -3525,16 +3523,17 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
paidInAdvanceBalances.loanTransactionToRepaymentScheduleMappings.forEach(m -> m.setInstallment(created));
} else {
boolean isLastRepaymentPeriod = model.isLastRepaymentPeriod(rp);
created.setFeeChargesCharged(calculatedFees.calculateValueBigDecimal(isLastRepaymentPeriod));
created.setPenaltyCharges(calculatedPenalties.calculateValueBigDecimal(isLastRepaymentPeriod));
created.setFeeChargesCharged(calculatedFees.calculateValueBigDecimal(reAgedInstallmentIndex));
created.setPenaltyCharges(calculatedPenalties.calculateValueBigDecimal(reAgedInstallmentIndex));
created.setInterestAccrued(calculatedInterestAccrued.calculateValueBigDecimal(isLastRepaymentPeriod));
created.setFeeAccrued(calculatedFeeAccrued.calculateValueBigDecimal(isLastRepaymentPeriod));
created.setPenaltyAccrued(calculatedPenaltyAccrued.calculateValueBigDecimal(isLastRepaymentPeriod));
created.setInterestAccrued(calculatedInterestAccrued.calculateValueBigDecimal(reAgedInstallmentIndex));
created.setFeeAccrued(calculatedFeeAccrued.calculateValueBigDecimal(reAgedInstallmentIndex));
created.setPenaltyAccrued(calculatedPenaltyAccrued.calculateValueBigDecimal(reAgedInstallmentIndex));
createChargeMappingsForInstallment(created, calculatedCharges, isLastRepaymentPeriod);
createChargeMappingsForInstallment(created, calculatedCharges, reAgedInstallmentIndex);
reAgedInstallmentIndex++;
}
created.setCreditedPrincipal(rp.getCreditedPrincipal().getAmount());
created.updateObligationsMet(currency, transactionDate);
installments.add(created);
}
@@ -3549,6 +3548,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments();
LoanReAgeParameter loanReAgeParameter = loanTransaction.getLoanReAgeParameter();
LocalDate transactionDate = loanTransaction.getTransactionDate();
LocalDate originalMaturityDate = installments.stream()
.filter(i -> !i.isDownPayment() && !i.isAdditional() && i.getDueDate() != null)
.map(LoanRepaymentScheduleInstallment::getDueDate).max(LocalDate::compareTo).orElseThrow();
Integer numberOfReAgeInstallments = loanReAgeParameter.getNumberOfInstallments();
Integer installmentAmountInMultiplesOf = loanTransaction.getLoan().getLoanProductRelatedDetail()
@@ -3613,8 +3615,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
return res;
}).reduce(new BalancesWithPaidInAdvance(currency), BalancesWithPaidInAdvance::summarizerAccumulator);
if (!balances.getPrincipal().isZero() || !balances.getInterest().isZero() || !balances.getFee().isZero()
|| !balances.getPenalty().isZero()) {
if (!transactionDate.isAfter(originalMaturityDate)) {
final LoanRepaymentScheduleInstallment earlyRepaidInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(loan,
firstReAgeInstallmentProps.reAgedInstallmentNumber(), firstReAgeInstallmentProps.fromDate(), transactionDate,
@@ -3634,9 +3635,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
InstallmentProcessingHelper.addOneToInstallmentNumberFromInstallment(installments,
earlyRepaidInstallment.getInstallmentNumber());
loan.getRepaymentScheduleInstallments().add(earlyRepaidInstallment);
installments.add(earlyRepaidInstallment);
}
// installment index which excludes earlyRepaidInstallment intallment index.
Integer reAgedInstallmentIndex = 0;
LoanRepaymentScheduleInstallment reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(loan,
firstReAgeInstallmentProps.reAgedInstallmentNumber, firstReAgeInstallmentProps.fromDate, loanReAgeParameter.getStartDate(),
calculatedPrincipal.value().getAmount(), calculatedInterest.value().getAmount(), calculatedFees.value().getAmount(),
@@ -3644,27 +3647,27 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
calculatedFeeAccrued.value().getAmount(), calculatedPenaltyAccrued.value().getAmount());
reAgedInstallment = insertOrReplaceRelatedInstallment(installments, reAgedInstallment, currency, transactionDate);
createChargeMappingsForInstallment(reAgedInstallment, calculatedCharges, false);
createChargeMappingsForInstallment(reAgedInstallment, calculatedCharges, reAgedInstallmentIndex);
reAgedInstallmentIndex++;
for (int i = 1; i < numberOfReAgeInstallments; i++) {
LocalDate calculatedDueDate = scheduledDateGenerator.getRepaymentPeriodDate(loanReAgeParameter.getFrequencyType(),
loanReAgeParameter.getFrequencyNumber(), reAgedInstallment.getDueDate());
calculateReAgedInstallmentDueDate(loanReAgeParameter, reAgedInstallment.getDueDate());
int nextReAgedInstallmentNumber = firstReAgeInstallmentProps.reAgedInstallmentNumber + i;
boolean isLastInstallment = i + 1 == numberOfReAgeInstallments;
reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(reAgedInstallment.getLoan(),
nextReAgedInstallmentNumber, reAgedInstallment.getDueDate(), calculatedDueDate,
calculatedPrincipal.calculateValueBigDecimal(isLastInstallment),
calculatedInterest.calculateValueBigDecimal(isLastInstallment),
calculatedFees.calculateValueBigDecimal(isLastInstallment),
calculatedPenalties.calculateValueBigDecimal(isLastInstallment),
calculatedInterestAccrued.calculateValueBigDecimal(isLastInstallment),
calculatedFeeAccrued.calculateValueBigDecimal(isLastInstallment),
calculatedPenaltyAccrued.calculateValueBigDecimal(isLastInstallment));
calculatedPrincipal.calculateValueBigDecimal(reAgedInstallmentIndex),
calculatedInterest.calculateValueBigDecimal(reAgedInstallmentIndex),
calculatedFees.calculateValueBigDecimal(reAgedInstallmentIndex),
calculatedPenalties.calculateValueBigDecimal(reAgedInstallmentIndex),
calculatedInterestAccrued.calculateValueBigDecimal(reAgedInstallmentIndex),
calculatedFeeAccrued.calculateValueBigDecimal(reAgedInstallmentIndex),
calculatedPenaltyAccrued.calculateValueBigDecimal(reAgedInstallmentIndex));
reAgedInstallment = insertOrReplaceRelatedInstallment(installments, reAgedInstallment, currency, transactionDate);
createChargeMappingsForInstallment(reAgedInstallment, calculatedCharges, isLastInstallment);
createChargeMappingsForInstallment(reAgedInstallment, calculatedCharges, reAgedInstallmentIndex);
reAgedInstallmentIndex++;
}
int lastReAgedInstallmentNumber = reAgedInstallment.getInstallmentNumber();
List<LoanRepaymentScheduleInstallment> toRemove = installments.stream()
@@ -3676,11 +3679,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
private void createChargeMappingsForInstallment(final LoanRepaymentScheduleInstallment installment,
List<ReAgedChargeEqualAmortizationValues> reAgedChargeEqualAmortizationValues, boolean isLastInstallment) {
List<ReAgedChargeEqualAmortizationValues> reAgedChargeEqualAmortizationValues, Integer index) {
reAgedChargeEqualAmortizationValues.forEach(amortizationValue -> {
installment.getInstallmentCharges()
.add(new LoanInstallmentCharge(amortizationValue.equalAmortizationValues.calculateValueBigDecimal(isLastInstallment),
amortizationValue.charge, installment));
installment.getInstallmentCharges().add(new LoanInstallmentCharge(
amortizationValue.equalAmortizationValues.calculateValueBigDecimal(index), amortizationValue.charge, installment));
});
}
@@ -857,7 +857,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
lastInterestPeriod.addBalanceCorrectionAmount(rp.getOutstandingPrincipal().negated());
}
rp.setEmi(rp.getTotalPaidAmount());
rp.setOutstandingMoved(true);
rp.moveOutstandingDueToReAging();
});
}
@@ -1697,15 +1697,14 @@ public final class ProgressiveEMICalculator implements EMICalculator {
EqualAmortizationValues principalEAV = calculateAdjustedEqualAmortizationValues(principal,
principal.add(interest).add(feesPenaltiesOutstanding),
interestEAV.value().add(feesPenaltiesEqualAmortizationValues.value()), repaymentPeriods.size(), null, currency);
RepaymentPeriod last = repaymentPeriods.getLast();
EqualAmortizationValues emiAEV = principalEAV.add(interestEAV);
repaymentPeriods.forEach(rp -> {
boolean isLast = last.equals(rp);
rp.setReAgedInterest(interestEAV.calculateValue(isLast));
Money emi = emiAEV.calculateValue(isLast);
for (int i = 0; i < repaymentPeriods.size(); i++) {
RepaymentPeriod rp = repaymentPeriods.get(i);
rp.setReAgedInterest(interestEAV.calculateValue(i));
Money emi = emiAEV.calculateValue(i);
rp.setEmi(emi);
rp.setOriginalEmi(emi);
});
}
}
@Override
@@ -1722,40 +1721,46 @@ public final class ProgressiveEMICalculator implements EMICalculator {
LoanReAgeParameter reageParameter, Money feesPenaltiesOutstanding,
EqualAmortizationValues feesPenaltiesEqualAmortizationValues) {
LocalDate originalMaturityDate = interestSchedule.getMaturityDate();
boolean isAfterOriginalMaturityDate = transactionDate.isAfter(originalMaturityDate);
List<RepaymentPeriod> reAgedRepaymentPeriods = new ArrayList<>(reageParameter.getNumberOfInstallments());
OutstandingDetails reAgeingAmounts = precalculateReAgeEqualAmortizationAmount(interestSchedule, transactionDate, reageParameter);
Money zero = interestSchedule.zero();
// calculate already paid balances from transaction date
OutstandingDetails paidBalancesFromTransactionDate = calculatePaidBalancesAfterDate(interestSchedule, transactionDate);
// find future chargebacks.
// find future balance corrections
Money futureCreditedPrincipals = interestSchedule.repaymentPeriods().stream()
.filter(rp -> !rp.getFromDate().isBefore(transactionDate)).filter(rp -> rp.getDueDate().isAfter(transactionDate))
.map(RepaymentPeriod::getCreditedPrincipal).reduce(zero, Money::add);
// set maturity date to transaction date and remove all repayment periods after it.
accelerateMaturityDateTo(interestSchedule, transactionDate);
addCredit(interestSchedule, transactionDate, futureCreditedPrincipals, zero);
// close all open repayment period while keep paid amounts
interestSchedule.repaymentPeriods().forEach(rp -> {
rp.getInterestPeriods().getLast()
.addCreditedInterestAmount(MathUtil.min(rp.getOutstandingInterest(), rp.getCreditedInterest(), false).negated());
rp.setEmi(rp.getTotalPaidAmount());
rp.setOutstandingMoved(true);
rp.moveOutstandingDueToReAging();
});
// stop calculate unrecognised interest at this point because all
interestSchedule.getLastRepaymentPeriod().setNoUnrecognisedInterest(true);
if (!paidBalancesFromTransactionDate.getOutstandingInterest().isZero()
|| !paidBalancesFromTransactionDate.getOutstandingPrincipal().isZero()) {
if (!originalMaturityDate.isBefore(transactionDate)) {
createRepaymentPeriodForEarlyRepaidAmountsDuringReAgeing(interestSchedule,
paidBalancesFromTransactionDate.getOutstandingPrincipal(), paidBalancesFromTransactionDate.getOutstandingInterest(),
true);
addFirstReAgedPeriod(interestSchedule, interestSchedule.getLastRepaymentPeriod());
}
updateModelForReageEqualAmortization(interestSchedule, reageParameter, reAgedRepaymentPeriods, isAfterOriginalMaturityDate);
updateModelForReageEqualAmortization(interestSchedule, reageParameter, reAgedRepaymentPeriods);
updateEMIForReAgeEqualAmortization(reAgedRepaymentPeriods, reAgeingAmounts.getOutstandingPrincipal(),
reAgeingAmounts.getOutstandingInterest(), feesPenaltiesOutstanding, feesPenaltiesEqualAmortizationValues,
interestSchedule.zero().getCurrency());
zero.getCurrency());
calculateOutstandingBalance(interestSchedule);
@@ -1767,31 +1772,17 @@ public final class ProgressiveEMICalculator implements EMICalculator {
}
private void updateModelForReageEqualAmortization(ProgressiveLoanInterestScheduleModel interestSchedule,
LoanReAgeParameter reageParameter, List<RepaymentPeriod> reAgedRepaymentPeriods, boolean isAfterOriginalMaturityDate) {
LoanReAgeParameter reageParameter, List<RepaymentPeriod> reAgedRepaymentPeriods) {
int numberOfInstallmentsToAdd = reageParameter.getNumberOfInstallments();
LocalDate toDate = reageParameter.getStartDate();
RepaymentPeriod previous = interestSchedule.getLastRepaymentPeriod();
int frequency = reageParameter.getFrequencyNumber();
PeriodFrequencyType frequencyType = reageParameter.getFrequencyType();
if (!isAfterOriginalMaturityDate) {
// merge first reaged period
RepaymentPeriod firstReAgedPeriod = interestSchedule.getLastRepaymentPeriod();
firstReAgedPeriod.setDueDate(toDate);
firstReAgedPeriod.getLastInterestPeriod().setDueDate(toDate);
firstReAgedPeriod.setReAged(true);
firstReAgedPeriod.getPrevious().ifPresent(prev -> prev.setNoUnrecognisedInterest(true));
reAgedRepaymentPeriods.add(firstReAgedPeriod);
// update params for next reage repayment period calculation
numberOfInstallmentsToAdd--;
toDate = scheduledDateGenerator.getRepaymentPeriodDate(frequencyType, frequency, toDate);
}
// insert new reaged repayment periods
for (int i = 0; i < numberOfInstallmentsToAdd; i++) {
RepaymentPeriod repaymentPeriod = RepaymentPeriod.create(previous, previous.getDueDate(), toDate, interestSchedule.zero(),
previous.getMc(), previous.getLoanProductRelatedDetail());
interestSchedule.mc(), previous.getLoanProductRelatedDetail());
repaymentPeriod.setTotalCapitalizedIncomeAmount(previous.getTotalCapitalizedIncomeAmount());
repaymentPeriod.setTotalDisbursedAmount(previous.getTotalDisbursedAmount());
repaymentPeriod.setReAged(true);
@@ -1818,14 +1809,6 @@ public final class ProgressiveEMICalculator implements EMICalculator {
calculateRateFactorForRepaymentPeriod(targetPeriod, interestSchedule);
}
private static void addFirstReAgedPeriod(ProgressiveLoanInterestScheduleModel interestSchedule, RepaymentPeriod targetPeriod) {
RepaymentPeriod repaymentPeriodToInsert = RepaymentPeriod.create(targetPeriod, targetPeriod.getDueDate(),
interestSchedule.getMaturityDate(), interestSchedule.zero(), interestSchedule.mc(),
interestSchedule.loanProductRelatedDetail());
repaymentPeriodToInsert.setReAged(true);
interestSchedule.repaymentPeriods().add(repaymentPeriodToInsert);
}
private OutstandingDetails calculatePaidBalancesAfterDate(ProgressiveLoanInterestScheduleModel interestSchedule,
LocalDate transactionDate) {
Money principal = interestSchedule.repaymentPeriods().stream().filter(rp -> !rp.getDueDate().isBefore(transactionDate))
@@ -1844,9 +1827,9 @@ public final class ProgressiveEMICalculator implements EMICalculator {
equalMonthlyValue = Money.roundToMultiplesOf(equalMonthlyValue, installmentAmountInMultiplesOf);
}
Money adjustmentForLastInstallment = totalOutstanding.minus(equalMonthlyValue.multipliedBy(numberOfInstallments));
return new EqualAmortizationValues(equalMonthlyValue, adjustmentForLastInstallment);
return new EqualAmortizationValues(totalOutstanding, numberOfInstallments, equalMonthlyValue, adjustmentForLastInstallment);
}
return new EqualAmortizationValues(Money.zero(currency), Money.zero(currency));
return new EqualAmortizationValues(totalOutstanding, numberOfInstallments, Money.zero(currency), Money.zero(currency));
}
@Override
@@ -1857,6 +1840,6 @@ public final class ProgressiveEMICalculator implements EMICalculator {
installmentAmountInMultiplesOf, currency);
Money value = calculatedEMI.value().minus(sumOfOtherEqualAmortizationValues);
Money adjust = outstanding.minus(value.multipliedBy(numberOfInstallments));
return new EqualAmortizationValues(value, adjust);
return new EqualAmortizationValues(outstanding, numberOfInstallments, value, adjust);
}
}
@@ -19,23 +19,38 @@
package org.apache.fineract.portfolio.loanproduct.calc.data;
import java.math.BigDecimal;
import java.util.Objects;
import org.apache.fineract.organisation.monetary.domain.Money;
public record EqualAmortizationValues(Money value, Money adjustment) {
public record EqualAmortizationValues(Money totalOutstanding, Integer numberOfInstallments, Money value, Money adjustment) {
public Money getAdjustedValue() {
return value.add(adjustment);
}
public Money calculateValue(boolean isLast) {
return (isLast ? getAdjustedValue() : value);
/**
* calculates value according to the index of the installments
*
* @param index
* index accepted 0 to number of (installments - 1)
* @return calculated value for the given index
*/
public Money calculateValue(Integer index) {
if (getAdjustedValue().isLessThanZero()) {
return totalOutstanding.minus(value.multipliedBy(index + 1)).isLessThanZero() ? value.zero() : value;
}
return (index == numberOfInstallments - 1 ? getAdjustedValue() : value);
}
public BigDecimal calculateValueBigDecimal(boolean isLast) {
return calculateValue(isLast).getAmount();
public BigDecimal calculateValueBigDecimal(Integer index) {
return calculateValue(index).getAmount();
}
public EqualAmortizationValues add(EqualAmortizationValues other) {
return new EqualAmortizationValues(value.add(other.value), adjustment.add(other.adjustment));
if (!Objects.equals(numberOfInstallments, other.numberOfInstallments)) {
throw new RuntimeException("Incompatible EqualAmortizationValues. numberOfInstallments parameter should match.");
}
return new EqualAmortizationValues(totalOutstanding.add(other.totalOutstanding), numberOfInstallments, value.add(other.value),
adjustment.add(other.adjustment));
}
}
@@ -91,8 +91,10 @@ public class RepaymentPeriod {
@Getter
@Setter
private boolean isOutstandingMoved = false;
private Money creditedPrincipalMovedDueReAge;
@Getter
@Setter
private Money creditedInterestMovedDueReAge;
@Setter
@Getter
private boolean noUnrecognisedInterest;
@@ -125,6 +127,8 @@ public class RepaymentPeriod {
this.reAged = reAged;
this.reAgedEarlyRepaymentHolder = reAgedEarlyRepaymentHolder;
this.reAgedInterest = reAgedInterest;
this.creditedInterestMovedDueReAge = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc);
this.creditedInterestMovedDueReAge = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc);
}
public static RepaymentPeriod empty(RepaymentPeriod previous, MathContext mc, ILoanConfigurationDetails loanProductRelatedDetail) {
@@ -148,7 +152,8 @@ public class RepaymentPeriod {
repaymentPeriod.getPaidPrincipal(), repaymentPeriod.getPaidInterest(), repaymentPeriod.getFutureUnrecognizedInterest(), mc,
repaymentPeriod.getLoanProductRelatedDetail(), repaymentPeriod.isNoUnrecognisedInterest(), repaymentPeriod.isReAged(),
repaymentPeriod.isReAgedEarlyRepaymentHolder(), repaymentPeriod.getReAgedInterest());
newRepaymentPeriod.setOutstandingMoved(repaymentPeriod.isOutstandingMoved());
newRepaymentPeriod.setCreditedPrincipalMovedDueReAge(repaymentPeriod.getCreditedPrincipalMovedDueReAge());
newRepaymentPeriod.setCreditedInterestMovedDueReAge(repaymentPeriod.getCreditedInterestMovedDueReAge());
// There is always at least 1 interest period, by default with same from-due date as repayment period
for (InterestPeriod interestPeriod : repaymentPeriod.getInterestPeriods()) {
newRepaymentPeriod.getInterestPeriods().add(InterestPeriod.copy(newRepaymentPeriod, interestPeriod, mc));
@@ -162,7 +167,8 @@ public class RepaymentPeriod {
repaymentPeriod.getDueDate(), new ArrayList<>(), repaymentPeriod.getEmi(), repaymentPeriod.getOriginalEmi(), zero, zero,
zero, mc, repaymentPeriod.getLoanProductRelatedDetail(), repaymentPeriod.isNoUnrecognisedInterest(),
repaymentPeriod.isReAged(), repaymentPeriod.isReAgedEarlyRepaymentHolder(), repaymentPeriod.getReAgedInterest());
newRepaymentPeriod.setOutstandingMoved(repaymentPeriod.isOutstandingMoved());
newRepaymentPeriod.setCreditedPrincipalMovedDueReAge(repaymentPeriod.getCreditedPrincipalMovedDueReAge());
newRepaymentPeriod.setCreditedInterestMovedDueReAge(repaymentPeriod.getCreditedInterestMovedDueReAge());
// There is always at least 1 interest period, by default with same from-due date as repayment period
for (InterestPeriod interestPeriod : repaymentPeriod.getInterestPeriods()) {
var interestPeriodCopy = InterestPeriod.copy(newRepaymentPeriod, interestPeriod);
@@ -311,7 +317,8 @@ public class RepaymentPeriod {
* @return
*/
public Money getTotalCreditedAmount() {
return isOutstandingMoved ? Money.zero(getCurrency(), getMc()) : getCreditedPrincipal().plus(getCreditedInterest(), getMc());
return getCreditedPrincipal().plus(getCreditedInterest(), getMc()).minus(getCreditedInterestMovedDueReAge(), getMc())
.minus(getCreditedPrincipalMovedDueReAge(), getMc());
}
/**
@@ -480,4 +487,9 @@ public class RepaymentPeriod {
public Money getTotalCapitalizedIncomeAmount() {
return MathUtil.nullToZero(totalCapitalizedIncomeAmount, getCurrency(), getMc());
}
public void moveOutstandingDueToReAging() {
setCreditedPrincipalMovedDueReAge(getCreditedPrincipal());
setCreditedInterestMovedDueReAge(getCreditedInterest());
}
}
@@ -31,6 +31,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
@@ -4239,6 +4240,147 @@ class ProgressiveEMICalculatorTest {
@Nested
public class ReAgeEqualAmortization {
private ProgressiveLoanInterestScheduleModel generateSchedule() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = BigDecimal.valueOf(15.678);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_365.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 17.43, 0.0, 0.0, 1.33, 16.1, 83.9);
checkPeriod(interestSchedule, 0, 1, 17.43, 0.013315561644, 1.3315561644, 1.33, 16.1, 83.9);
checkPeriod(interestSchedule, 1, 0, 17.43, 0.012456493151, 1.04509977537, 1.05, 16.38, 67.52);
checkPeriod(interestSchedule, 2, 0, 17.43, 0.013315561644, 0.899066722202, 0.90, 16.53, 50.99);
checkPeriod(interestSchedule, 3, 0, 17.43, 0.012886027397, 0.657058536972, 0.66, 16.77, 34.22);
checkPeriod(interestSchedule, 4, 0, 17.43, 0.013315561644, 0.455658519458, 0.46, 16.97, 17.25);
checkPeriod(interestSchedule, 5, 0, 17.47, 0.012886027397, 0.222283972598, 0.22, 17.25, 0.0);
return interestSchedule;
}
@Test
public void test_chargeBackOn2ndRP_ReAgeingOn1stRPsDueDate_EQUAL_AMORTIZATION_FULL_INTEREST() {
ProgressiveLoanInterestScheduleModel interestSchedule = generateSchedule();
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(16.1)));
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(1.33)));
emiCalculator.creditPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), Money.of(currency, BigDecimal.valueOf(17.43)));
// No repayment no interest recalculation
LocalDate reAgingStartDate = LocalDate.of(2024, 2, 1);
LocalDate transactionDate = LocalDate.of(2024, 2, 1);
OutstandingDetails outstandingAmountsTillDate = emiCalculator.getOutstandingAmountsTillDate(interestSchedule,
interestSchedule.getMaturityDate());
LoanReAgeParameter reageParameter = new LoanReAgeParameter(null, PeriodFrequencyType.MONTHS, 1, reAgingStartDate, 6,
LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_FULL_INTEREST, null);
// Update the existing model with re-aged periods
emiCalculator.reAgeEqualAmortization(interestSchedule, transactionDate, reageParameter, Money.zero(currency),
new EqualAmortizationValues(Money.zero(currency), 6, Money.zero(currency), Money.zero(currency)));
OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule,
interestSchedule.getMaturityDate());
Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingInterest().getAmount(),
outstandingAmountsTillDateAfterReage.getOutstandingInterest().getAmount());
Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingPrincipal().getAmount(),
outstandingAmountsTillDateAfterReage.getOutstandingPrincipal().getAmount());
}
@Test
public void test_chargeBackOn2ndRP_ReAgeingOn2stRPsDueDate_EQUAL_AMORTIZATION_FULL_INTEREST() {
ProgressiveLoanInterestScheduleModel interestSchedule = generateSchedule();
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(16.1)));
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(1.33)));
emiCalculator.creditPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), Money.of(currency, BigDecimal.valueOf(17.43)));
// No repayment no interest recalculation
LocalDate reAgingStartDate = LocalDate.of(2024, 3, 1);
LocalDate transactionDate = LocalDate.of(2024, 3, 1);
OutstandingDetails outstandingAmountsTillDate = emiCalculator.getOutstandingAmountsTillDate(interestSchedule,
interestSchedule.getMaturityDate());
LoanReAgeParameter reageParameter = new LoanReAgeParameter(null, PeriodFrequencyType.MONTHS, 1, reAgingStartDate, 6,
LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_FULL_INTEREST, null);
// Update the existing model with re-aged periods
emiCalculator.reAgeEqualAmortization(interestSchedule, transactionDate, reageParameter, Money.zero(currency),
new EqualAmortizationValues(Money.zero(currency), 6, Money.zero(currency), Money.zero(currency)));
OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule,
interestSchedule.getMaturityDate());
Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingInterest().getAmount(),
outstandingAmountsTillDateAfterReage.getOutstandingInterest().getAmount());
Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingPrincipal().getAmount(),
outstandingAmountsTillDateAfterReage.getOutstandingPrincipal().getAmount());
}
@Test
public void test_chargeBackOn2ndRP_ReAgeingDuring2stRP_EQUAL_AMORTIZATION_FULL_INTEREST() {
ProgressiveLoanInterestScheduleModel interestSchedule = generateSchedule();
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(16.1)));
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(1.33)));
emiCalculator.creditPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), Money.of(currency, BigDecimal.valueOf(17.43)));
// No repayment no interest recalculation
LocalDate reAgingStartDate = LocalDate.of(2024, 2, 10);
LocalDate transactionDate = LocalDate.of(2024, 2, 10);
OutstandingDetails outstandingAmountsTillDate = emiCalculator.getOutstandingAmountsTillDate(interestSchedule,
interestSchedule.getMaturityDate());
LoanReAgeParameter reageParameter = new LoanReAgeParameter(null, PeriodFrequencyType.MONTHS, 1, reAgingStartDate, 6,
LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_FULL_INTEREST, null);
// Update the existing model with re-aged periods
emiCalculator.reAgeEqualAmortization(interestSchedule, transactionDate, reageParameter, Money.zero(currency),
new EqualAmortizationValues(Money.zero(currency), 6, Money.zero(currency), Money.zero(currency)));
OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule,
interestSchedule.getMaturityDate());
Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingInterest().getAmount(),
outstandingAmountsTillDateAfterReage.getOutstandingInterest().getAmount());
Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingPrincipal().getAmount(),
outstandingAmountsTillDateAfterReage.getOutstandingPrincipal().getAmount());
}
@Test
public void test_transactionInMiddleOfPeriod_EQUAL_AMORTIZATION_FULL_INTEREST_noTransactionTilDate_noInterestRecalc() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
@@ -4291,7 +4433,7 @@ class ProgressiveEMICalculatorTest {
// Update the existing model with re-aged periods
emiCalculator.reAgeEqualAmortization(interestSchedule, loanTransaction.getTransactionDate(), reageParameter,
Money.zero(currency), new EqualAmortizationValues(Money.zero(currency), Money.zero(currency)));
Money.zero(currency), new EqualAmortizationValues(Money.zero(currency), 6, Money.zero(currency), Money.zero(currency)));
OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule,
interestSchedule.getMaturityDate());
@@ -4380,7 +4522,7 @@ class ProgressiveEMICalculatorTest {
// Update the existing model with re-aged periods
emiCalculator.reAgeEqualAmortization(interestSchedule, loanTransaction.getTransactionDate(), reageParameter,
Money.zero(currency), new EqualAmortizationValues(Money.zero(currency), Money.zero(currency)));
Money.zero(currency), new EqualAmortizationValues(Money.zero(currency), 6, Money.zero(currency), Money.zero(currency)));
OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule,
interestSchedule.getMaturityDate());
@@ -4439,7 +4581,7 @@ class ProgressiveEMICalculatorTest {
// Update the existing model with re-aged periods
emiCalculator.reAgeEqualAmortization(interestSchedule, transactionDate, reageParameter, Money.zero(currency),
new EqualAmortizationValues(Money.zero(currency), Money.zero(currency)));
new EqualAmortizationValues(Money.zero(currency), 6, Money.zero(currency), Money.zero(currency)));
OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule,
interestSchedule.getMaturityDate());
@@ -4499,7 +4641,7 @@ class ProgressiveEMICalculatorTest {
// Update the existing model with re-aged periods
emiCalculator.reAgeEqualAmortization(interestSchedule, transactionDate, reageParameter, Money.zero(currency),
new EqualAmortizationValues(Money.zero(currency), Money.zero(currency)));
new EqualAmortizationValues(Money.zero(currency), 6, Money.zero(currency), Money.zero(currency)));
OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule,
interestSchedule.getMaturityDate());
@@ -4582,7 +4724,7 @@ class ProgressiveEMICalculatorTest {
// Update the existing model with re-aged periods
emiCalculator.reAgeEqualAmortization(interestSchedule, transactionDate, reageParameter, Money.zero(currency),
new EqualAmortizationValues(Money.zero(currency), Money.zero(currency)));
new EqualAmortizationValues(Money.zero(currency), 6, Money.zero(currency), Money.zero(currency)));
OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule,
interestSchedule.getMaturityDate());
@@ -4665,7 +4807,7 @@ class ProgressiveEMICalculatorTest {
// Update the existing model with re-aged periods
emiCalculator.reAgeEqualAmortization(interestSchedule, transactionDate, reageParameter, Money.zero(currency),
new EqualAmortizationValues(Money.zero(currency), Money.zero(currency)));
new EqualAmortizationValues(Money.zero(currency), 6, Money.zero(currency), Money.zero(currency)));
OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule,
interestSchedule.getMaturityDate());
@@ -4738,7 +4880,7 @@ class ProgressiveEMICalculatorTest {
// Update the existing model with re-aged periods
emiCalculator.reAgeEqualAmortization(interestSchedule, transactionDate, reageParameter, Money.zero(currency),
new EqualAmortizationValues(Money.zero(currency), Money.zero(currency)));
new EqualAmortizationValues(Money.zero(currency), 6, Money.zero(currency), Money.zero(currency)));
OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule,
interestSchedule.getMaturityDate());
@@ -4811,7 +4953,7 @@ class ProgressiveEMICalculatorTest {
// Update the existing model with re-aged periods
emiCalculator.reAgeEqualAmortization(interestSchedule, transactionDate, reageParameter, Money.zero(currency),
new EqualAmortizationValues(Money.zero(currency), Money.zero(currency)));
new EqualAmortizationValues(Money.zero(currency), 6, Money.zero(currency), Money.zero(currency)));
OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule,
interestSchedule.getMaturityDate());
@@ -4824,6 +4966,49 @@ class ProgressiveEMICalculatorTest {
}
}
@Nested
public class EqualAmortizationValue {
@Test
public void test_AmortizationTotalIsLessThanInstallmentNumber() {
EqualAmortizationValues actual = emiCalculator.calculateEqualAmortizationValues(Money.of(currency, BigDecimal.valueOf(0.04)), 6,
null, MonetaryCurrency.fromCurrencyData(currency));
Assertions.assertEquals(0.01, actual.calculateValueBigDecimal(0).doubleValue());
Assertions.assertEquals(0.01, actual.calculateValueBigDecimal(1).doubleValue());
Assertions.assertEquals(0.01, actual.calculateValueBigDecimal(2).doubleValue());
Assertions.assertEquals(0.01, actual.calculateValueBigDecimal(3).doubleValue());
Assertions.assertEquals(0.0, actual.calculateValueBigDecimal(4).doubleValue());
Assertions.assertEquals(0.0, actual.calculateValueBigDecimal(5).doubleValue());
}
@Test
public void test_AmortizationIsJustBiggerThanInstallmentNumber() {
EqualAmortizationValues actual = emiCalculator.calculateEqualAmortizationValues(Money.of(currency, BigDecimal.valueOf(0.07)), 6,
null, MonetaryCurrency.fromCurrencyData(currency));
Assertions.assertEquals(0.01, actual.calculateValueBigDecimal(0).doubleValue());
Assertions.assertEquals(0.01, actual.calculateValueBigDecimal(1).doubleValue());
Assertions.assertEquals(0.01, actual.calculateValueBigDecimal(2).doubleValue());
Assertions.assertEquals(0.01, actual.calculateValueBigDecimal(3).doubleValue());
Assertions.assertEquals(0.01, actual.calculateValueBigDecimal(4).doubleValue());
Assertions.assertEquals(0.02, actual.calculateValueBigDecimal(5).doubleValue());
}
@Test
public void test_AmortizationNonEdgeCase() {
EqualAmortizationValues actual = emiCalculator.calculateEqualAmortizationValues(Money.of(currency, BigDecimal.valueOf(0.59)), 6,
null, MonetaryCurrency.fromCurrencyData(currency));
Assertions.assertEquals(0.1, actual.calculateValueBigDecimal(0).doubleValue());
Assertions.assertEquals(0.1, actual.calculateValueBigDecimal(1).doubleValue());
Assertions.assertEquals(0.1, actual.calculateValueBigDecimal(2).doubleValue());
Assertions.assertEquals(0.1, actual.calculateValueBigDecimal(3).doubleValue());
Assertions.assertEquals(0.1, actual.calculateValueBigDecimal(4).doubleValue());
Assertions.assertEquals(0.09, actual.calculateValueBigDecimal(5).doubleValue());
}
}
// utilities
private List<LoanScheduleModelRepaymentPeriod> generateExpectedRepaymentPeriods(LocalDate disbursementDate) {
return switch (loanProductRelatedDetail.getRepaymentPeriodFrequencyType()) {