Skip to content

Commit 747121b

Browse files
somasorosdpcadamsaghy
authored andcommitted
FINERACT-2354: Re-aging: -Interest Handling Option: Equal amortization
1 parent 47e8674 commit 747121b

File tree

17 files changed

+3126
-121
lines changed

17 files changed

+3126
-121
lines changed

fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,4 +461,17 @@ public static LocalDateTime convertDateTimeStringToLocalDateTime(String dateTime
461461
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors, e);
462462
}
463463
}
464+
465+
/**
466+
* Returns the earlier date. If date1 is before date2 it return date1 otherwise date2.
467+
*
468+
* @param date1
469+
* non null date1
470+
* @param date2
471+
* non null date2
472+
* @return earlier date
473+
*/
474+
public static LocalDate min(@NonNull LocalDate date1, @NonNull LocalDate date2) {
475+
return date1.isBefore(date2) ? date1 : date2;
476+
}
464477
}

fineract-e2e-tests-runner/src/test/resources/features/LoanReAging.feature

Lines changed: 1676 additions & 3 deletions
Large diffs are not rendered by default.

fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,9 +261,21 @@ public LoanRepaymentScheduleInstallment(Loan loan, Integer installmentNumber, Lo
261261
}
262262

263263
public static LoanRepaymentScheduleInstallment newReAgedInstallment(final Loan loan, final Integer installmentNumber,
264-
final LocalDate fromDate, final LocalDate dueDate, final BigDecimal principal) {
265-
return new LoanRepaymentScheduleInstallment(loan, installmentNumber, fromDate, dueDate, principal, null, null, null, null, null,
266-
null, null, false, false, true);
264+
final LocalDate fromDate, final LocalDate dueDate, final BigDecimal principal, final BigDecimal interest, final BigDecimal fees,
265+
final BigDecimal penalties, final BigDecimal interestAccrued, final BigDecimal feeAccrued, final BigDecimal penaltyAccrued) {
266+
LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, installmentNumber, fromDate, dueDate,
267+
principal, interest, fees, penalties, null, null, null, null, false, false, true);
268+
installment.setInterestAccrued(interestAccrued);
269+
installment.setFeeAccrued(feeAccrued);
270+
installment.setPenaltyAccrued(penaltyAccrued);
271+
return installment;
272+
}
273+
274+
public static LoanRepaymentScheduleInstallment newReAgedInstallment(final Loan loan, final Integer installmentNumber,
275+
final LocalDate fromDate, final LocalDate dueDate, final BigDecimal principal, final BigDecimal interest, final BigDecimal fees,
276+
final BigDecimal penalties) {
277+
return new LoanRepaymentScheduleInstallment(loan, installmentNumber, fromDate, dueDate, principal, interest, fees, penalties, null,
278+
null, null, null, false, false, true);
267279
}
268280

269281
public static LoanRepaymentScheduleInstallment getLastNonDownPaymentInstallment(List<LoanRepaymentScheduleInstallment> installments) {

fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java

Lines changed: 489 additions & 90 deletions
Large diffs are not rendered by default.

fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,14 @@
2424
import java.time.LocalDate;
2525
import java.util.List;
2626
import java.util.Optional;
27+
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
2728
import org.apache.fineract.organisation.monetary.domain.Money;
2829
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
2930
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
31+
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
3032
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
3133
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
34+
import org.apache.fineract.portfolio.loanproduct.calc.data.EqualAmortizationValues;
3235
import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails;
3336
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
3437
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
@@ -151,4 +154,18 @@ void updateModelRepaymentPeriodsDuringReAge(ProgressiveLoanInterestScheduleModel
151154
*/
152155
@NotNull
153156
Money getOutstandingInterestTillDate(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate tillDate);
157+
158+
OutstandingDetails precalculateReAgeEqualAmortizationAmount(ProgressiveLoanInterestScheduleModel interestSchedule,
159+
LocalDate transactionDate, LoanReAgeParameter reageParameter);
160+
161+
void reAgeEqualAmortization(ProgressiveLoanInterestScheduleModel interestSchedule, LocalDate transactionDate,
162+
LoanReAgeParameter reageParameter, Money feesPenaltiesOutstanding,
163+
EqualAmortizationValues feesPenaltiesEqualAmortizationValues);
164+
165+
EqualAmortizationValues calculateEqualAmortizationValues(Money totalOutstanding, Integer numberOfInstallments,
166+
Integer installmentAmountInMultiplesOf, MonetaryCurrency currency);
167+
168+
EqualAmortizationValues calculateAdjustedEqualAmortizationValues(Money outstanding, Money total,
169+
Money sumOfOtherEqualAmortizationValues, Integer numberOfInstallments, Integer installmentAmountInMultiplesOf,
170+
MonetaryCurrency currency);
154171
}

fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.apache.fineract.infrastructure.core.service.DateUtils;
4040
import org.apache.fineract.infrastructure.core.service.MathUtil;
4141
import org.apache.fineract.organisation.monetary.data.CurrencyData;
42+
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
4243
import org.apache.fineract.organisation.monetary.domain.Money;
4344
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
4445
import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType;
@@ -47,11 +48,14 @@
4748
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
4849
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
4950
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
51+
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeInterestHandlingType;
52+
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
5053
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
5154
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
5255
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
5356
import org.apache.fineract.portfolio.loanproduct.calc.data.EmiAdjustment;
5457
import org.apache.fineract.portfolio.loanproduct.calc.data.EmiChangeOperation;
58+
import org.apache.fineract.portfolio.loanproduct.calc.data.EqualAmortizationValues;
5559
import org.apache.fineract.portfolio.loanproduct.calc.data.InterestPeriod;
5660
import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails;
5761
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
@@ -1584,4 +1588,185 @@ private long getUncountablePeriods(final List<RepaymentPeriod> relatedRepaymentP
15841588
.filter(repaymentPeriod -> originalEmi.isLessThan(repaymentPeriod.getTotalPaidAmount())) //
15851589
.count(); //
15861590
}
1591+
1592+
private void accelerateRepaymentDueDateTo(ProgressiveLoanInterestScheduleModel interestSchedule, RepaymentPeriod repaymentPeriod,
1593+
LocalDate transactionDate) {
1594+
repaymentPeriod.setDueDate(transactionDate);
1595+
repaymentPeriod.getInterestPeriods().getLast().setDueDate(transactionDate);
1596+
calculateRateFactorForRepaymentPeriod(repaymentPeriod, interestSchedule);
1597+
}
1598+
1599+
private void accelerateMaturityDateTo(ProgressiveLoanInterestScheduleModel interestSchedule, LocalDate transactionDate) {
1600+
Optional<RepaymentPeriod> repaymentPeriod = interestSchedule.findRepaymentPeriod(transactionDate);
1601+
if (repaymentPeriod.isPresent()) {
1602+
if (!repaymentPeriod.get().getDueDate().isEqual(transactionDate)) {
1603+
accelerateRepaymentDueDateTo(interestSchedule, repaymentPeriod.get(), transactionDate);
1604+
}
1605+
if (!interestSchedule.isLastRepaymentPeriod(repaymentPeriod.get())) {
1606+
interestSchedule.repaymentPeriods().removeIf(rp -> rp.getDueDate().isAfter(transactionDate));
1607+
}
1608+
}
1609+
}
1610+
1611+
private void updateEMIForReAgeEqualAmortization(List<RepaymentPeriod> repaymentPeriods, Money principal, Money interest,
1612+
Money feesPenaltiesOutstanding, EqualAmortizationValues feesPenaltiesEqualAmortizationValues, MonetaryCurrency currency) {
1613+
EqualAmortizationValues interestEAV = calculateEqualAmortizationValues(interest, repaymentPeriods.size(), null, currency);
1614+
EqualAmortizationValues principalEAV = calculateAdjustedEqualAmortizationValues(principal,
1615+
principal.add(interest).add(feesPenaltiesOutstanding),
1616+
interestEAV.value().add(feesPenaltiesEqualAmortizationValues.value()), repaymentPeriods.size(), null, currency);
1617+
RepaymentPeriod last = repaymentPeriods.getLast();
1618+
EqualAmortizationValues emiAEV = principalEAV.add(interestEAV);
1619+
repaymentPeriods.forEach(rp -> {
1620+
boolean isLast = last.equals(rp);
1621+
rp.setReAgedInterest(interestEAV.calculateValue(isLast));
1622+
Money emi = emiAEV.calculateValue(isLast);
1623+
rp.setEmi(emi);
1624+
rp.setOriginalEmi(emi);
1625+
});
1626+
}
1627+
1628+
@Override
1629+
public OutstandingDetails precalculateReAgeEqualAmortizationAmount(ProgressiveLoanInterestScheduleModel interestSchedule,
1630+
LocalDate transactionDate, LoanReAgeParameter reageParameter) {
1631+
return getOutstandingAmountsTillDate(interestSchedule,
1632+
reageParameter.getInterestHandlingType().equals(LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_PAYABLE_INTEREST)
1633+
? transactionDate
1634+
: interestSchedule.getMaturityDate());
1635+
}
1636+
1637+
@Override
1638+
public void reAgeEqualAmortization(ProgressiveLoanInterestScheduleModel interestSchedule, LocalDate transactionDate,
1639+
LoanReAgeParameter reageParameter, Money feesPenaltiesOutstanding,
1640+
EqualAmortizationValues feesPenaltiesEqualAmortizationValues) {
1641+
LocalDate originalMaturityDate = interestSchedule.getMaturityDate();
1642+
boolean isAfterOriginalMaturityDate = transactionDate.isAfter(originalMaturityDate);
1643+
List<RepaymentPeriod> reAgedRepaymentPeriods = new ArrayList<>(reageParameter.getNumberOfInstallments());
1644+
OutstandingDetails reAgeingAmounts = precalculateReAgeEqualAmortizationAmount(interestSchedule, transactionDate, reageParameter);
1645+
1646+
// calculate already paid balances from transaction date
1647+
OutstandingDetails paidBalancesFromTransactionDate = calculatePaidBalancesAfterDate(interestSchedule, transactionDate);
1648+
1649+
// set maturity date to transaction date and remove all repayment periods after it.
1650+
accelerateMaturityDateTo(interestSchedule, transactionDate);
1651+
1652+
// close all open repayment period while keep paid amounts
1653+
interestSchedule.repaymentPeriods().forEach(rp -> {
1654+
rp.getInterestPeriods().getLast()
1655+
.addCreditedInterestAmount(MathUtil.min(rp.getOutstandingInterest(), rp.getCreditedInterest(), false).negated());
1656+
rp.setEmi(rp.getTotalPaidAmount());
1657+
rp.setOutstandingMovedDueToReAging(true);
1658+
});
1659+
1660+
// stop calculate unrecognised interest at this point because all
1661+
interestSchedule.getLastRepaymentPeriod().setNoUnrecognisedInterest(true);
1662+
1663+
if (!paidBalancesFromTransactionDate.getOutstandingInterest().isZero()
1664+
|| !paidBalancesFromTransactionDate.getOutstandingPrincipal().isZero()) {
1665+
createRepaymentPeriodForEarlyRepaidAmountsDuringReAgeing(interestSchedule,
1666+
paidBalancesFromTransactionDate.getOutstandingPrincipal(), paidBalancesFromTransactionDate.getOutstandingInterest());
1667+
}
1668+
1669+
updateModelForReageEqualAmortization(interestSchedule, reageParameter, reAgedRepaymentPeriods, isAfterOriginalMaturityDate);
1670+
1671+
updateEMIForReAgeEqualAmortization(reAgedRepaymentPeriods, reAgeingAmounts.getOutstandingPrincipal(),
1672+
reAgeingAmounts.getOutstandingInterest(), feesPenaltiesOutstanding, feesPenaltiesEqualAmortizationValues,
1673+
interestSchedule.zero().getCurrency());
1674+
1675+
calculateOutstandingBalance(interestSchedule);
1676+
1677+
LocalDate zeroInterestFrom = DateUtils.min(transactionDate, originalMaturityDate);
1678+
interestSchedule.addInterestRate(zeroInterestFrom, BigDecimal.ZERO);
1679+
1680+
calculateLastUnpaidRepaymentPeriodEMI(interestSchedule, transactionDate);
1681+
1682+
}
1683+
1684+
private void updateModelForReageEqualAmortization(ProgressiveLoanInterestScheduleModel interestSchedule,
1685+
LoanReAgeParameter reageParameter, List<RepaymentPeriod> reAgedRepaymentPeriods, boolean isAfterOriginalMaturityDate) {
1686+
int numberOfInstallmentsToAdd = reageParameter.getNumberOfInstallments();
1687+
LocalDate toDate = reageParameter.getStartDate();
1688+
RepaymentPeriod previous = interestSchedule.getLastRepaymentPeriod();
1689+
int frequency = reageParameter.getFrequencyNumber();
1690+
PeriodFrequencyType frequencyType = reageParameter.getFrequencyType();
1691+
1692+
if (!isAfterOriginalMaturityDate) {
1693+
// merge first reaged period
1694+
RepaymentPeriod firstReAgedPeriod = interestSchedule.getLastRepaymentPeriod();
1695+
firstReAgedPeriod.setDueDate(toDate);
1696+
firstReAgedPeriod.getLastInterestPeriod().setDueDate(toDate);
1697+
firstReAgedPeriod.setReAged(true);
1698+
firstReAgedPeriod.getPrevious().ifPresent(prev -> prev.setNoUnrecognisedInterest(true));
1699+
reAgedRepaymentPeriods.add(firstReAgedPeriod);
1700+
1701+
// update params for next reage repayment period calculation
1702+
numberOfInstallmentsToAdd--;
1703+
toDate = scheduledDateGenerator.getRepaymentPeriodDate(frequencyType, frequency, toDate);
1704+
}
1705+
1706+
// insert new reaged repayment periods
1707+
for (int i = 0; i < numberOfInstallmentsToAdd; i++) {
1708+
RepaymentPeriod repaymentPeriod = RepaymentPeriod.create(previous, previous.getDueDate(), toDate, interestSchedule.zero(),
1709+
previous.getMc(), previous.getLoanProductRelatedDetail());
1710+
repaymentPeriod.setTotalCapitalizedIncomeAmount(previous.getTotalCapitalizedIncomeAmount());
1711+
repaymentPeriod.setTotalDisbursedAmount(previous.getTotalDisbursedAmount());
1712+
repaymentPeriod.setReAged(true);
1713+
interestSchedule.repaymentPeriods().add(repaymentPeriod);
1714+
reAgedRepaymentPeriods.add(repaymentPeriod);
1715+
previous = repaymentPeriod;
1716+
toDate = scheduledDateGenerator.getRepaymentPeriodDate(frequencyType, frequency, toDate);
1717+
}
1718+
}
1719+
1720+
private void createRepaymentPeriodForEarlyRepaidAmountsDuringReAgeing(ProgressiveLoanInterestScheduleModel interestSchedule,
1721+
Money totalPaidPrincipal, Money totalPaidInterest) {
1722+
RepaymentPeriod targetPeriod = interestSchedule.getLastRepaymentPeriod();
1723+
1724+
Money paidInterestToAdd = totalPaidInterest.minus(targetPeriod.getPaidInterest());
1725+
Money paidPrincipalToAdd = totalPaidPrincipal.minus(targetPeriod.getPaidPrincipal());
1726+
targetPeriod.addPaidInterestAmount(paidInterestToAdd);
1727+
targetPeriod.addPaidPrincipalAmount(paidPrincipalToAdd);
1728+
targetPeriod.setEmi(targetPeriod.getTotalPaidAmount());
1729+
targetPeriod.setReAged(true);
1730+
targetPeriod.setReAgedEarlyRepaymentHolder(true);
1731+
1732+
RepaymentPeriod repaymentPeriodToInsert = RepaymentPeriod.create(targetPeriod, targetPeriod.getDueDate(),
1733+
interestSchedule.getMaturityDate(), interestSchedule.zero(), interestSchedule.mc(),
1734+
interestSchedule.loanProductRelatedDetail());
1735+
repaymentPeriodToInsert.setReAged(true);
1736+
interestSchedule.repaymentPeriods().add(repaymentPeriodToInsert);
1737+
}
1738+
1739+
private OutstandingDetails calculatePaidBalancesAfterDate(ProgressiveLoanInterestScheduleModel interestSchedule,
1740+
LocalDate transactionDate) {
1741+
Money principal = interestSchedule.repaymentPeriods().stream().filter(rp -> !rp.getDueDate().isBefore(transactionDate))
1742+
.map(RepaymentPeriod::getPaidPrincipal).reduce(interestSchedule.zero(), Money::add);
1743+
Money interest = interestSchedule.repaymentPeriods().stream().filter(rp -> !rp.getDueDate().isBefore(transactionDate))
1744+
.map(RepaymentPeriod::getPaidInterest).reduce(interestSchedule.zero(), Money::add);
1745+
return new OutstandingDetails(principal, interest);
1746+
}
1747+
1748+
@Override
1749+
public EqualAmortizationValues calculateEqualAmortizationValues(Money totalOutstanding, Integer numberOfInstallments,
1750+
Integer installmentAmountInMultiplesOf, MonetaryCurrency currency) {
1751+
if (totalOutstanding.isGreaterThanZero()) {
1752+
Money equalMonthlyValue = totalOutstanding.dividedBy(numberOfInstallments, totalOutstanding.getMc());
1753+
if (installmentAmountInMultiplesOf != null) {
1754+
equalMonthlyValue = Money.roundToMultiplesOf(equalMonthlyValue, installmentAmountInMultiplesOf);
1755+
}
1756+
Money adjustmentForLastInstallment = totalOutstanding.minus(equalMonthlyValue.multipliedBy(numberOfInstallments));
1757+
return new EqualAmortizationValues(equalMonthlyValue, adjustmentForLastInstallment);
1758+
}
1759+
return new EqualAmortizationValues(Money.zero(currency), Money.zero(currency));
1760+
}
1761+
1762+
@Override
1763+
public EqualAmortizationValues calculateAdjustedEqualAmortizationValues(Money outstanding, Money total,
1764+
Money sumOfOtherEqualAmortizationValues, Integer numberOfInstallments, Integer installmentAmountInMultiplesOf,
1765+
MonetaryCurrency currency) {
1766+
EqualAmortizationValues calculatedEMI = calculateEqualAmortizationValues(total, numberOfInstallments,
1767+
installmentAmountInMultiplesOf, currency);
1768+
Money value = calculatedEMI.value().minus(sumOfOtherEqualAmortizationValues);
1769+
Money adjust = outstanding.minus(value.multipliedBy(numberOfInstallments));
1770+
return new EqualAmortizationValues(value, adjust);
1771+
}
15871772
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.portfolio.loanproduct.calc.data;
20+
21+
import java.math.BigDecimal;
22+
import org.apache.fineract.organisation.monetary.domain.Money;
23+
24+
public record EqualAmortizationValues(Money value, Money adjustment) {
25+
26+
public Money getAdjustedValue() {
27+
return value.add(adjustment);
28+
}
29+
30+
public Money calculateValue(boolean isLast) {
31+
return (isLast ? getAdjustedValue() : value);
32+
}
33+
34+
public BigDecimal calculateValueBigDecimal(boolean isLast) {
35+
return calculateValue(isLast).getAmount();
36+
}
37+
38+
public EqualAmortizationValues add(EqualAmortizationValues other) {
39+
return new EqualAmortizationValues(value.add(other.value), adjustment.add(other.adjustment));
40+
}
41+
}

fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ public void addCapitalizedIncomePrincipalAmount(final Money additionalCapitalize
138138
}
139139

140140
public BigDecimal getCalculatedDueInterest() {
141-
if (isPaused()) {
141+
if (isPaused() || getRepaymentPeriod().isReAged()) {
142142
return getCreditedInterest().getAmount();
143143
}
144144

0 commit comments

Comments
 (0)